diff --git a/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt b/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt index 5eec6cb2..7c0a3cc4 100644 --- a/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt +++ b/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt @@ -84,11 +84,12 @@ class MainViewModelImpl( override fun onError(error: BaseError) { when (error) { - BaseError.SessionExpired -> { + BaseError.SessionExpired, + BaseError.AccountBlocked -> { isNeedToReplaceWithAuth.update { true } } - is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui + else -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui } } 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 830b0f26..6fe89038 100644 --- a/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt +++ b/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt @@ -2,7 +2,7 @@ package dev.meloda.fast.navigation import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import dev.meloda.fast.conversations.navigation.Conversations +import dev.meloda.fast.conversations.navigation.ConversationsGraph import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BottomNavigationItem @@ -21,10 +21,10 @@ object Main fun NavGraphBuilder.mainScreen( onError: (BaseError) -> Unit, onSettingsButtonClicked: () -> Unit, - onConversationClicked: (conversationId: Int) -> Unit, + onNavigateToMessagesHistory: (conversationId: Long) -> Unit, onPhotoClicked: (url: String) -> Unit, - onMessageClicked: (userId: Int) -> Unit, - onCreateChatClicked: () -> Unit + onMessageClicked: (userid: Long) -> Unit, + onNavigateToCreateChat: () -> Unit ) { val navigationItems = ImmutableList.of( BottomNavigationItem( @@ -37,7 +37,7 @@ fun NavGraphBuilder.mainScreen( titleResId = UiR.string.title_conversations, selectedIconResId = UiR.drawable.baseline_chat_24, unselectedIconResId = UiR.drawable.outline_chat_24, - route = Conversations + route = ConversationsGraph ), BottomNavigationItem( titleResId = UiR.string.title_profile, @@ -52,10 +52,10 @@ fun NavGraphBuilder.mainScreen( navigationItems = navigationItems, onError = onError, onSettingsButtonClicked = onSettingsButtonClicked, - onConversationItemClicked = onConversationClicked, + onNavigateToMessagesHistory = onNavigateToMessagesHistory, onPhotoClicked = onPhotoClicked, onMessageClicked = onMessageClicked, - onCreateChatClicked = onCreateChatClicked + onNavigateToCreateChat = onNavigateToCreateChat ) } } 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 3971abc9..15a5c5b1 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt @@ -37,7 +37,7 @@ import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.conversations.navigation.Conversations -import dev.meloda.fast.conversations.navigation.conversationsScreen +import dev.meloda.fast.conversations.navigation.conversationsGraph import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.friends.navigation.friendsScreen import dev.meloda.fast.model.BaseError @@ -46,7 +46,8 @@ import dev.meloda.fast.navigation.MainGraph import dev.meloda.fast.profile.navigation.profileScreen import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState -import dev.meloda.fast.ui.theme.LocalScrollToTop +import dev.meloda.fast.ui.theme.LocalNavController +import dev.meloda.fast.ui.theme.LocalReselectedTab import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalUser import dev.meloda.fast.ui.util.ImmutableList @@ -57,10 +58,10 @@ fun MainScreen( navigationItems: ImmutableList, onError: (BaseError) -> Unit = {}, onSettingsButtonClicked: () -> Unit = {}, - onConversationItemClicked: (conversationId: Int) -> Unit = {}, + onNavigateToMessagesHistory: (conversationId: Long) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {}, - onMessageClicked: (userId: Int) -> Unit = {}, - onCreateChatClicked: () -> Unit = {} + onMessageClicked: (userid: Long) -> Unit = {}, + onNavigateToCreateChat: () -> Unit = {} ) { val theme = LocalThemeConfig.current val hazeState = remember { HazeState() } @@ -75,7 +76,7 @@ fun MainScreen( derivedStateOf { user?.photo100 } } - var scrollToTop by remember { + var tabReselected by remember { mutableStateOf( navigationItems.associate { it.route to false @@ -113,7 +114,7 @@ fun MainScreen( } } } else { - scrollToTop = scrollToTop.toMutableMap().also { + tabReselected = tabReselected.toMutableMap().also { it[navigationItems[index].route] = true } } @@ -164,7 +165,8 @@ fun MainScreen( CompositionLocalProvider( LocalHazeState provides hazeState, LocalBottomPadding provides padding.calculateBottomPadding(), - LocalScrollToTop provides scrollToTop + LocalReselectedTab provides tabReselected, + LocalNavController provides navController ) { NavHost( navController = navController, @@ -182,18 +184,17 @@ fun MainScreen( onPhotoClicked = onPhotoClicked, onMessageClicked = onMessageClicked, onScrolledToTop = { - scrollToTop = scrollToTop.toMutableMap().also { + tabReselected = tabReselected.toMutableMap().also { it[Friends] = false } }, ) - conversationsScreen( + conversationsGraph( onError = onError, - onConversationItemClicked = onConversationItemClicked, - onCreateChatClicked = onCreateChatClicked, - navController = navController, + onNavigateToMessagesHistory = onNavigateToMessagesHistory, + onNavigateToCreateChat = onNavigateToCreateChat, onScrolledToTop = { - scrollToTop = scrollToTop.toMutableMap().also { + tabReselected = tabReselected.toMutableMap().also { it[Conversations] = false } } @@ -201,8 +202,7 @@ fun MainScreen( profileScreen( onError = onError, onSettingsButtonClicked = onSettingsButtonClicked, - onPhotoClicked = onPhotoClicked, - navController = navController + onPhotoClicked = onPhotoClicked ) } } 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 97b2e1cf..a0a318a2 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext @@ -38,6 +39,8 @@ import dev.meloda.fast.photoviewer.navigation.photoViewScreen import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.settingsScreen import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.theme.LocalNavController +import dev.meloda.fast.ui.theme.LocalNavRootController @Composable fun RootScreen( @@ -111,53 +114,59 @@ fun RootScreen( } if (startDestination != null) { - NavHost( - navController = navController, - startDestination = requireNotNull(startDestination), - enterTransition = { fadeIn(animationSpec = tween(200)) }, - exitTransition = { fadeOut(animationSpec = tween(200)) } + CompositionLocalProvider( + LocalNavRootController provides navController, + LocalNavController provides navController ) { - authNavGraph( - onNavigateToMain = { - viewModel.onUserAuthenticated() - navController.navigateToMain() - }, - navController = navController - ) - mainScreen( - onError = viewModel::onError, - onSettingsButtonClicked = navController::navigateToSettings, - onConversationClicked = navController::navigateToMessagesHistory, - onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }, - onMessageClicked = navController::navigateToMessagesHistory, - onCreateChatClicked = navController::navigateToCreateChat - ) + NavHost( + navController = navController, + startDestination = requireNotNull(startDestination), + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + authNavGraph( + onNavigateToMain = { + viewModel.onUserAuthenticated() + navController.navigateToMain() + }, + navController = navController + ) - messagesHistoryScreen( - onError = viewModel::onError, - onBack = navController::navigateUp, - onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials - ) - chatMaterialsScreen( - onBack = navController::navigateUp, - onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) } - ) - createChatScreen( - onChatCreated = { conversationId -> - navController.popBackStack() - navController.navigateToMessagesHistory(conversationId) - }, - navController = navController - ) + mainScreen( + onError = viewModel::onError, + onSettingsButtonClicked = navController::navigateToSettings, + onNavigateToMessagesHistory = navController::navigateToMessagesHistory, + onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }, + onMessageClicked = navController::navigateToMessagesHistory, + onNavigateToCreateChat = navController::navigateToCreateChat + ) - settingsScreen( - onBack = navController::navigateUp, - onLogOutButtonClicked = { navController.navigateToAuth(true) }, - onLanguageItemClicked = navController::navigateToLanguagePicker - ) - languagePickerScreen(onBack = navController::navigateUp) + messagesHistoryScreen( + onError = viewModel::onError, + onBack = navController::navigateUp, + onNavigateToChatMaterials = navController::navigateToChatMaterials + ) + chatMaterialsScreen( + onBack = navController::navigateUp, + onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) } + ) + createChatScreen( + onChatCreated = { conversationId -> + navController.popBackStack() + navController.navigateToMessagesHistory(conversationId) + }, + navController = navController + ) - photoViewScreen(onBack = navController::navigateUp) + settingsScreen( + onBack = navController::navigateUp, + onLogOutButtonClicked = { navController.navigateToAuth(true) }, + onLanguageItemClicked = navController::navigateToLanguagePicker + ) + languagePickerScreen(onBack = navController::navigateUp) + + photoViewScreen(onBack = navController::navigateUp) + } } } } diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/AppConstants.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/AppConstants.kt index a7a0bff9..9460b9c4 100644 --- a/core/common/src/main/kotlin/dev/meloda/fast/common/AppConstants.kt +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/AppConstants.kt @@ -4,7 +4,7 @@ object AppConstants { const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive" - const val API_VERSION = "5.173" + const val API_VERSION = "5.238" const val URL_OAUTH = "https://oauth.vk.com" const val URL_API = "https://api.vk.com/method" diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/VkConstants.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/VkConstants.kt index f01bc8a8..a9b5250b 100644 --- a/core/common/src/main/kotlin/dev/meloda/fast/common/VkConstants.kt +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/VkConstants.kt @@ -5,12 +5,12 @@ object VkConstants { const val GROUP_FIELDS = "description,members_count,counters,status,verified" const val USER_FIELDS = - "photo_50,photo_100,photo_200,photo_400_orig,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate" + "photo_50,photo_100,photo_200,photo_400_orig,status,screen_name,online_info,last_seen,verified,sex,bdate" const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS" - const val LP_VERSION = 10 + const val LP_VERSION = 19 const val VK_APP_ID = "2274003" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" @@ -18,6 +18,11 @@ object VkConstants { const val FAST_GROUP_ID = -119516304 const val FAST_APP_ID = "6964679" + const val MESSENGER_APP_ID = 51453752 + const val MESSENGER_APP_SECRET = "4UyuCUsdK8pVCNoeQuGi" + + const val MESSENGER_APP_SCOPE = 1454174 + object Auth { const val SCOPE = "notify," + "friends," + diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt index eee07f7d..583d1c34 100644 --- a/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt @@ -75,6 +75,11 @@ fun MutableStateFlow.setValue(function: (T) -> T) { update { newValue } } +fun MutableStateFlow.updateValue(block: T.() -> T) { + val newValue = block(value) + update { newValue } +} + fun Any.asInt(): Int { return when (this) { is Number -> this.toInt() @@ -83,6 +88,14 @@ fun Any.asInt(): Int { } } +fun Any.asLong(): Long { + return when(this) { + is Number -> this.toLong() + + else -> throw IllegalArgumentException("Object is not numeric") + } +} + fun Any.toList(mapper: (old: Any) -> T): List { return when (this) { is List<*> -> this.mapNotNull { it?.run(mapper) } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/State.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/State.kt index ab5daa6f..9a2253af 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/State.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/State.kt @@ -25,8 +25,6 @@ sealed class State { data object InternalError : Error() data class OAuthError(val error: OAuthErrorDomain) : Error() - - data class TestError(val message: String) : Error() } fun isLoading(): Boolean = this is Loading @@ -38,8 +36,8 @@ sealed class State { } inline fun State.processState( - error: (error: State.Error) -> (Unit), - success: (data: T) -> (Unit), + error: (error: State.Error) -> Unit, + success: (data: T) -> Unit, idle: (() -> (Unit)) = {}, loading: (() -> (Unit)) = {}, any: () -> Unit = {} @@ -61,11 +59,41 @@ inline fun State.processState( } } +fun OAuthErrorDomain?.toStateApiError(): State.Error { + if (this == null) return State.Error.ConnectionError + return State.Error.OAuthError(this) +} + fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) { null -> State.Error.ConnectionError else -> State.Error.ApiError(VkErrorCode.parse(code), message) } +fun ApiResult.asState() = when (this) { + is ApiResult.Success -> State.Success(this.value) + + is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError + is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR + is ApiResult.Failure.HttpFailure -> this.error.toStateApiError() + is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() +} + +fun ApiResult.asState(successMapper: (T) -> N) = + when (this) { + is ApiResult.Success -> State.Success(successMapper(this.value)) + + is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError + is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR + is ApiResult.Failure.HttpFailure -> this.error.toStateApiError() + is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() + } + +fun ApiResult.success(): T = + when (this) { + is ApiResult.Success -> value + else -> throw IllegalArgumentException() + } + fun ApiResult.mapToState() = when (this) { is ApiResult.Success -> State.Success(this.value) diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt index 377e32d7..f8fbce7b 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt @@ -6,17 +6,18 @@ object UserConfig { private const val ARG_CURRENT_USER_ID = "current_user_id" - var currentUserId: Int = -1 - get() = AppSettings.getInt(ARG_CURRENT_USER_ID, -1) + var currentUserId: Long = -1 + get() = AppSettings.getLong(ARG_CURRENT_USER_ID, -1) set(value) { field = value - AppSettings.edit { putInt(ARG_CURRENT_USER_ID, value) } + AppSettings.edit { putLong(ARG_CURRENT_USER_ID, value) } } - var userId: Int = -1 + var userId: Long = -1 var accessToken: String = "" var fastToken: String? = "" var trustedHash: String? = null + var exchangeToken: String? = null fun clear() { currentUserId = -1 diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt index 7722c6a3..6857719e 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt @@ -10,7 +10,7 @@ class VkGroupsMap( private val groups: List ) { - private val map: HashMap by lazy { + private val map: HashMap by lazy { HashMap(groups.associateBy(VkGroupDomain::id)) } @@ -36,7 +36,7 @@ class VkGroupsMap( if (message.fromId >= 0) null else map[abs(message.fromId)] - fun group(groupId: Int): VkGroupDomain? = map[abs(groupId)] + fun group(groupId: Long): VkGroupDomain? = map[abs(groupId)] companion object { diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/VkMemoryCache.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/VkMemoryCache.kt index 2a1d491e..8c5e8ebb 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/VkMemoryCache.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/VkMemoryCache.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.data +import dev.meloda.fast.data.UserConfig.userId import dev.meloda.fast.model.api.domain.VkContactDomain import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkGroupDomain @@ -9,11 +10,11 @@ import kotlin.math.abs object VkMemoryCache { - private val users: HashMap = hashMapOf() - private val groups: HashMap = hashMapOf() - private val messages: HashMap = hashMapOf() - private val conversations: HashMap = hashMapOf() - private val contacts: HashMap = hashMapOf() + private val users: HashMap = hashMapOf() + private val groups: HashMap = hashMapOf() + private val messages: HashMap = hashMapOf() + private val conversations: HashMap = hashMapOf() + private val contacts: HashMap = hashMapOf() fun appendUsers(users: List) { users.forEach { user -> VkMemoryCache.users[user.id] = user } @@ -37,83 +38,83 @@ object VkMemoryCache { contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact } } - operator fun set(userId: Int, user: VkUser) { + operator fun set(userid: Long, user: VkUser) { users[userId] = user } - operator fun set(groupId: Int, group: VkGroupDomain) { + operator fun set(groupId: Long, group: VkGroupDomain) { groups[groupId] = group } - operator fun set(messageId: Int, message: VkMessage) { + operator fun set(messageId: Long, message: VkMessage) { messages[messageId] = message } - operator fun set(conversationId: Int, conversation: VkConversation) { + operator fun set(conversationId: Long, conversation: VkConversation) { conversations[conversationId] = conversation } - operator fun set(contactId: Int, contact: VkContactDomain) { + operator fun set(contactId: Long, contact: VkContactDomain) { contacts[contactId] = contact } - fun getUser(id: Int): VkUser? { + fun getUser(id: Long): VkUser? { return getUsers(id).firstOrNull() } - fun getUsers(vararg ids: Int): List { + fun getUsers(vararg ids: Long): List { return getUsers(ids.toList()) } - fun getUsers(ids: List): List { + fun getUsers(ids: List): List { return ids.mapNotNull { id -> users[id] } } - fun getGroup(id: Int): VkGroupDomain? { + fun getGroup(id: Long): VkGroupDomain? { return getGroups(id).firstOrNull() } - fun getGroups(vararg ids: Int): List { + fun getGroups(vararg ids: Long): List { return getGroups(ids.toList()) } - fun getGroups(ids: List): List { + fun getGroups(ids: List): List { return ids.mapNotNull { id -> groups[id] } } - fun getMessage(id: Int): VkMessage? { + fun getMessage(id: Long): VkMessage? { return getMessages(id).firstOrNull() } - fun getMessages(vararg ids: Int): List { + fun getMessages(vararg ids: Long): List { return getMessages(ids.toList()) } - fun getMessages(ids: List): List { + fun getMessages(ids: List): List { return ids.mapNotNull { id -> messages[id] } } - fun getConversation(id: Int): VkConversation? { + fun getConversation(id: Long): VkConversation? { return getConversations(id).firstOrNull() } - fun getConversations(vararg ids: Int): List { + fun getConversations(vararg ids: Long): List { return getConversations(ids.toList()) } - fun getConversations(ids: List): List { + fun getConversations(ids: List): List { return ids.mapNotNull { id -> conversations[id] } } - fun getContact(id: Int): VkContactDomain? { + fun getContact(id: Long): VkContactDomain? { return getContacts(id).firstOrNull() } - fun getContacts(vararg ids: Int): List { + fun getContacts(vararg ids: Long): List { return getContacts(ids.toList()) } - fun getContacts(ids: List): List { + fun getContacts(ids: List): List { return ids.mapNotNull { id -> contacts[id] } } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt index b867eac3..ba46bf11 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.data +import dev.meloda.fast.data.UserConfig.userId import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkMessage @@ -9,7 +10,7 @@ class VkUsersMap( private val users: List ) { - private val map: HashMap by lazy { + private val map: HashMap by lazy { HashMap(users.associateBy(VkUser::id)) } @@ -35,7 +36,7 @@ class VkUsersMap( if (message.fromId > 0) map[message.fromId] else null - fun user(userId: Int): VkUser? = map[userId] + fun user(userid: Long): VkUser? = map[userId] companion object { diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/VkUtils.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/VkUtils.kt new file mode 100644 index 00000000..c3034d48 --- /dev/null +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/VkUtils.kt @@ -0,0 +1,34 @@ +package dev.meloda.fast.data + +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.network.VkErrorCode + +object VkUtils { + + fun parseError(error: State.Error): BaseError? { + return when (error) { + is State.Error.ApiError -> { + when (error.errorCode) { + VkErrorCode.USER_AUTHORIZATION_FAILED -> { + if (error.errorMessage.startsWith( + "User authorization failed: user is blocked." + ) + ) { + BaseError.AccountBlocked + } else { + BaseError.SessionExpired + } + } + + else -> BaseError.SimpleError(message = error.errorMessage) + } + } + + State.Error.ConnectionError -> BaseError.ConnectionError + State.Error.InternalError -> BaseError.InternalError + State.Error.UnknownError -> BaseError.UnknownError + + else -> null + } + } +} diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepository.kt index 08215d90..f6f1cbef 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepository.kt @@ -1,12 +1,32 @@ package dev.meloda.fast.data.api.auth +import com.slack.eithernet.ApiResult +import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse +import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse +import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.network.RestApiErrorDomain -import com.slack.eithernet.ApiResult interface AuthRepository { + suspend fun logout(): ApiResult + suspend fun validatePhone( validationSid: String ): ApiResult + + suspend fun getAnonymToken( + clientId: String, + clientSecret: String + ): ApiResult + + suspend fun exchangeSilentToken( + anonymToken: String, + silentToken: String, + silentUuid: String + ): ApiResult + + suspend fun getExchangeToken( + accessToken: String + ): ApiResult } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepositoryImpl.kt index b9af106e..5ecb9bda 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/auth/AuthRepositoryImpl.kt @@ -1,10 +1,17 @@ package dev.meloda.fast.data.api.auth +import com.slack.eithernet.ApiResult +import dev.meloda.fast.common.VkConstants +import dev.meloda.fast.model.api.requests.ExchangeSilentTokenRequest +import dev.meloda.fast.model.api.requests.GetAnonymTokenRequest +import dev.meloda.fast.model.api.requests.GetExchangeTokenRequest +import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse +import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse +import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.service.auth.AuthService -import com.slack.eithernet.ApiResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -12,9 +19,50 @@ class AuthRepositoryImpl( private val service: AuthService ) : AuthRepository { + override suspend fun logout(): ApiResult = + withContext(Dispatchers.IO) { + service.logout( + clientId = VkConstants.MESSENGER_APP_ID.toString(), + clientSecret = VkConstants.MESSENGER_APP_SECRET + ).mapApiDefault() + } + override suspend fun validatePhone( validationSid: String ): ApiResult = withContext(Dispatchers.IO) { service.validatePhone(validationSid).mapApiDefault() } + + override suspend fun getAnonymToken( + clientId: String, + clientSecret: String + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = GetAnonymTokenRequest( + clientId = clientId, + clientSecret = clientSecret + ) + + service.getAnonymToken(requestModel.map).mapApiDefault() + } + + override suspend fun exchangeSilentToken( + anonymToken: String, + silentToken: String, + silentUuid: String + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = ExchangeSilentTokenRequest( + anonymToken = anonymToken, + silentToken = silentToken, + silentUuid = silentUuid + ) + + service.exchangeSilentToken(requestModel.map).mapApiDefault() + } + + override suspend fun getExchangeToken( + accessToken: String + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = GetExchangeTokenRequest(accessToken = accessToken) + service.getExchangeToken(requestModel.map).mapApiDefault() + } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt index 7a9c3dd1..22743633 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt @@ -1,22 +1,30 @@ package dev.meloda.fast.data.api.conversations import com.slack.eithernet.ApiResult +import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.network.RestApiErrorDomain interface ConversationsRepository { + suspend fun storeConversations(conversations: List) + suspend fun getConversations( count: Int?, - offset: Int? + offset: Int?, + filter: ConversationsFilter ): ApiResult, RestApiErrorDomain> suspend fun getConversationsById( - peerIds: List + peerIds: List, + extended: Boolean? = null, + fields: String? = null ): ApiResult, RestApiErrorDomain> - suspend fun storeConversations(conversations: List) - suspend fun delete(peerId: Int): ApiResult - suspend fun pin(peerId: Int): ApiResult - suspend fun unpin(peerId: Int): ApiResult + suspend fun delete(peerId: Long): ApiResult + suspend fun pin(peerId: Long): ApiResult + suspend fun unpin(peerId: Long): ApiResult + suspend fun reorderPinned(peerIds: List): ApiResult + suspend fun archive(peerId: Long): ApiResult + suspend fun unarchive(peerId: Long): ApiResult } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt index 85d0be97..28b1141d 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt @@ -6,37 +6,50 @@ import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkUsersMap import dev.meloda.fast.database.dao.ConversationDao +import dev.meloda.fast.database.dao.GroupDao +import dev.meloda.fast.database.dao.MessageDao +import dev.meloda.fast.database.dao.UserDao +import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.domain.VkConversation +import dev.meloda.fast.model.api.domain.VkGroupDomain +import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.asEntity -import dev.meloda.fast.model.api.requests.ConversationsDeleteRequest import dev.meloda.fast.model.api.requests.ConversationsGetRequest -import dev.meloda.fast.model.api.requests.ConversationsPinRequest -import dev.meloda.fast.model.api.requests.ConversationsUnpinRequest import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.service.conversations.ConversationsService import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ConversationsRepositoryImpl( private val conversationsService: ConversationsService, + private val messageDao: MessageDao, + private val userDao: UserDao, + private val groupDao: GroupDao, private val conversationDao: ConversationDao ) : ConversationsRepository { + override suspend fun storeConversations(conversations: List) { + conversationDao.insertAll(conversations.map(VkConversation::asEntity)) + } + override suspend fun getConversations( count: Int?, - offset: Int? + offset: Int?, + filter: ConversationsFilter ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = ConversationsGetRequest( count = count, offset = offset, fields = VkConstants.ALL_FIELDS, - filter = "all", + filter = filter, extended = true, startMessageId = null ) @@ -56,7 +69,7 @@ class ConversationsRepositoryImpl( VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendContacts(contactsList) - response.items.map { item -> + val conversations = response.items.map { item -> val lastMessage = item.lastMessage?.asDomain()?.let { message -> message.copy( user = usersMap.messageUser(message), @@ -72,6 +85,17 @@ class ConversationsRepositoryImpl( ).also { VkMemoryCache[conversation.id] = it } } } + + val messages = conversations.mapNotNull(VkConversation::lastMessage) + + launch(Dispatchers.IO) { + conversationDao.insertAll(conversations.map(VkConversation::asEntity)) + messageDao.insertAll(messages.map(VkMessage::asEntity)) + userDao.insertAll(profilesList.map(VkUser::asEntity)) + groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) + } + + conversations }, errorMapper = { error -> error?.toDomain() @@ -80,13 +104,16 @@ class ConversationsRepositoryImpl( } override suspend fun getConversationsById( - peerIds: List + peerIds: List, + extended: Boolean?, + fields: String? ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { - val requestParams = mapOf( - "peer_ids" to peerIds.joinToString(separator = ","), - "extended" to "1", - "fields" to VkConstants.ALL_FIELDS - ) + val requestParams = mutableMapOf( + "peer_ids" to peerIds.joinToString(separator = ",") + ).apply { + extended?.let { this["extended"] = if (it) "1" else "0" } + fields?.let { this["fields"] = it } + } conversationsService.getConversationsById(requestParams).mapApiResult( successMapper = { apiResponse -> @@ -99,11 +126,7 @@ class ConversationsRepositoryImpl( val usersMap = VkUsersMap.forUsers(profilesList) val groupsMap = VkGroupsMap.forGroups(groupsList) - VkMemoryCache.appendUsers(profilesList) - VkMemoryCache.appendGroups(groupsList) - VkMemoryCache.appendContacts(contactsList) - - response.items.map { item -> + val conversations = response.items.map { item -> item.asDomain().let { conversation -> conversation.copy( user = usersMap.conversationUser(conversation), @@ -111,6 +134,18 @@ class ConversationsRepositoryImpl( ).also { VkMemoryCache[conversation.id] = it } } } + + launch(Dispatchers.IO) { + conversationDao.insertAll(conversations.map(VkConversation::asEntity)) + userDao.insertAll(profilesList.map(VkUser::asEntity)) + groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) + } + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + conversations }, errorMapper = { error -> error?.toDomain() @@ -118,31 +153,43 @@ class ConversationsRepositoryImpl( ) } - override suspend fun storeConversations(conversations: List) { - conversationDao.insertAll(conversations.map(VkConversation::asEntity)) - } - - override suspend fun delete(peerId: Int): ApiResult = + override suspend fun delete(peerId: Long): ApiResult = withContext(Dispatchers.IO) { - val requestModel = ConversationsDeleteRequest(peerId = peerId) - - conversationsService.delete(requestModel.map).mapApiResult( + conversationsService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult( successMapper = { response -> response.requireResponse().lastDeletedId }, errorMapper = { error -> error?.toDomain() } ) } override suspend fun pin( - peerId: Int + peerId: Long ): ApiResult = withContext(Dispatchers.IO) { - val requestModel = ConversationsPinRequest(peerId = peerId) - conversationsService.pin(requestModel.map).mapApiDefault() + conversationsService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault() } override suspend fun unpin( - peerId: Int + peerId: Long ): ApiResult = withContext(Dispatchers.IO) { - val requestModel = ConversationsUnpinRequest(peerId = peerId) - conversationsService.unpin(requestModel.map).mapApiDefault() + conversationsService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault() + } + + override suspend fun reorderPinned( + peerIds: List + ): ApiResult = withContext(Dispatchers.IO) { + conversationsService + .reorderPinned(mapOf("peer_ids" to peerIds.joinToString(","))) + .mapApiDefault() + } + + override suspend fun archive( + peerId: Long + ): ApiResult = withContext(Dispatchers.IO) { + conversationsService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault() + } + + override suspend fun unarchive( + peerId: Long + ): ApiResult = withContext(Dispatchers.IO) { + conversationsService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault() } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/files/FilesRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/files/FilesRepository.kt index 748dd3a2..69822c50 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/files/FilesRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/files/FilesRepository.kt @@ -16,7 +16,7 @@ class FilesRepository( // AUDIO_MESSAGE("audio_message") // } // -// suspend fun getMessagesUploadServer(peerId: Int, type: FileType) = +// suspend fun getMessagesUploadServer(peerid: Long, type: FileType) = // filesService.getUploadServer( // mapOf( // "peer_id" to peerId.toString(), diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt index b0d9d61e..361a8e1c 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepository.kt @@ -22,7 +22,7 @@ interface FriendsRepository { suspend fun getOnlineFriends( count: Int?, offset: Int? - ): ApiResult, RestApiErrorDomain> + ): ApiResult, RestApiErrorDomain> suspend fun storeUsers(users: List) } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt index b526bc90..4719d010 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/friends/FriendsRepositoryImpl.kt @@ -2,7 +2,7 @@ package dev.meloda.fast.data.api.friends import dev.meloda.fast.common.VkConstants import dev.meloda.fast.data.VkMemoryCache -import dev.meloda.fast.database.dao.UsersDao +import dev.meloda.fast.database.dao.UserDao import dev.meloda.fast.model.FriendsInfo import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.domain.VkUser @@ -21,7 +21,7 @@ import kotlinx.coroutines.withContext class FriendsRepositoryImpl( private val service: FriendsService, - private val dao: UsersDao + private val dao: UserDao ) : FriendsRepository { override suspend fun getAllFriends( @@ -69,7 +69,7 @@ class FriendsRepositoryImpl( override suspend fun getOnlineFriends( count: Int?, offset: Int? - ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = GetOnlineFriendsRequest( order = "hints", count = count, 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 2eb008c3..c997ac8b 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,91 +1,112 @@ package dev.meloda.fast.data.api.messages import com.slack.eithernet.ApiResult +import dev.meloda.fast.model.api.data.VkChatData 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.responses.MessagesGetConversationMembersResponse +import dev.meloda.fast.model.api.responses.MessagesSendResponse import dev.meloda.fast.network.RestApiErrorDomain interface MessagesRepository { + suspend fun storeMessages(messages: List) + suspend fun getHistory( - conversationId: Int, + conversationId: Long, offset: Int?, count: Int? ): ApiResult suspend fun getById( - messagesIds: List, + peerCmIds: List?, + peerId: Long?, + messagesIds: List?, + cmIds: List?, extended: Boolean?, fields: String? ): ApiResult, RestApiErrorDomain> suspend fun send( - peerId: Int, - randomId: Int, + peerId: Long, + randomId: Long, message: String?, - replyTo: Int?, + replyTo: Long?, attachments: List? - ): ApiResult + ): ApiResult suspend fun markAsRead( - peerId: Int, - startMessageId: Int? + peerId: Long, + startMessageId: Long? ): ApiResult suspend fun getHistoryAttachments( - peerId: Int, + peerId: Long, count: Int?, offset: Int?, attachmentTypes: List, - conversationMessageId: Int + cmId: Long ): ApiResult, RestApiErrorDomain> suspend fun createChat( - userIds: List?, + userIds: List?, title: String? - ): ApiResult + ): ApiResult suspend fun pin( - peerId: Int, - messageId: Int?, - conversationMessageId: Int? + peerId: Long, + messageId: Long? = null, + cmId: Long? = null ): ApiResult suspend fun unpin( - peerId: Int + peerId: Long ): ApiResult suspend fun markAsImportant( - peerId: Int, - messageIds: List?, - conversationMessageIds: List?, + peerId: Long, + messageIds: List? = null, + cmIds: List? = null, important: Boolean - ): ApiResult, RestApiErrorDomain> + ): ApiResult, RestApiErrorDomain> suspend fun delete( - peerId: Int, - messageIds: List?, - conversationMessageIds: List?, + peerId: Long, + messageIds: List?, + cmIds: List?, spam: Boolean, deleteForAll: Boolean ): ApiResult, RestApiErrorDomain> - suspend fun storeMessages(messages: List) -// -// suspend fun edit( -// params: MessagesEditRequest -// ): ApiResult -// -// suspend fun getChat( -// params: MessagesGetChatRequest -// ): ApiResult -// -// suspend fun getConversationMembers( -// params: MessagesGetConversationMembersRequest -// ): ApiResult -// -// suspend fun removeChatUser( -// params: MessagesRemoveChatUserRequest -// ): ApiResult + suspend fun edit( + peerId: Long, + messageId: Long? = null, + cmId: Long? = null, + message: String? = null, + lat: Float? = null, + long: Float? = null, + attachments: List? = null, + notParseLinks: Boolean = false, + keepSnippets: Boolean = true, + keepForwardedMessages: Boolean = true + ): ApiResult + + suspend fun getChat( + chatId: Long, + fields: String? = null + ): ApiResult + + suspend fun getConversationMembers( + peerId: Long, + offset: Int? = null, + count: Int? = null, + extended: Boolean? = null, + fields: String? = null + ): ApiResult + + suspend fun removeChatUser( + chatId: Long, + memberId: Long + ): ApiResult } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt index 901e3ae1..a0c4c186 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 @@ -5,40 +5,57 @@ import dev.meloda.fast.common.VkConstants import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkUsersMap +import dev.meloda.fast.database.dao.ConversationDao +import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.MessageDao +import dev.meloda.fast.database.dao.UserDao import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData +import dev.meloda.fast.model.api.data.VkChatData import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage +import dev.meloda.fast.model.api.domain.VkConversation +import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.asEntity import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest import dev.meloda.fast.model.api.requests.MessagesDeleteRequest +import dev.meloda.fast.model.api.requests.MessagesEditRequest import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest +import dev.meloda.fast.model.api.requests.MessagesGetChatRequest +import dev.meloda.fast.model.api.requests.MessagesGetConversationMembersRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest import dev.meloda.fast.model.api.requests.MessagesMarkAsImportantRequest import dev.meloda.fast.model.api.requests.MessagesMarkAsReadRequest import dev.meloda.fast.model.api.requests.MessagesPinMessageRequest +import dev.meloda.fast.model.api.requests.MessagesRemoveChatUserRequest import dev.meloda.fast.model.api.requests.MessagesSendRequest import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest +import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse +import dev.meloda.fast.model.api.responses.MessagesSendResponse 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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class MessagesRepositoryImpl( private val messagesService: MessagesService, private val messageDao: MessageDao, + private val userDao: UserDao, + private val groupDao: GroupDao, + private val conversationDao: ConversationDao ) : MessagesRepository { override suspend fun getHistory( - conversationId: Int, + conversationId: Long, offset: Int?, count: Int? ): ApiResult = withContext(Dispatchers.IO) { @@ -89,6 +106,13 @@ class MessagesRepositoryImpl( } } + launch(Dispatchers.IO) { + conversationDao.insertAll(conversations.map(VkConversation::asEntity)) + messageDao.insertAll(messages.map(VkMessage::asEntity)) + userDao.insertAll(profilesList.map(VkUser::asEntity)) + groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) + } + MessagesHistoryInfo( messages = messages, conversations = conversations @@ -101,12 +125,18 @@ class MessagesRepositoryImpl( } override suspend fun getById( - messagesIds: List, + peerCmIds: List?, + peerId: Long?, + messagesIds: List?, + cmIds: List?, extended: Boolean?, fields: String? ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = MessagesGetByIdRequest( + peerCmIds = peerCmIds, + peerId = peerId, messagesIds = messagesIds, + cmIds = cmIds, extended = extended, fields = fields ) @@ -116,12 +146,15 @@ class MessagesRepositoryImpl( 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 -> + 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) + + val domainMessages = messages.map { message -> message.asDomain().copy( user = usersMap.messageUser(message), group = groupsMap.messageGroup(message), @@ -129,18 +162,30 @@ class MessagesRepositoryImpl( actionGroup = groupsMap.messageActionGroup(message) ) } + + launch(Dispatchers.IO) { + messageDao.insertAll(domainMessages.map(VkMessage::asEntity)) + userDao.insertAll(profilesList.map(VkUser::asEntity)) + groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) + } + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + domainMessages }, errorMapper = { error -> error?.toDomain() } ) } override suspend fun send( - peerId: Int, - randomId: Int, + peerId: Long, + randomId: Long, message: String?, - replyTo: Int?, + replyTo: Long?, attachments: List? - ): ApiResult = withContext(Dispatchers.IO) { + ): ApiResult = withContext(Dispatchers.IO) { val requestModel = MessagesSendRequest( peerId = peerId, randomId = randomId, @@ -153,8 +198,8 @@ class MessagesRepositoryImpl( } override suspend fun markAsRead( - peerId: Int, - startMessageId: Int? + peerId: Long, + startMessageId: Long? ): ApiResult = withContext(Dispatchers.IO) { val requestModel = MessagesMarkAsReadRequest( peerId = peerId, @@ -165,11 +210,11 @@ class MessagesRepositoryImpl( } override suspend fun getHistoryAttachments( - peerId: Int, + peerId: Long, count: Int?, offset: Int?, attachmentTypes: List, - conversationMessageId: Int + cmId: Long ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = MessagesGetHistoryAttachmentsRequest( @@ -179,7 +224,7 @@ class MessagesRepositoryImpl( offset = offset, preserveOrder = true, attachmentTypes = attachmentTypes, - conversationMessageId = conversationMessageId, + conversationMessageId = cmId, fields = VkConstants.ALL_FIELDS ) @@ -195,6 +240,11 @@ class MessagesRepositoryImpl( VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendContacts(contactsList) + launch(Dispatchers.IO) { + userDao.insertAll(profilesList.map(VkUser::asEntity)) + groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) + } + response.items.map(VkAttachmentHistoryMessageData::toDomain) }, errorMapper = { error -> @@ -204,9 +254,9 @@ class MessagesRepositoryImpl( } override suspend fun createChat( - userIds: List?, + userIds: List?, title: String? - ): ApiResult = withContext(Dispatchers.IO) { + ): ApiResult = withContext(Dispatchers.IO) { val requestModel = MessagesCreateChatRequest( userIds = userIds, title = title @@ -221,14 +271,14 @@ class MessagesRepositoryImpl( } override suspend fun pin( - peerId: Int, - messageId: Int?, - conversationMessageId: Int? + peerId: Long, + messageId: Long?, + cmId: Long? ): ApiResult = withContext(Dispatchers.IO) { val requestModel = MessagesPinMessageRequest( peerId = peerId, messageId = messageId, - conversationMessageId = conversationMessageId + conversationMessageId = cmId ) messagesService.pin(requestModel.map).mapApiResult( @@ -240,18 +290,18 @@ class MessagesRepositoryImpl( } override suspend fun unpin( - peerId: Int + peerId: Long ): ApiResult = withContext(Dispatchers.IO) { val requestModel = MessagesUnpinMessageRequest(peerId = peerId) messagesService.unpin(requestModel.map).mapApiDefault() } override suspend fun markAsImportant( - peerId: Int, - messageIds: List?, - conversationMessageIds: List?, + peerId: Long, + messageIds: List?, + cmIds: List?, important: Boolean - ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = MessagesMarkAsImportantRequest( messagesIds = messageIds.orEmpty(), important = important @@ -260,16 +310,16 @@ class MessagesRepositoryImpl( } override suspend fun delete( - peerId: Int, - messageIds: List?, - conversationMessageIds: List?, + peerId: Long, + messageIds: List?, + cmIds: List?, spam: Boolean, deleteForAll: Boolean ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = MessagesDeleteRequest( peerId = peerId, messagesIds = messageIds, - conversationsMessagesIds = conversationMessageIds, + conversationsMessagesIds = cmIds, isSpam = spam, deleteForAll = deleteForAll ) @@ -280,58 +330,74 @@ class MessagesRepositoryImpl( messageDao.insertAll(messages.map(VkMessage::asEntity)) } -// override suspend fun markAsImportant( -// params: MessagesMarkAsImportantRequest -// ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { -// messagesService.markAsImportant(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun delete( -// params: MessagesDeleteRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.delete(params.map).mapResult( -// successMapper = {}, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun edit( -// params: MessagesEditRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.edit(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun getChat( -// params: MessagesGetChatRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.getChat(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun getConversationMembers( -// params: MessagesGetConversationMembersRequest -// ): ApiResult = -// withContext(Dispatchers.IO) { -// messagesService.getConversationMembers(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } -// -// override suspend fun removeChatUser( -// params: MessagesRemoveChatUserRequest -// ): ApiResult = withContext(Dispatchers.IO) { -// messagesService.removeChatUser(params.map).mapResult( -// successMapper = { response -> response.requireResponse() }, -// errorMapper = { error -> error?.toDomain() } -// ) -// } + override suspend fun edit( + peerId: Long, + messageId: Long?, + cmId: Long?, + message: String?, + lat: Float?, + long: Float?, + attachments: List?, + notParseLinks: Boolean, + keepSnippets: Boolean, + keepForwardedMessages: Boolean + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesEditRequest( + peerId = peerId, + messageId = messageId, + cmId = cmId, + message = message, + lat = lat, + long = long, + attachments = attachments, + notParseLinks = notParseLinks, + keepSnippets = keepSnippets, + keepForwardedMessages = keepForwardedMessages + ) + + messagesService.edit(requestModel.map).mapApiDefault() + } + + override suspend fun getChat( + chatId: Long, + fields: String? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetChatRequest( + chatId = chatId, + fields = fields + ) + + messagesService.getChat(requestModel.map).mapApiDefault() + } + + override suspend fun getConversationMembers( + peerId: Long, + offset: Int?, + count: Int?, + extended: Boolean?, + fields: String? + ): ApiResult = + withContext(Dispatchers.IO) { + val requestModel = MessagesGetConversationMembersRequest( + peerId = peerId, + offset = offset, + count = count, + extended = extended, + fields = fields + ) + + messagesService.getConversationMembers(requestModel.map).mapApiDefault() + } + + override suspend fun removeChatUser( + chatId: Long, + memberId: Long + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesRemoveChatUserRequest( + chatId = chatId, + memberId = memberId + ) + + messagesService.removeChatUser(requestModel.map).mapApiDefault() + } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepository.kt index 4c4e3723..8cc3c7a1 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepository.kt @@ -1,6 +1,9 @@ package dev.meloda.fast.data.api.oauth +import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.responses.AuthDirectResponse +import dev.meloda.fast.model.api.responses.GetSilentTokenResponse +import dev.meloda.fast.network.OAuthErrorDomain interface OAuthRepository { @@ -11,5 +14,14 @@ interface OAuthRepository { validationCode: String?, captchaSid: String?, captchaKey: String? - ): AuthDirectResponse + ): ApiResult + + suspend fun getSilentToken( + login: String, + password: String, + forceSms: Boolean, + validationCode: String?, + captchaSid: String?, + captchaKey: String?, + ): ApiResult } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepositoryImpl.kt index 7292b610..4e22ae69 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/oauth/OAuthRepositoryImpl.kt @@ -1,10 +1,16 @@ package dev.meloda.fast.data.api.oauth +import com.slack.eithernet.ApiResult import dev.meloda.fast.common.VkConstants import dev.meloda.fast.model.api.requests.AuthDirectRequest import dev.meloda.fast.model.api.responses.AuthDirectResponse +import dev.meloda.fast.model.api.responses.GetSilentTokenResponse +import dev.meloda.fast.network.OAuthErrorDomain +import dev.meloda.fast.network.ValidationType +import dev.meloda.fast.network.VkOAuthError +import dev.meloda.fast.network.VkOAuthErrorType +import dev.meloda.fast.network.mapResult import dev.meloda.fast.network.service.oauth.OAuthService -import com.slack.eithernet.ApiResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -18,37 +24,190 @@ class OAuthRepositoryImpl( forceSms: Boolean, validationCode: String?, captchaSid: String?, - captchaKey: String? - ): AuthDirectResponse = withContext(Dispatchers.IO) { + captchaKey: String?, + ): ApiResult = withContext(Dispatchers.IO) { val requestModel = AuthDirectRequest( grantType = VkConstants.Auth.GrantType.PASSWORD, - clientId = VkConstants.VK_APP_ID, - clientSecret = VkConstants.VK_SECRET, + clientId = VkConstants.MESSENGER_APP_ID.toString(), + clientSecret = VkConstants.MESSENGER_APP_SECRET, username = login, password = password, - scope = VkConstants.Auth.SCOPE, + scope = VkConstants.MESSENGER_APP_SCOPE.toString(), validationForceSms = forceSms, validationCode = validationCode, captchaSid = captchaSid, captchaKey = captchaKey, ) - when (val result = oAuthService.auth(requestModel.map)) { - is ApiResult.Success -> result.value + oAuthService.auth(requestModel.map).mapResult( + successMapper = { + it + }, + errorMapper = { response -> + val error = response?.error?.let(VkOAuthError::parse) + val errorType = response?.errorType?.let(VkOAuthErrorType::parse) - is ApiResult.Failure.HttpFailure -> { - requireNotNull(result.error) + when (error) { + null -> OAuthErrorDomain.UnknownError + + VkOAuthError.FLOOD_CONTROL -> OAuthErrorDomain.TooManyTriesError + + VkOAuthError.NEED_VALIDATION -> { + if (response.banInfo != null) { + val info = requireNotNull(response.banInfo) + + OAuthErrorDomain.UserBannedError( + memberName = info.memberName, + message = info.message, + accessToken = info.accessToken, + restoreUrl = info.restoreUrl + ) + } else { + OAuthErrorDomain.ValidationRequiredError( + description = response.errorDescription.orEmpty(), + validationType = response.validationType.orEmpty() + .let(ValidationType::parse), + validationSid = response.validationSid.orEmpty(), + phoneMask = response.phoneMask.orEmpty(), + redirectUri = response.redirectUri.orEmpty(), + validationResend = response.validationResend, + restoreIfCannotGetCode = response.restoreIfCannotGetCode + ) + } + } + + VkOAuthError.NEED_CAPTCHA -> { + OAuthErrorDomain.CaptchaRequiredError( + captchaSid = response.captchaSid.orEmpty(), + captchaImageUrl = response.captchaImage.orEmpty() + ) + } + + VkOAuthError.INVALID_CLIENT -> { + OAuthErrorDomain.InvalidCredentialsError + } + + VkOAuthError.INVALID_REQUEST -> { + when (errorType) { + null -> OAuthErrorDomain.UnknownError + + VkOAuthErrorType.WRONG_OTP -> { + OAuthErrorDomain.WrongValidationCode + } + + VkOAuthErrorType.WRONG_OTP_FORMAT -> { + OAuthErrorDomain.WrongValidationCodeFormat + } + + VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> { + OAuthErrorDomain.TooManyTriesError + } + + VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> { + OAuthErrorDomain.InvalidCredentialsError + } + } + } + + VkOAuthError.UNKNOWN -> OAuthErrorDomain.UnknownError + } } - - is ApiResult.Failure.ApiFailure -> TODO() - - is ApiResult.Failure.NetworkFailure -> { - // TODO: 13/07/2024, Danil Nikolaev: implement showing network error - TODO() - } - is ApiResult.Failure.UnknownFailure -> TODO() - - else -> throw IllegalStateException("Unknown result") - } + ) } + + override suspend fun getSilentToken( + login: String, + password: String, + forceSms: Boolean, + validationCode: String?, + captchaSid: String?, + captchaKey: String?, + ): ApiResult = + withContext(Dispatchers.IO) { + val requestModel = AuthDirectRequest( + grantType = VkConstants.Auth.GrantType.PASSWORD, + clientId = VkConstants.MESSENGER_APP_ID.toString(), + clientSecret = VkConstants.MESSENGER_APP_SECRET, + username = login, + password = password, + scope = VkConstants.MESSENGER_APP_SCOPE.toString(), + validationForceSms = forceSms, + validationCode = validationCode, + captchaSid = captchaSid, + captchaKey = captchaKey, + ) + + oAuthService.getSilentToken(requestModel.map).mapResult( + successMapper = { it }, + errorMapper = { response -> + val error = response?.error?.let(VkOAuthError::parse) + val errorType = response?.errorType?.let(VkOAuthErrorType::parse) + + when (error) { + null -> OAuthErrorDomain.UnknownError + + VkOAuthError.FLOOD_CONTROL -> OAuthErrorDomain.TooManyTriesError + + VkOAuthError.NEED_VALIDATION -> { + if (response.banInfo != null) { + val info = requireNotNull(response.banInfo) + + OAuthErrorDomain.UserBannedError( + memberName = info.memberName, + message = info.message, + accessToken = info.accessToken, + restoreUrl = info.restoreUrl + ) + } else { + OAuthErrorDomain.ValidationRequiredError( + description = response.errorDescription.orEmpty(), + validationType = response.validationType.orEmpty() + .let(ValidationType::parse), + validationSid = response.validationSid.orEmpty(), + phoneMask = response.phoneMask.orEmpty(), + redirectUri = response.redirectUri.orEmpty(), + validationResend = response.validationResend, + restoreIfCannotGetCode = response.restoreIfCannotGetCode + ) + } + } + + VkOAuthError.NEED_CAPTCHA -> { + OAuthErrorDomain.CaptchaRequiredError( + captchaSid = response.captchaSid.orEmpty(), + captchaImageUrl = response.captchaImage.orEmpty() + ) + } + + VkOAuthError.INVALID_CLIENT -> { + OAuthErrorDomain.InvalidCredentialsError + } + + VkOAuthError.INVALID_REQUEST -> { + when (errorType) { + null -> OAuthErrorDomain.UnknownError + + VkOAuthErrorType.WRONG_OTP -> { + OAuthErrorDomain.WrongValidationCode + } + + VkOAuthErrorType.WRONG_OTP_FORMAT -> { + OAuthErrorDomain.WrongValidationCodeFormat + } + + VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> { + OAuthErrorDomain.TooManyTriesError + } + + VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> { + OAuthErrorDomain.InvalidCredentialsError + } + } + } + + VkOAuthError.UNKNOWN -> OAuthErrorDomain.UnknownError + } + } + ) + } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/photos/PhotosRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/photos/PhotosRepository.kt index efea4f13..30eab6a8 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/photos/PhotosRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/photos/PhotosRepository.kt @@ -8,7 +8,7 @@ class PhotosRepository( private val photosService: PhotosService ) { - suspend fun getMessagesUploadServer(peerId: Int) = + suspend fun getMessagesUploadServer(peerId: Long) = photosService.getUploadServer(mapOf("peer_id" to peerId.toString())) suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepository.kt index eac02481..ce5712b9 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepository.kt @@ -1,18 +1,18 @@ package dev.meloda.fast.data.api.users +import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.network.RestApiErrorDomain -import com.slack.eithernet.ApiResult interface UsersRepository { suspend fun get( - userIds: List?, + userIds: List?, fields: String?, nomCase: String? ): ApiResult, RestApiErrorDomain> - suspend fun getLocalUsers(userIds: List): List + suspend fun getLocalUsers(userIds: List): List suspend fun storeUsers(users: List) } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepositoryImpl.kt index d206e66e..03ed23eb 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/users/UsersRepositoryImpl.kt @@ -1,7 +1,8 @@ package dev.meloda.fast.data.api.users +import com.slack.eithernet.ApiResult import dev.meloda.fast.data.VkMemoryCache -import dev.meloda.fast.database.dao.UsersDao +import dev.meloda.fast.database.dao.UserDao import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.asEntity @@ -11,18 +12,17 @@ import dev.meloda.fast.model.database.asExternalModel import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.service.users.UsersService -import com.slack.eithernet.ApiResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class UsersRepositoryImpl( private val service: UsersService, - private val dao: UsersDao + private val dao: UserDao ) : UsersRepository { override suspend fun get( - userIds: List?, + userIds: List?, fields: String?, nomCase: String? ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { @@ -38,7 +38,9 @@ class UsersRepositoryImpl( val users = response.map(VkUserData::mapToDomain) - launch { storeUsers(users) } + launch(Dispatchers.IO) { + storeUsers(users) + } VkMemoryCache.appendUsers(users) @@ -51,7 +53,7 @@ class UsersRepositoryImpl( } override suspend fun getLocalUsers( - userIds: List + userIds: List ): List = withContext(Dispatchers.IO) { dao.getAllByIds(userIds).map(VkUserEntity::asExternalModel) } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepository.kt index 97c5fe67..40478fed 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepository.kt @@ -6,7 +6,7 @@ interface AccountsRepository { suspend fun getAccounts(): List - suspend fun getAccountById(userId: Int): AccountEntity? + suspend fun getAccountById(userId: Long): AccountEntity? suspend fun storeAccounts(accounts: List) } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepositoryImpl.kt index ae5e254b..5e77e94d 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/db/AccountsRepositoryImpl.kt @@ -9,7 +9,7 @@ class AccountsRepositoryImpl( override suspend fun getAccounts(): List = accountDao.getAll() - override suspend fun getAccountById(userId: Int): AccountEntity? = + override suspend fun getAccountById(userId: Long): AccountEntity? = accountDao.getById(userId) override suspend fun storeAccounts( diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/di/DataModule.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/di/DataModule.kt index 10e878af..d091e805 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/di/DataModule.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/di/DataModule.kt @@ -65,7 +65,6 @@ val dataModule = module { singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class - // TODO: 11/08/2024, Danil Nikolaev: find a better solution single(named("token_interceptor")) { AccessTokenInterceptor() } diff --git a/core/database/schemas/dev.meloda.fast.database.AccountsDatabase/2.json b/core/database/schemas/dev.meloda.fast.database.AccountsDatabase/2.json index 116a04f5..5f5007c3 100644 --- a/core/database/schemas/dev.meloda.fast.database.AccountsDatabase/2.json +++ b/core/database/schemas/dev.meloda.fast.database.AccountsDatabase/2.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "3ebd234270e36902d3d461af38664869", + "identityHash": "ca007bca2ab4a9b901662792042770ad", "entities": [ { "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, PRIMARY KEY(`userId`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, `exchangeToken` TEXT, PRIMARY KEY(`userId`))", "fields": [ { "fieldPath": "userId", @@ -31,6 +31,12 @@ "columnName": "trustedHash", "affinity": "TEXT", "notNull": false + }, + { + "fieldPath": "exchangeToken", + "columnName": "exchangeToken", + "affinity": "TEXT", + "notNull": false } ], "primaryKey": { @@ -46,7 +52,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ebd234270e36902d3d461af38664869')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ca007bca2ab4a9b901662792042770ad')" ] } } \ No newline at end of file diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/AccountsDatabase.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/AccountsDatabase.kt index 0f31410e..e2576c65 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/AccountsDatabase.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/AccountsDatabase.kt @@ -7,7 +7,7 @@ import dev.meloda.fast.model.database.AccountEntity @Database( entities = [AccountEntity::class], - version = 2 + version = 3 ) abstract class AccountsDatabase : RoomDatabase() { abstract fun accountDao(): AccountDao diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt index 4bb5af38..c4abb7f2 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt @@ -6,7 +6,7 @@ import androidx.room.TypeConverters import dev.meloda.fast.database.dao.ConversationDao import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.MessageDao -import dev.meloda.fast.database.dao.UsersDao +import dev.meloda.fast.database.dao.UserDao import dev.meloda.fast.database.typeconverters.Converters import dev.meloda.fast.model.database.VkConversationEntity import dev.meloda.fast.model.database.VkGroupEntity @@ -21,11 +21,11 @@ import dev.meloda.fast.model.database.VkUserEntity VkConversationEntity::class ], - version = 8 + version = 10 ) @TypeConverters(Converters::class) abstract class CacheDatabase : RoomDatabase() { - abstract fun userDao(): UsersDao + abstract fun userDao(): UserDao abstract fun groupDao(): GroupDao abstract fun messageDao(): MessageDao abstract fun conversationDao(): ConversationDao diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/AccountDao.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/AccountDao.kt index 2ed58fa8..bbbdddb7 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/AccountDao.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/AccountDao.kt @@ -11,8 +11,8 @@ abstract class AccountDao : EntityDao { abstract suspend fun getAll(): List @Query("SELECT * FROM accounts WHERE userId = :userId") - abstract suspend fun getById(userId: Int): AccountEntity? + abstract suspend fun getById(userId: Long): AccountEntity? @Query("DELETE FROM accounts WHERE userId = :userId") - abstract suspend fun deleteById(userId: Int) + abstract suspend fun deleteById(userId: Long) } diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConversationDao.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConversationDao.kt index a435fb94..8f40b279 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConversationDao.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConversationDao.kt @@ -16,11 +16,11 @@ abstract class ConversationDao : EntityDao { abstract suspend fun getAllByIds(ids: List): List @Query("SELECT * FROM conversations WHERE id IS (:id)") - abstract suspend fun getById(id: Int): VkConversationEntity? + abstract suspend fun getById(id: Long): VkConversationEntity? @Transaction @Query("SELECT * FROM conversations WHERE id IS (:id)") - abstract suspend fun getByIdWithMessage(id: Int): ConversationWithMessage? + abstract suspend fun getByIdWithMessage(id: Long): ConversationWithMessage? @Query("DELETE FROM conversations WHERE rowid IN (:ids)") abstract suspend fun deleteByIds(ids: List): Int diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt index 3a5d844d..68e9b634 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt @@ -11,13 +11,13 @@ abstract class MessageDao : EntityDao { abstract suspend fun getAll(): List @Query("SELECT * FROM messages WHERE peerId IS (:conversationId)") - abstract suspend fun getAll(conversationId: Int): List + abstract suspend fun getAll(conversationId: Long): List @Query("SELECT * FROM messages WHERE id IN (:ids)") abstract suspend fun getAllByIds(ids: List): List @Query("SELECT * FROM messages WHERE id IS (:messageId)") - abstract suspend fun getById(messageId: Int): VkMessageEntity? + abstract suspend fun getById(messageId: Long): VkMessageEntity? @Query("DELETE FROM messages WHERE id IN (:ids)") abstract suspend fun deleteByIds(ids: List): Int diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/UsersDao.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/UserDao.kt similarity index 65% rename from core/database/src/main/kotlin/dev/meloda/fast/database/dao/UsersDao.kt rename to core/database/src/main/kotlin/dev/meloda/fast/database/dao/UserDao.kt index a2ca9b23..086936a0 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/UsersDao.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/UserDao.kt @@ -5,14 +5,14 @@ import androidx.room.Query import dev.meloda.fast.model.database.VkUserEntity @Dao -abstract class UsersDao : EntityDao { +abstract class UserDao : EntityDao { @Query("SELECT * FROM users") abstract suspend fun getAll(): List @Query("SELECT * FROM users WHERE id IN (:ids)") - abstract suspend fun getAllByIds(ids: List): List + abstract suspend fun getAllByIds(ids: List): List @Query("DELETE FROM users WHERE id IN (:ids)") - abstract suspend fun deleteByIds(ids: List): Int + abstract suspend fun deleteByIds(ids: List): Int } diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt index 4405d9ed..251e807c 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/di/DatabaseModule.kt @@ -3,12 +3,15 @@ package dev.meloda.fast.database.di import androidx.room.Room import dev.meloda.fast.database.AccountsDatabase import dev.meloda.fast.database.CacheDatabase +import dev.meloda.fast.database.di.migration.migrationFrom2To3 import org.koin.core.scope.Scope import org.koin.dsl.module val databaseModule = module { single { - Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts").build() + Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts") + .addMigrations(migrationFrom2To3) + .build() } single { get().accountDao() } diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/di/migration/MigrationFrom2To3.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/di/migration/MigrationFrom2To3.kt new file mode 100644 index 00000000..1099d79a --- /dev/null +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/di/migration/MigrationFrom2To3.kt @@ -0,0 +1,14 @@ +package dev.meloda.fast.database.di.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val migrationFrom2To3 = object : Migration( + startVersion = 2, + endVersion = 3 +) { + + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE accounts ADD COLUMN exchangeToken TEXT DEFAULT null") + } +} diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/typeconverters/Converters.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/typeconverters/Converters.kt index 589e705b..fd439916 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/typeconverters/Converters.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/typeconverters/Converters.kt @@ -13,6 +13,15 @@ class Converters { .split(", ") .mapNotNull(String::toIntOrNull) + @TypeConverter + fun longListToString(list: List): String = list.joinToString() + + @TypeConverter + fun stringToLongList(string: String): List = + string + .split(", ") + .mapNotNull(String::toLongOrNull) + @TypeConverter fun stringListToString(list: List): String = list.joinToString() diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt index 4d869d65..8f69cdf8 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt @@ -45,7 +45,7 @@ object SettingsKeys { const val KEY_ENABLE_HAPTIC = "enable_haptic" const val DEFAULT_ENABLE_HAPTIC = true const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level" - const val DEFAULT_NETWORK_LOG_LEVEL = 0 + const val DEFAULT_NETWORK_LOG_LEVEL = 3 const val KEY_USE_SYSTEM_FONT = "use_system_font" const val DEFAULT_USE_SYSTEM_FONT = false const val KEY_MORE_ANIMATIONS = "more_animations" @@ -53,5 +53,5 @@ object SettingsKeys { const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" - const val ID_DMITRY = 37610580 + const val ID_DMITRY = 37610580L } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCase.kt index 21c3c43a..897a6bb6 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCase.kt @@ -1,12 +1,32 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State +import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse +import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse +import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import kotlinx.coroutines.flow.Flow -interface AuthUseCase { +interface AuthUseCase : BaseUseCase { + + fun logout(): Flow> fun validatePhone( validationSid: String ): Flow> + + suspend fun getAnonymToken( + clientId: String, + clientSecret: String + ): Flow> + + suspend fun exchangeSilentToken( + anonymToken: String, + silentToken: String, + silentUuid: String + ): Flow> + + suspend fun getExchangeToken( + accessToken: String + ): Flow> } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCaseImpl.kt index 89676bcb..bb279844 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/AuthUseCaseImpl.kt @@ -3,16 +3,44 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State import dev.meloda.fast.data.api.auth.AuthRepository import dev.meloda.fast.data.mapToState +import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse +import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse +import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow class AuthUseCaseImpl(private val repository: AuthRepository) : AuthUseCase { - override fun validatePhone(validationSid: String): Flow> = flow { - emit(State.Loading) + override fun logout(): Flow> = flowNewState { repository.logout().mapToState() } - val newState = repository.validatePhone(validationSid).mapToState() - emit(newState) + override fun validatePhone(validationSid: String): Flow> = + flowNewState { repository.validatePhone(validationSid = validationSid).mapToState() } + + override suspend fun getAnonymToken( + clientId: String, + clientSecret: String + ): Flow> = flowNewState { + repository.getAnonymToken( + clientId = clientId, + clientSecret = clientSecret + ).mapToState() + } + + override suspend fun exchangeSilentToken( + anonymToken: String, + silentToken: String, + silentUuid: String + ): Flow> = flowNewState { + repository.exchangeSilentToken( + anonymToken = anonymToken, + silentToken = silentToken, + silentUuid = silentUuid + ).mapToState() + } + + override suspend fun getExchangeToken( + accessToken: String + ): Flow> = flowNewState { + repository.getExchangeToken(accessToken = accessToken).mapToState() } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/BaseUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/BaseUseCase.kt new file mode 100644 index 00000000..c3aa674f --- /dev/null +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/BaseUseCase.kt @@ -0,0 +1,16 @@ +package dev.meloda.fast.domain + +import dev.meloda.fast.data.State +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow + +interface BaseUseCase { + + suspend fun FlowCollector>.emitState(stateBlock: suspend () -> State) { + emit(State.Loading) + emit(stateBlock()) + } + + fun flowNewState(stateBlock: suspend () -> State) = + flow { emitState(stateBlock) } +} diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCase.kt index d8d86b4e..895866aa 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCase.kt @@ -1,19 +1,29 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State +import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.api.domain.VkConversation import kotlinx.coroutines.flow.Flow -interface ConversationsUseCase { - - fun getConversations( - count: Int?, - offset: Int?, - ): Flow>> - - fun delete(peerId: Int): Flow> - - fun changePinState(peerId: Int, pin: Boolean): Flow> +interface ConversationsUseCase : BaseUseCase { suspend fun storeConversations(conversations: List) + + fun getConversations( + count: Int? = null, + offset: Int? = null, + filter: ConversationsFilter + ): Flow>> + + fun getById( + peerIds: List, + extended: Boolean? = null, + fields: String? = null + ): Flow>> + + fun delete(peerId: Long): Flow> + + fun changePinState(peerId: Long, pin: Boolean): Flow> + + fun changeArchivedState(peerId: Long, archive: Boolean): Flow> } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCaseImpl.kt index 88c6e70d..6574153d 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConversationsUseCaseImpl.kt @@ -3,116 +3,69 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State import dev.meloda.fast.data.api.conversations.ConversationsRepository import dev.meloda.fast.data.mapToState +import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.api.domain.VkConversation import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext class ConversationsUseCaseImpl( private val repository: ConversationsRepository, ) : ConversationsUseCase { - // override fun getConversations( -// count: Int?, -// offset: Int?, -// fields: String, -// filter: String, -// extended: Boolean?, -// startMessageId: Int? -// ): Flow> = flow { -// emit(dev.meloda.fast.network.State.Loading) -// -// val newState = conversationsRepository.getConversations( -// params = ConversationsGetRequest( -// count = count, -// offset = offset, -// fields = fields, -// filter = filter, -// extended = extended, -// startMessageId = startMessageId -// ) -// ).fold( -// onSuccess = { response -> dev.meloda.fast.network.State.Success(response.toDomain()) }, -// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError }, -// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR }, -// onHttpFailure = { result -> result.error.toStateApiError() }, -// onApiFailure = { result -> result.error.toStateApiError() } -// ) -// emit(newState) -// } -// - - // -// override fun pin(peerId: Int): Flow> = flow { -// emit(dev.meloda.fast.network.State.Loading) -// -// val newState = conversationsRepository.pin( -// ConversationsPinRequest(peerId = peerId) -// ).fold( -// onSuccess = { dev.meloda.fast.network.State.Success(Unit) }, -// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError }, -// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR }, -// onHttpFailure = { result -> result.error.toStateApiError() }, -// onApiFailure = { result -> result.error.toStateApiError() } -// ) -// emit(newState) -// } -// -// override fun unpin(peerId: Int): Flow> = flow { -// emit(dev.meloda.fast.network.State.Loading) -// -// val newState = conversationsRepository.unpin( -// ConversationsUnpinRequest(peerId = peerId) -// ).fold( -// onSuccess = { dev.meloda.fast.network.State.Success(Unit) }, -// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError }, -// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR }, -// onHttpFailure = { result -> result.error.toStateApiError() }, -// onApiFailure = { result -> result.error.toStateApiError() } -// ) -// emit(newState) -// } -// -// override suspend fun storeConversations(conversations: List) { -// conversationsDao.insertAll(conversations.map(VkConversationDomain::mapToDb)) -// } -// -// override suspend fun storeGroups(groups: List) { -// groupsDao.insertAll(groups.map(VkGroupDomain::mapToDB)) -// } - override fun getConversations( - count: Int?, - offset: Int? - ): Flow>> = flow { - emit(State.Loading) - - val newState = repository.getConversations(count, offset).mapToState() - emit(newState) - } - override suspend fun storeConversations( conversations: List ) = withContext(Dispatchers.IO) { repository.storeConversations(conversations) } - override fun delete(peerId: Int): Flow> = flow { - emit(State.Loading) - - val newState = repository.delete(peerId = peerId).mapToState() - emit(newState) + override fun getConversations( + count: Int?, + offset: Int?, + filter: ConversationsFilter + ): Flow>> = flowNewState { + repository.getConversations( + count = count, + offset = offset, + filter = filter + ).mapToState() } - override fun changePinState(peerId: Int, pin: Boolean): Flow> = flow { - emit(State.Loading) + override fun getById( + peerIds: List, + extended: Boolean?, + fields: String? + ): Flow>> = flowNewState { + repository.getConversationsById( + peerIds = peerIds, + extended = extended, + fields = fields + ).mapToState() + } - val newState = if (pin) { + override fun delete(peerId: Long): Flow> = flowNewState { + repository.delete(peerId = peerId).mapToState() + } + + override fun changePinState( + peerId: Long, + pin: Boolean + ): Flow> = flowNewState { + if (pin) { repository.pin(peerId) } else { repository.unpin(peerId) }.mapToState() + } - emit(newState) + override fun changeArchivedState( + peerId: Long, + archive: Boolean + ): Flow> = flowNewState { + if (archive) { + repository.archive(peerId) + } else { + repository.unarchive(peerId) + }.mapToState() } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt index 67b05e14..731c6aab 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCase.kt @@ -22,7 +22,7 @@ interface FriendsUseCase { fun getOnlineFriends( count: Int?, offset: Int? - ): Flow>> + ): Flow>> suspend fun storeUsers(users: List) } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt index 53c54945..7790af9b 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/FriendsUseCaseImpl.kt @@ -36,7 +36,7 @@ class FriendsUseCaseImpl(private val repository: FriendsRepository) : override fun getOnlineFriends( count: Int?, offset: Int? - ): Flow>> = flow { + ): Flow>> = flow { emit(State.Loading) val newState = repository.getOnlineFriends(count, offset).mapToState() 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 37d322dc..ea43539c 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 @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flow class GetLocalUserByIdUseCase(private val repository: UsersRepository) { - operator fun invoke(userId: Int): Flow> = flow { + operator fun invoke(userId: Long): Flow> = flow { emit(State.Loading) val newState = kotlin.runCatching { @@ -21,7 +21,7 @@ class GetLocalUserByIdUseCase(private val repository: UsersRepository) { emit(newState) } - suspend fun proceed(userId: Int): VkUser? { + suspend fun proceed(userId: Long): VkUser? { return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull() } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUsersByIdsUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUsersByIdsUseCase.kt index e371323a..ceb31c9a 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUsersByIdsUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/GetLocalUsersByIdsUseCase.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flow class GetLocalUsersByIdsUseCase(private val repository: UsersRepository) { - operator fun invoke(userIds: List): Flow>> = flow { + operator fun invoke(userIds: List): Flow>> = flow { emit(State.Loading) val newState = kotlin.runCatching { diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt index 309430ab..be9f2830 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt @@ -5,19 +5,21 @@ import dev.meloda.fast.data.api.conversations.ConversationsRepository import dev.meloda.fast.data.mapToState import dev.meloda.fast.model.api.domain.VkConversation import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow class LoadConversationsByIdUseCase( private val conversationsRepository: ConversationsRepository -) { +) : BaseUseCase { - operator fun invoke(peerIds: List): Flow>> = flow { - emit(State.Loading) - - val newState = conversationsRepository - .getConversationsById(peerIds = peerIds) - .mapToState() - - emit(newState) + operator fun invoke( + peerIds: List, + extended: Boolean? = null, + fields: String? = null + ): Flow>> = flowNewState { + conversationsRepository + .getConversationsById( + peerIds = peerIds, + extended = extended, + fields = fields, + ).mapToState() } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUserByIdUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUserByIdUseCase.kt index a8b93a8a..ad776240 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUserByIdUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUserByIdUseCase.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.flow class LoadUserByIdUseCase(private val repository: UsersRepository) { operator fun invoke( - userId: Int?, + userId: Long?, fields: String = VkConstants.USER_FIELDS, nomCase: String? = null ): Flow> = flow { diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUsersByIdsUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUsersByIdsUseCase.kt index 0f845ba6..49f523e2 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUsersByIdsUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadUsersByIdsUseCase.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.flow class LoadUsersByIdsUseCase(private val repository: UsersRepository) { operator fun invoke( - userIds: List?, + userIds: List?, fields: String = VkConstants.USER_FIELDS, nomCase: String? = null ): Flow>> = flow { diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt index e66b4734..5223a06e 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt @@ -3,21 +3,24 @@ package dev.meloda.fast.domain import android.util.Log import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.extensions.asInt +import dev.meloda.fast.common.extensions.asLong import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.toList import dev.meloda.fast.data.UserConfig -import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.processState import dev.meloda.fast.model.ApiEvent +import dev.meloda.fast.model.ConversationFlags import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.MessageFlags +import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkMessage import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext @@ -25,6 +28,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class LongPollUpdatesParser( + private val conversationsUseCase: ConversationsUseCase, private val messagesUseCase: MessagesUseCase ) { private val job = SupervisorJob() @@ -68,6 +72,458 @@ class LongPollUpdatesParser( ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event) ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event) + ApiEvent.MESSAGE_UPDATED -> parseMessageUpdated(eventType, event) + ApiEvent.MESSAGE_CACHE_CLEAR -> parseMessageCacheClear(eventType, event) + } + } + + private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val cmId = event[1].asLong() + val flags = event[2].asInt() + val peerId = event[3].asLong() + + val eventsToSend = mutableListOf() + + val parsedFlags = MessageFlags.parse(flags) + parsedFlags.forEach { flag -> + when (flag) { + MessageFlags.IMPORTANT -> { + val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant( + peerId = peerId, + cmId = cmId, + marked = true + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.SPAM -> { + val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam( + peerId = peerId, + cmId = cmId + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.DELETED -> { + val eventToSend = + if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) { + LongPollParsedEvent.MessageDeleted( + peerId = peerId, + cmId = cmId, + forAll = true + ) + } else { + LongPollParsedEvent.MessageDeleted( + peerId = peerId, + cmId = cmId, + forAll = false + ) + } + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.AUDIO_LISTENED -> { + val eventToSend = LongPollParsedEvent.AudioMessageListened( + peerId = peerId, + cmId = cmId + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + else -> Unit + } + } + + eventsToSend.forEach { eventToSend -> + listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback)?.onEvent(eventToSend) + } + } + } + } + + private fun parseMessageClearFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val cmId = event[1].asLong() + val flags = event[2].asInt() + val peerId = event[3].asLong() + + val eventsToSend = mutableListOf() + + val parsedFlags = MessageFlags.parse(flags) + + coroutineScope.launch { + parsedFlags.forEach { flag -> + when (flag) { + MessageFlags.IMPORTANT -> { + val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant( + peerId = peerId, + cmId = cmId, + marked = false + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.SPAM -> { + if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) { + withContext(Dispatchers.IO) { + val message = loadMessage( + peerId = peerId, + cmId = cmId + ) + message?.let { + val eventToSend = + LongPollParsedEvent.MessageMarkedAsNotSpam(message = message) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + } + } + } + + MessageFlags.DELETED -> { + withContext(Dispatchers.IO) { + val message = loadMessage( + peerId = peerId, + cmId = cmId + ) + message?.let { + val eventToSend = + LongPollParsedEvent.MessageRestored(message = message) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + } + } + + else -> Unit + } + } + + eventsToSend.forEach { eventToSend -> + listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners -> + listeners.map { vkEventCallback -> + vkEventCallback.onEvent(eventToSend) + } + } + } + } + } + + private fun parseMessageNew(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val cmId = event[1].asLong() + val peerId = event[4].asLong() + + coroutineScope.launch(Dispatchers.IO) { + val message = + async { loadMessage(peerId = peerId, cmId = cmId) }.await() + + val conversation = + async { + loadConversation( + peerId = peerId, + extended = true, + fields = VkConstants.ALL_FIELDS + ) + }.await() + + message?.let { + listenersMap[LongPollEvent.MESSAGE_NEW]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.NewMessage( + message = message, + inArchive = conversation?.isArchived == true + // TODO: 03-Apr-25, Danil Nikolaev: + // load user settings about restoring chats with + // enabled notifications from archive + ) + ) + } + } + } + } + } + + private fun parseMessageEdit(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val cmId = event[1].asLong() + val peerId = event[3].asLong() + + coroutineScope.launch(Dispatchers.IO) { + loadMessage( + peerId = peerId, + cmId = cmId + )?.let { message -> + listenersMap[LongPollEvent.MESSAGE_EDITED]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent(LongPollParsedEvent.MessageEdited(message)) + } + } + } + } + } + + private fun parseMessageReadIncoming(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val peerId = event[1].asLong() + val cmId = event[2].asLong() + val unreadCount = event[3].asInt() + + listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.IncomingMessageRead( + peerId = peerId, + cmId = cmId, + unreadCount = unreadCount + ) + ) + } + } + } + + private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val peerId = event[1].asLong() + val cmId = event[2].asLong() + val unreadCount = event[3].asInt() + + listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.OutgoingMessageRead( + peerId = peerId, + cmId = cmId, + unreadCount = unreadCount + ) + ) + } + } + } + + private fun parseChatClearFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asLong() + val flags = event[2].asInt() + + val eventsToSend = mutableListOf() + + val parsedFlags = ConversationFlags.parse(flags) + + coroutineScope.launch(Dispatchers.IO) { + parsedFlags.forEach { flag -> + when (flag) { + ConversationFlags.ARCHIVED -> { + val conversation = loadConversation( + peerId = peerId, + extended = true, + fields = VkConstants.ALL_FIELDS + ) ?: return@forEach + + val message = loadMessage( + peerId = peerId, + cmId = conversation.lastCmId + ) + + val eventToSend = LongPollParsedEvent.ChatArchived( + conversation = conversation.copy(lastMessage = message), + archived = false + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + else -> Unit + } + } + + eventsToSend.forEach { eventToSend -> + listenersMap[LongPollEvent.CHAT_CLEAR_FLAGS]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback)?.onEvent( + eventToSend + ) + } + } + } + } + } + + private fun parseChatSetFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asLong() + val flags = event[2].asInt() + + val eventsToSend = mutableListOf() + + val parsedFlags = ConversationFlags.parse(flags) + + coroutineScope.launch(Dispatchers.IO) { + parsedFlags.forEach { flag -> + when (flag) { + ConversationFlags.ARCHIVED -> { + val conversation = loadConversation( + peerId = peerId, + extended = true, + fields = VkConstants.ALL_FIELDS + ) ?: return@forEach + + val message = loadMessage( + peerId = peerId, + cmId = conversation.lastCmId + ) + + val eventToSend = LongPollParsedEvent.ChatArchived( + conversation = conversation.copy(lastMessage = message), + archived = true + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + else -> Unit + } + } + + eventsToSend.forEach { eventToSend -> + listenersMap[LongPollEvent.CHAT_SET_FLAGS]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback)?.onEvent( + eventToSend + ) + } + } + } + } + } + + private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asLong() + val cmId = event[2].asLong() + + listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.ChatCleared( + peerId = peerId, + toCmId = cmId + ) + ) + } + } + } + + private fun parseChatMajorChanged(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asLong() + val majorId = event[2].asInt() + + listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.ChatMajorChanged( + peerId = peerId, + majorId = majorId, + ) + ) + } + } + } + + private fun parseChatMinorChanged(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asLong() + val minorId = event[2].asInt() + + listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.ChatMinorChanged( + peerId = peerId, + minorId = minorId, + ) + ) + } } } @@ -92,8 +548,8 @@ class LongPollUpdatesParser( else -> return } - val peerId = event[1].asInt() - val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId } + val peerId = event[1].asLong() + val userIds = event[2].toList(Any::asLong).filter { it != UserConfig.userId } val totalCount = event[3].asInt() val timestamp = event[4].asInt() @@ -145,325 +601,57 @@ class LongPollUpdatesParser( } } - private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") + private fun parseMessageUpdated(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType $event") - val messageId = event[1].asInt() - val flags = event[2].asInt() - val peerId = event[3].asInt() - - val eventsToSend = mutableListOf() - - val parsedFlags = MessageFlags.parse(flags) - parsedFlags.forEach { flag -> - when (flag) { - MessageFlags.IMPORTANT -> { // marked as important - val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant( - peerId = peerId, - messageId = messageId, - marked = true - ) - eventsToSend += eventToSend - - listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } - - MessageFlags.SPAM -> { // marked as spam - val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam( - peerId = peerId, - messageId = messageId - ) - eventsToSend += eventToSend - - listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } - - MessageFlags.DELETED -> { - val eventToSend = - if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) { // deleted for all - LongPollParsedEvent.MessageDeleted( - peerId = peerId, - messageId = messageId, - forAll = true - ) - } else { // deleted only for me - LongPollParsedEvent.MessageDeleted( - peerId = peerId, - messageId = messageId, - forAll = false - ) - } - eventsToSend += eventToSend - - listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } - - MessageFlags.AUDIO_LISTENED -> { // audio message listened - val eventToSend = LongPollParsedEvent.AudioMessageListened( - peerId = peerId, - messageId = messageId - ) - eventsToSend += eventToSend - - listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } - - else -> Unit - } - } - - eventsToSend.forEach { eventToSend -> - listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback)?.onEvent(eventToSend) - } - } - } - } - - private fun parseMessageClearFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - - val messageId = event[1].asInt() - val flags = event[2].asInt() - val peerId = event[3].asInt() - - val eventsToSend = mutableListOf() - - val parsedFlags = MessageFlags.parse(flags) - - coroutineScope.launch { - parsedFlags.forEach { flag -> - when (flag) { - MessageFlags.IMPORTANT -> { // not important anymore - val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant( - peerId = peerId, - messageId = messageId, - marked = false - ) - eventsToSend += eventToSend - - listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } - - MessageFlags.SPAM -> { - if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) { // not spam anymore - withContext(Dispatchers.IO) { - val message = loadMessage(messageId) - message?.let { - val eventToSend = - LongPollParsedEvent.MessageMarkedAsNotSpam(message = message) - eventsToSend += eventToSend - - listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } - } - } - } - - MessageFlags.DELETED -> { // restored - withContext(Dispatchers.IO) { - val message = loadMessage(messageId) - message?.let { - val eventToSend = - LongPollParsedEvent.MessageRestored(message = message) - eventsToSend += eventToSend - - listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } - } - } - - else -> Unit - } - } - - eventsToSend.forEach { eventToSend -> - listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners -> - listeners.map { vkEventCallback -> - vkEventCallback.onEvent(eventToSend) - } - } - } - } - } - - private fun parseMessageNew(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val messageId = event[1].asInt() + val cmId = event[1].asLong() + val peerId = event[4].asLong() coroutineScope.launch(Dispatchers.IO) { - loadMessage(messageId)?.let { message -> - listenersMap[LongPollEvent.MESSAGE_NEW]?.let { + loadMessage( + peerId = peerId, + cmId = cmId + )?.let { message -> + listenersMap[LongPollEvent.MESSAGE_UPDATED]?.let { it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(LongPollParsedEvent.NewMessage(message)) + (vkEventCallback as VkEventCallback) + .onEvent(LongPollParsedEvent.MessageUpdated(message)) } } } } } - private fun parseMessageEdit(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val messageId = event[1].asInt() + private fun parseMessageCacheClear(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType $event") + + val messageId = event[1].asLong() coroutineScope.launch(Dispatchers.IO) { - loadMessage(messageId)?.let { message -> - listenersMap[LongPollEvent.MESSAGE_EDITED]?.let { + loadMessage(messageId = messageId)?.let { message -> + listenersMap[LongPollEvent.MESSAGE_CACHE_CLEAR]?.let { it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(LongPollParsedEvent.MessageEdited(message)) + (vkEventCallback as VkEventCallback) + .onEvent(LongPollParsedEvent.MessageCacheClear(message)) } } } } } - private fun parseMessageReadIncoming(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val peerId = event[1].asInt() - val messageId = event[2].asInt() - val unreadCount = event[3].asInt() + private suspend fun loadMessage( + peerId: Long? = null, + cmId: Long? = null, + messageId: Long? = null + ): VkMessage? = suspendCoroutine { continuation -> + require((peerId != null && cmId != null) || messageId != null) - listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.IncomingMessageRead( - peerId = peerId, - messageId = messageId, - unreadCount = unreadCount - ) - ) - } - } - } - - private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val peerId = event[1].asInt() - val messageId = event[2].asInt() - val unreadCount = event[3].asInt() - - listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.OutgoingMessageRead( - peerId = peerId, - messageId = messageId, - unreadCount = unreadCount - ) - ) - } - } - } - - private fun parseChatClearFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - } - - private fun parseChatSetFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - } - - private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - - val peerId = event[1].asInt() - val messageId = event[2].asInt() - - listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.ChatCleared( - peerId = peerId, - toMessageId = messageId - ) - ) - } - } - } - - private fun parseChatMajorChanged(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - - val peerId = event[1].asInt() - val majorId = event[2].asInt() - - listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.ChatMajorChanged( - peerId = peerId, - majorId = majorId, - ) - ) - } - } - } - - private fun parseChatMinorChanged(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - - val peerId = event[1].asInt() - val minorId = event[2].asInt() - - listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.ChatMinorChanged( - peerId = peerId, - minorId = minorId, - ) - ) - } - } - } - - private suspend fun loadMessage(messageId: Int): VkMessage? = suspendCoroutine { continuation -> coroutineScope.launch(Dispatchers.IO) { messagesUseCase.getById( - messageIds = listOf(messageId), + peerCmIds = null, + peerId = peerId, + messageIds = messageId?.let(::listOf), + cmIds = cmId?.let(::listOf), extended = true, fields = VkConstants.ALL_FIELDS ).listenValue(this) { state -> @@ -478,9 +666,6 @@ class LongPollUpdatesParser( return@listenValue } - VkMemoryCache[message.id] = message - messagesUseCase.storeMessage(message) - continuation.resume(message) } ) @@ -488,6 +673,35 @@ class LongPollUpdatesParser( } } + private suspend fun loadConversation( + peerId: Long, + extended: Boolean = false, + fields: String? = null + ): VkConversation? = suspendCoroutine { continuation -> + coroutineScope.launch(Dispatchers.IO) { + conversationsUseCase.getById( + peerIds = listOf(peerId), + extended = extended, + fields = fields + ).listenValue(coroutineScope) { state -> + state.processState( + error = { error -> + Log.e("LongPollUpdatesParser", "loadConversation: error: $error") + continuation.resume(null) + }, + success = { response -> + val conversation = response.singleOrNull() ?: run { + continuation.resume(null) + return@listenValue + } + + continuation.resume(conversation) + } + ) + } + } + } + @Suppress("UNCHECKED_CAST") private fun registerListener( eventType: LongPollEvent, @@ -564,6 +778,10 @@ class LongPollUpdatesParser( registerListener(LongPollEvent.CHAT_MINOR_CHANGED, assembleEventCallback(block)) } + fun onChatArchived(block: (LongPollParsedEvent.ChatArchived) -> Unit) { + registerListener(LongPollEvent.CHAT_ARCHIVED, assembleEventCallback(block)) + } + fun onInteractions(block: (LongPollParsedEvent.Interaction) -> Unit) { registerListeners( eventTypes = listOf( @@ -576,10 +794,6 @@ class LongPollUpdatesParser( listener = assembleEventCallback(block) ) } - - fun clearListeners() { - listenersMap.clear() - } } internal inline fun assembleEventCallback( 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 aa120a6b..3694a50e 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 @@ -5,71 +5,77 @@ import dev.meloda.fast.data.api.messages.MessagesHistoryInfo 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.responses.MessagesSendResponse import kotlinx.coroutines.flow.Flow -interface MessagesUseCase { +interface MessagesUseCase : BaseUseCase { + + suspend fun storeMessage(message: VkMessage) + suspend fun storeMessages(messages: List) fun getMessagesHistory( - conversationId: Int, + conversationId: Long, count: Int?, offset: Int? ): Flow> fun getById( - messageIds: List, + peerCmIds: List?, + peerId: Long?, + messageIds: List?, + cmIds: List?, extended: Boolean?, fields: String? ): Flow>> fun sendMessage( - peerId: Int, - randomId: Int, + peerId: Long, + randomId: Long, message: String?, - replyTo: Int?, + replyTo: Long?, attachments: List? - ): Flow> + ): Flow> fun markAsRead( - peerId: Int, - startMessageId: Int + peerId: Long, + startMessageId: Long ): Flow> fun getHistoryAttachments( - peerId: Int, - count: Int?, - offset: Int?, + peerId: Long, + count: Int? = null, + offset: Int? = null, attachmentTypes: List, - conversationMessageId: Int + cmId: Long ): Flow>> fun createChat( - userIds: List?, - title: String? - ): Flow> + userIds: List? = null, + title: String + ): Flow> fun pin( - peerId: Int, - messageId: Int?, - conversationMessageId: Int? + peerId: Long, + messageId: Long? = null, + cmId: Long? = null ): Flow> fun unpin( - peerId: Int + peerId: Long ): Flow> fun markAsImportant( - peerId: Int, - messageIds: List, + peerId: Long, + messageIds: List? = null, + cmIds: List? = null, important: Boolean - ): Flow>> + ): Flow>> fun delete( - peerId: Int, - messageIds: List, + peerId: Long, + messageIds: List? = null, + cmIds: List? = null, spam: Boolean = false, deleteForAll: Boolean = false ): 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 b6e77697..166e39f4 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 @@ -7,163 +7,13 @@ import dev.meloda.fast.data.mapToState 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.responses.MessagesSendResponse import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow class MessagesUseCaseImpl( - private val repository: MessagesRepository + private val repository: MessagesRepository, ) : MessagesUseCase { - override fun getMessagesHistory( - conversationId: Int, - count: Int?, - offset: Int? - ): Flow> = flow { - emit(State.Loading) - - val newState = repository.getHistory( - conversationId = conversationId, - offset = offset, - count = count - ).mapToState() - - emit(newState) - } - - override fun getById( - messageIds: List, - extended: Boolean?, - fields: String? - ): Flow>> = flow { - emit(State.Loading) - - val newState = repository.getById( - messagesIds = messageIds, - extended = extended, - fields = fields - ).mapToState() - - emit(newState) - } - - override fun sendMessage( - peerId: Int, - randomId: Int, - message: String?, - replyTo: Int?, - attachments: List? - ): Flow> = flow { - emit(State.Loading) - - val newState = repository.send( - peerId = peerId, - randomId = randomId, - message = message, - replyTo = replyTo, - attachments = attachments - ).mapToState() - - emit(newState) - } - - override fun markAsRead( - peerId: Int, - startMessageId: Int - ): Flow> = flow { - emit(State.Loading) - - val newState = repository.markAsRead( - peerId = peerId, - startMessageId = startMessageId - ).mapToState() - - 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 fun createChat(userIds: List?, title: String?): Flow> = flow { - emit(State.Loading) - val newState = repository.createChat(userIds, title).mapToState() - emit(newState) - } - - override fun pin( - peerId: Int, - messageId: Int?, - conversationMessageId: Int? - ): Flow> = flow { - emit(State.Loading) - - val newState = repository.pin( - peerId = peerId, - messageId = messageId, - conversationMessageId = conversationMessageId - ).mapToState() - - emit(newState) - } - - override fun unpin(peerId: Int): Flow> = flow { - emit(State.Loading) - val newState = repository.unpin(peerId = peerId).mapToState() - emit(newState) - } - - override fun markAsImportant( - peerId: Int, - messageIds: List, - important: Boolean - ): Flow>> = flow { - emit(State.Loading) - - val newState = repository.markAsImportant( - peerId = peerId, - messageIds = messageIds, - conversationMessageIds = null, - important = important - ).mapToState() - - emit(newState) - } - - override fun delete( - peerId: Int, - messageIds: List, - spam: Boolean, - deleteForAll: Boolean - ): Flow>> = flow { - emit(State.Loading) - - val newState = repository.delete( - peerId = peerId, - messageIds = messageIds, - conversationMessageIds = null, - spam = spam, - deleteForAll = deleteForAll - ).mapToState() - - emit(newState) - } - override suspend fun storeMessage(message: VkMessage) { repository.storeMessages(listOf(message)) } @@ -171,4 +21,129 @@ class MessagesUseCaseImpl( override suspend fun storeMessages(messages: List) { repository.storeMessages(messages) } + + override fun getMessagesHistory( + conversationId: Long, + count: Int?, + offset: Int? + ): Flow> = flowNewState { + repository.getHistory( + conversationId = conversationId, + offset = offset, + count = count + ).mapToState() + } + + override fun getById( + peerCmIds: List?, + peerId: Long?, + messageIds: List?, + cmIds: List?, + extended: Boolean?, + fields: String? + ): Flow>> = flowNewState { + repository.getById( + peerCmIds = peerCmIds, + peerId = peerId, + messagesIds = messageIds, + cmIds = cmIds, + extended = extended, + fields = fields + ).mapToState() + } + + override fun sendMessage( + peerId: Long, + randomId: Long, + message: String?, + replyTo: Long?, + attachments: List? + ): Flow> = flowNewState { + repository.send( + peerId = peerId, + randomId = randomId, + message = message, + replyTo = replyTo, + attachments = attachments + ).mapToState() + } + + override fun markAsRead( + peerId: Long, + startMessageId: Long + ): Flow> = flowNewState { + repository.markAsRead( + peerId = peerId, + startMessageId = startMessageId + ).mapToState() + } + + override fun getHistoryAttachments( + peerId: Long, + count: Int?, + offset: Int?, + attachmentTypes: List, + cmId: Long + ): Flow>> = flowNewState { + repository.getHistoryAttachments( + peerId = peerId, + count = count, + offset = offset, + attachmentTypes = attachmentTypes, + cmId = cmId + ).mapToState() + } + + override fun createChat( + userIds: List?, + title: String + ): Flow> = flowNewState { + repository.createChat(userIds, title).mapToState() + } + + override fun pin( + peerId: Long, + messageId: Long?, + cmId: Long? + ): Flow> = flowNewState { + repository.pin( + peerId = peerId, + messageId = messageId, + cmId = cmId + ).mapToState() + } + + override fun unpin(peerId: Long): Flow> = flowNewState { + repository.unpin(peerId = peerId).mapToState() + } + + override fun markAsImportant( + peerId: Long, + messageIds: List?, + cmIds: List?, + important: Boolean + ): Flow>> = flowNewState { + repository.markAsImportant( + peerId = peerId, + messageIds = messageIds, + cmIds = cmIds, + important = important + ).mapToState() + } + + override fun delete( + peerId: Long, + messageIds: List?, + cmIds: List?, + spam: Boolean, + deleteForAll: Boolean + ): Flow>> = flowNewState { + repository.delete( + peerId = peerId, + messageIds = messageIds, + cmIds = cmIds, + spam = spam, + deleteForAll = deleteForAll + ).mapToState() + } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCase.kt index 4f8737ef..5a9d6b6f 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCase.kt @@ -2,6 +2,7 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State import dev.meloda.fast.model.AuthInfo +import dev.meloda.fast.model.api.responses.GetSilentTokenResponse import kotlinx.coroutines.flow.Flow interface OAuthUseCase { @@ -14,4 +15,13 @@ interface OAuthUseCase { captchaSid: String?, captchaKey: String? ): Flow> + + fun getSilentToken( + login: String, + password: String, + forceSms: Boolean, + validationCode: String?, + captchaSid: String?, + captchaKey: String? + ): Flow> } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt index 082f2c2d..e6e3da62 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt @@ -2,11 +2,9 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State import dev.meloda.fast.data.api.oauth.OAuthRepository +import dev.meloda.fast.data.asState import dev.meloda.fast.model.AuthInfo -import dev.meloda.fast.network.OAuthErrorDomain -import dev.meloda.fast.network.ValidationType -import dev.meloda.fast.network.VkOAuthError -import dev.meloda.fast.network.VkOAuthErrorType +import dev.meloda.fast.model.api.responses.GetSilentTokenResponse import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -24,109 +22,45 @@ class OAuthUseCaseImpl( ): Flow> = flow { emit(State.Loading) - val response = oAuthRepository.auth( + val newState = oAuthRepository.auth( login = login, password = password, + forceSms = forceSms, validationCode = validationCode, captchaSid = captchaSid, - captchaKey = captchaKey, - forceSms = forceSms - ) - - kotlin.runCatching { - val error = response.error?.let(VkOAuthError::parse) - val errorType = response.errorType?.let(VkOAuthErrorType::parse) - - val newState = when (error) { - null -> { - State.Success( - AuthInfo( - userId = response.userId, - accessToken = response.accessToken, - validationHash = response.validationHash - ) - ) - } - - VkOAuthError.FLOOD_CONTROL -> { - State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError) - } - - VkOAuthError.NEED_VALIDATION -> { - if (response.banInfo != null) { - val info = requireNotNull(response.banInfo) - - State.Error.OAuthError( - OAuthErrorDomain.UserBannedError( - memberName = info.memberName, - message = info.message, - accessToken = info.accessToken, - restoreUrl = info.restoreUrl - ) - ) - } else { - State.Error.OAuthError( - OAuthErrorDomain.ValidationRequiredError( - description = response.errorDescription.orEmpty(), - validationType = response.validationType.orEmpty() - .let(ValidationType::parse), - validationSid = response.validationSid.orEmpty(), - phoneMask = response.phoneMask.orEmpty(), - redirectUri = response.redirectUri.orEmpty(), - validationResend = response.validationResend, - restoreIfCannotGetCode = response.restoreIfCannotGetCode - ) - ) - } - } - - VkOAuthError.NEED_CAPTCHA -> { - State.Error.OAuthError( - OAuthErrorDomain.CaptchaRequiredError( - captchaSid = response.captchaSid.orEmpty(), - captchaImageUrl = response.captchaImage.orEmpty() - ) - ) - } - - VkOAuthError.INVALID_CLIENT -> { - State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError) - } - - VkOAuthError.INVALID_REQUEST -> { - when (errorType) { - null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError) - - VkOAuthErrorType.WRONG_OTP -> { - State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode) - } - - VkOAuthErrorType.WRONG_OTP_FORMAT -> { - State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat) - } - - VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> { - State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError) - } - - VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> { - State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError) - } - } - } - - VkOAuthError.UNKNOWN -> { - State.Error.OAuthError(OAuthErrorDomain.UnknownError) - } - } - - emit(newState) - }.fold( - onSuccess = { - }, - onFailure = { - emit(State.Error.TestError(it.stackTraceToString())) + captchaKey = captchaKey + ).asState( + successMapper = { + AuthInfo( + userId = it.userId!!, + accessToken = it.accessToken!!, + validationHash = it.validationHash!! + ) } ) + + emit(newState) + } + + override fun getSilentToken( + login: String, + password: String, + forceSms: Boolean, + validationCode: String?, + captchaSid: String?, + captchaKey: String? + ): Flow> = flow { + emit(State.Loading) + + val newState = oAuthRepository.getSilentToken( + login = login, + password = password, + forceSms = forceSms, + validationCode = validationCode, + captchaSid = captchaSid, + captchaKey = captchaKey + ).asState() + + emit(newState) } } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt index 1e2cbc14..49a3c4d9 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt @@ -1,15 +1,17 @@ package dev.meloda.fast.model enum class ApiEvent(val value: Int) { - MESSAGE_SET_FLAGS(2), - MESSAGE_CLEAR_FLAGS(3), - MESSAGE_NEW(4), - MESSAGE_EDIT(5), - MESSAGE_READ_INCOMING(6), - MESSAGE_READ_OUTGOING(7), + MESSAGE_SET_FLAGS(10002), + MESSAGE_CLEAR_FLAGS(10003), + MESSAGE_NEW(10004), + MESSAGE_EDIT(10005), + MESSAGE_READ_INCOMING(10006), + MESSAGE_READ_OUTGOING(10007), CHAT_CLEAR_FLAGS(10), CHAT_SET_FLAGS(12), - MESSAGES_DELETED(13), + MESSAGES_DELETED(10013), + MESSAGE_UPDATED(10018), + MESSAGE_CACHE_CLEAR(10019), CHAT_MAJOR_CHANGED(20), CHAT_MINOR_CHANGED(21), TYPING(63), diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/AuthInfo.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/AuthInfo.kt index 5afa9ba2..5b568175 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/AuthInfo.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/AuthInfo.kt @@ -1,7 +1,7 @@ package dev.meloda.fast.model data class AuthInfo( - val userId: Int?, - val accessToken: String?, - val validationHash: String? + val userId: Long, + val accessToken: String, + val validationHash: String ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/BaseError.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/BaseError.kt index 86cc6e2f..963c989a 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/BaseError.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/BaseError.kt @@ -6,6 +6,10 @@ import androidx.compose.runtime.Immutable sealed class BaseError { data object SessionExpired : BaseError() + data object AccountBlocked : BaseError() + data object ConnectionError : BaseError() + data object InternalError : BaseError() + data object UnknownError : BaseError() data class SimpleError(val message: String) : BaseError() } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt index 19ab4de6..fcae5e05 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt @@ -13,5 +13,20 @@ enum class ConversationFlags(val value: Int) { DO_NOT_NOTIFY_ALL_MENTIONS(524288), MARKED_AS_UNREAD(1048576), ARCHIVED(8388608), - CALL_IN_PROGRESS(16777216), + CALL_IN_PROGRESS(16777216); + + companion object { + + fun parse(mask: Int): List { + val flags = mutableListOf() + + ConversationFlags.entries.forEach { flag -> + if (mask and flag.value > 0) { + flags.add(flag) + } + } + + return flags + } + } } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationsFilter.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationsFilter.kt new file mode 100644 index 00000000..9e0ebfd2 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationsFilter.kt @@ -0,0 +1,5 @@ +package dev.meloda.fast.model + +enum class ConversationsFilter { + ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/FriendsInfo.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/FriendsInfo.kt index 2364476b..34e6aa5a 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/FriendsInfo.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/FriendsInfo.kt @@ -4,5 +4,5 @@ import dev.meloda.fast.model.api.domain.VkUser data class FriendsInfo( val friends: List, - val onlineFriendsIds: List + val onlineFriendsIds: List ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt index 2c25bc13..8622af44 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt @@ -21,7 +21,10 @@ enum class LongPollEvent { MARKED_AS_SPAM, MARKED_AS_NOT_SPAM, MESSAGE_DELETED, + MESSAGE_UPDATED, + MESSAGE_CACHE_CLEAR, MESSAGE_RESTORED, AUDIO_MESSAGE_LISTENED, - CHAT_CLEARED + CHAT_CLEARED, + CHAT_ARCHIVED } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt index 822a6cf8..4095ee8f 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt @@ -1,39 +1,47 @@ package dev.meloda.fast.model +import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkMessage sealed interface LongPollParsedEvent { - data class NewMessage(val message: VkMessage) : LongPollParsedEvent + data class NewMessage( + val message: VkMessage, + val inArchive: Boolean + ) : LongPollParsedEvent data class MessageEdited(val message: VkMessage) : LongPollParsedEvent + data class MessageUpdated(val message: VkMessage) : LongPollParsedEvent + + data class MessageCacheClear(val message: VkMessage) : LongPollParsedEvent + data class IncomingMessageRead( - val peerId: Int, - val messageId: Int, + val peerId: Long, + val cmId: Long, val unreadCount: Int, ) : LongPollParsedEvent data class OutgoingMessageRead( - val peerId: Int, - val messageId: Int, + val peerId: Long, + val cmId: Long, val unreadCount: Int, ) : LongPollParsedEvent data class ChatMajorChanged( - val peerId: Int, + val peerId: Long, val majorId: Int, ) : LongPollParsedEvent data class ChatMinorChanged( - val peerId: Int, + val peerId: Long, val minorId: Int ) : LongPollParsedEvent data class Interaction( val interactionType: InteractionType, - val peerId: Int, - val userIds: List, + val peerId: Long, + val userIds: List, val totalCount: Int, val timestamp: Int ) : LongPollParsedEvent @@ -49,14 +57,14 @@ sealed interface LongPollParsedEvent { ) : LongPollParsedEvent data class MessageMarkedAsImportant( - val peerId: Int, - val messageId: Int, + val peerId: Long, + val cmId: Long, val marked: Boolean ) : LongPollParsedEvent data class MessageMarkedAsSpam( - val peerId: Int, - val messageId: Int + val peerId: Long, + val cmId: Long ) : LongPollParsedEvent data class MessageMarkedAsNotSpam( @@ -64,8 +72,8 @@ sealed interface LongPollParsedEvent { ) : LongPollParsedEvent data class MessageDeleted( - val peerId: Int, - val messageId: Int, + val peerId: Long, + val cmId: Long, val forAll: Boolean ) : LongPollParsedEvent @@ -74,12 +82,17 @@ sealed interface LongPollParsedEvent { ) : LongPollParsedEvent data class AudioMessageListened( - val peerId: Int, - val messageId: Int + val peerId: Long, + val cmId: Long ) : LongPollParsedEvent data class ChatCleared( - val peerId: Int, - val toMessageId: Int - ): LongPollParsedEvent + val peerId: Long, + val toCmId: Long + ) : LongPollParsedEvent + + data class ChatArchived( + val conversation: VkConversation, + val archived: Boolean + ) : LongPollParsedEvent } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/AttachmentType.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/AttachmentType.kt index 9b4d5124..a4ea83a7 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/AttachmentType.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/AttachmentType.kt @@ -27,7 +27,11 @@ enum class AttachmentType(var value: String) { AUDIO_PLAYLIST("audio_playlist"), PODCAST("podcast"), NARRATIVE("narrative"), - ARTICLE("article"); + ARTICLE("article"), + VIDEO_MESSAGE("video_message"), + GROUP_CHAT_STICKER("ugc_sticker"), + STICKER_PACK_PREVIEW("sticker_pack_preview") + ; fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkArticleData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkArticleData.kt index fc6de91d..aba4164d 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkArticleData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkArticleData.kt @@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkArticleData( - @Json(name = "id") val id: Int + @Json(name = "id") val id: Long ) : VkAttachmentData { fun toDomain(): VkArticleDomain = VkArticleDomain( diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentHistoryMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentHistoryMessageData.kt index f59b9978..27c11a35 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentHistoryMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentHistoryMessageData.kt @@ -1,15 +1,15 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage @JsonClass(generateAdapter = true) data class VkAttachmentHistoryMessageData( - @Json(name = "message_id") val messageId: Int, + @Json(name = "message_id") val messageId: Long, @Json(name = "date") val date: Int, - @Json(name = "cmid") val conversationMessageId: Int, - @Json(name = "from_id") val fromId: Int, + @Json(name = "cmid") val conversationMessageId: Long, + @Json(name = "from_id") val fromId: Long, @Json(name = "position") val position: Int, @Json(name = "attachment") val attachment: VkAttachmentItemData ) { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentItemData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentItemData.kt index 7959d170..47db4f55 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentItemData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAttachmentItemData.kt @@ -1,9 +1,9 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkAttachment -import dev.meloda.fast.model.api.domain.VkUnknownAttachment import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkAttachment +import dev.meloda.fast.model.api.domain.VkUnknownAttachment @JsonClass(generateAdapter = true) data class VkAttachmentItemData( @@ -32,7 +32,10 @@ data class VkAttachmentItemData( @Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?, @Json(name = "podcast") val podcast: VkPodcastData?, @Json(name = "narrative") val narrative: VkNarrativeData?, - @Json(name = "article") val article: VkArticleData? + @Json(name = "article") val article: VkArticleData?, + @Json(name = "video_message") val videoMessage: VkVideoMessageData?, + @Json(name = "ugc_sticker") val groupSticker: VkGroupStickerData?, + @Json(name = "sticker_pack_preview") val stickerPackPreview: VkStickerPackPreviewData? ) { fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) { AttachmentType.UNKNOWN -> VkUnknownAttachment @@ -60,5 +63,8 @@ data class VkAttachmentItemData( AttachmentType.PODCAST -> podcast?.toDomain() AttachmentType.NARRATIVE -> narrative?.toDomain() AttachmentType.ARTICLE -> article?.toDomain() + AttachmentType.VIDEO_MESSAGE -> videoMessage?.toDomain() + AttachmentType.GROUP_CHAT_STICKER -> groupSticker?.toDomain() + AttachmentType.STICKER_PACK_PREVIEW -> stickerPackPreview?.toDomain() } ?: VkUnknownAttachment } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt index 0622b4dd..fba8829e 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioData.kt @@ -1,31 +1,31 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkAudioDomain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkAudioDomain @JsonClass(generateAdapter = true) data class VkAudioData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "title") val title: String, @Json(name = "artist") val artist: String, @Json(name = "duration") val duration: Int, @Json(name = "url") val url: String, @Json(name = "date") val date: Int, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "access_key") val accessKey: String?, @Json(name = "is_explicit") val isExplicit: Boolean, @Json(name = "is_focus_track") val isFocusTrack: Boolean, @Json(name = "is_licensed") val isLicensed: Boolean?, - @Json(name = "genre_id") val genreId: Int?, + @Json(name = "genre_id") val genreId: Long?, @Json(name = "album") val album: Album?, ) : VkAttachmentData { @JsonClass(generateAdapter = true) data class Album( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "title") val title: String, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "access_key") val accessKey: String, @Json(name = "thumb") val thumb: Thumb? ) { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioMessageData.kt index f4645134..793af3d9 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioMessageData.kt @@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkAudioMessageData( - @Json(name = "id") val id: Int, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "id") val id: Long, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "duration") val duration: Int, @Json(name = "waveform") val waveform: List, @Json(name = "link_ogg") val linkOgg: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioPlaylistData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioPlaylistData.kt index 1f42373d..e21122a6 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioPlaylistData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkAudioPlaylistData.kt @@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkAudioPlaylistData( - @Json(name = "id") val id: Int, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "id") val id: Long, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "type") val type: Int, @Json(name = "title") val title: String, @Json(name = "description") val description: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCallData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCallData.kt index 6725a7cd..067067b6 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCallData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCallData.kt @@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkCallData( - @Json(name = "initiator_id") val initiatorId: Int, - @Json(name = "receiver_id") val receiverId: Int, + @Json(name = "initiator_id") val initiatorId: Long, + @Json(name = "receiver_id") val receiverId: Long, @Json(name = "state") val state: String, @Json(name = "time") val time: Int, @Json(name = "duration") val duration: Int, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatData.kt index 46f81ea8..aa2d6020 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatData.kt @@ -8,9 +8,9 @@ import com.squareup.moshi.JsonClass data class VkChatData( @Json(name = "type") val type: String, @Json(name = "val title") val title: String, - @Json(name = "admin_id") val adminId: Int, + @Json(name = "admin_id") val adminId: Long, @Json(name = "members_count") val membersCount: Int, - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "photo_50") val photo50: String, @Json(name = "photo_100") val photo100: String, @Json(name = "photo_200") val photo200: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatMemberData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatMemberData.kt index 8768f809..3323038c 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatMemberData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkChatMemberData.kt @@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkChatMemberData( - @Json(name = "member_id") val memberId: Int, + @Json(name = "member_id") val memberId: Long, @Json(name = "invited_by") val invitedBy: Int, @Json(name = "join_date") val joinDate: Int, @Json(name = "is_admin") val isAdmin: Boolean?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkContactData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkContactData.kt index c20835b0..08d38c7a 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkContactData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkContactData.kt @@ -6,10 +6,10 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkContactData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "name") val name: String, @Json(name = "can_write") val canWrite: Boolean, - @Json(name = "user_id") val userId: Int, + @Json(name = "user_id") val userId: Long, @Json(name = "last_seen_status") val lastSeenStatus: String?, @Json(name = "photo_50") val photo50: String?, @Json(name = "calls_id") val callsId: String diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkConversationData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkConversationData.kt index ee6044c7..07eb6b23 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkConversationData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkConversationData.kt @@ -1,21 +1,21 @@ package dev.meloda.fast.model.api.data +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import dev.meloda.fast.model.api.PeerType import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkMessage -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkConversationData( @Json(name = "peer") val peer: Peer, - @Json(name = "last_message_id") val lastMessageId: Int?, - @Json(name = "in_read") val inRead: Int, - @Json(name = "out_read") val outRead: Int, - @Json(name = "in_read_cmid") val inReadConversationMessageId: Int, - @Json(name = "out_read_cmid") val outReadConversationMessageId: Int, + @Json(name = "last_message_id") val lastMessageId: Long?, + @Json(name = "in_read") val inRead: Long, + @Json(name = "out_read") val outRead: Long, + @Json(name = "in_read_cmid") val inReadConversationMessageId: Long, + @Json(name = "out_read_cmid") val outReadConversationMessageId: Long, @Json(name = "sort_id") val sortId: SortId, - @Json(name = "last_conversation_message_id") val lastConversationMessageId: Int, + @Json(name = "last_conversation_message_id") val lastConversationMessageId: Long, @Json(name = "is_marked_unread") val isMarkedUnread: Boolean, @Json(name = "important") val important: Boolean, @Json(name = "push_settings") val pushSettings: PushSettings?, @@ -25,13 +25,14 @@ data class VkConversationData( @Json(name = "chat_settings") val chatSettings: ChatSettings?, @Json(name = "call_in_progress") val callInProgress: CallInProgress?, @Json(name = "unread_count") val unreadCount: Int?, + @Json(name = "is_archived") val isArchived: Boolean? ) { @JsonClass(generateAdapter = true) data class Peer( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "type") val type: String, - @Json(name = "local_id") val localId: Int, + @Json(name = "local_id") val localId: Long, ) @JsonClass(generateAdapter = true) @@ -55,7 +56,7 @@ data class VkConversationData( @JsonClass(generateAdapter = true) data class ChatSettings( - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "title") val title: String, @Json(name = "state") val state: String, @Json(name = "acl") val acl: Acl, @@ -119,7 +120,7 @@ data class VkConversationData( photo200 = chatSettings?.photo?.photo200, isCallInProgress = callInProgress != null, isPhantom = chatSettings?.isDisappearing == true, - lastConversationMessageId = lastConversationMessageId, + lastCmId = lastConversationMessageId, inRead = inRead, outRead = outRead, lastMessageId = lastMessageId, @@ -140,5 +141,6 @@ data class VkConversationData( pinnedMessage = chatSettings?.pinnedMessage?.mapToDomain(), user = null, group = null, + isArchived = isArchived == true ) } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCuratorData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCuratorData.kt index 397e9cc4..aa5c4298 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCuratorData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkCuratorData.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkCuratorData( - val id: Int, + val id: Long, val name: String, val description: String, val url: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkEventData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkEventData.kt index 3b5c8eca..3a66ce5f 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkEventData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkEventData.kt @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkEventData( @Json(name = "button_text") val buttonText: String, - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "is_favorite") val isFavorite: Boolean, @Json(name = "text") val text: String, @Json(name = "address") val address: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkFileData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkFileData.kt index b956c67a..cbbb1ebc 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkFileData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkFileData.kt @@ -1,13 +1,13 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkFileDomain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkFileDomain @JsonClass(generateAdapter = true) data class VkFileData( - @Json(name = "id") val id: Int, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "id") val id: Long, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "title") val title: String, @Json(name = "size") val size: Int, @Json(name = "ext") val extension: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGiftData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGiftData.kt index 2ac9e6be..36ecabf9 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGiftData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGiftData.kt @@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkGiftData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "thumb_256") val thumb256: String?, @Json(name = "thumb_96") val thumb96: String?, @Json(name = "thumb_48") val thumb48: String diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGraffitiData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGraffitiData.kt index c9750e43..5a8ba254 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGraffitiData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGraffitiData.kt @@ -1,13 +1,13 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkGraffitiDomain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkGraffitiDomain @JsonClass(generateAdapter = true) data class VkGraffitiData( - @Json(name = "id") val id: Int, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "id") val id: Long, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "url") val url: String, @Json(name = "width") val width: Int, @Json(name = "height") val height: Int, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupCallData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupCallData.kt index 7f7ae60e..0e3b7a36 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupCallData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupCallData.kt @@ -1,12 +1,12 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkGroupCallDomain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkGroupCallDomain @JsonClass(generateAdapter = true) data class VkGroupCallData( - @Json(name = "initiator_id") val initiatorId: Int, + @Json(name = "initiator_id") val initiatorId: Long, @Json(name = "join_link") val joinLink: String, @Json(name = "participants") val participants: Participants ) { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupData.kt index f1e8824f..7aa4af31 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupData.kt @@ -7,7 +7,7 @@ import kotlin.math.abs @JsonClass(generateAdapter = true) data class VkGroupData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "name") val name: String, @Json(name = "screen_name") val screenName: String, @Json(name = "is_closed") val isClosed: Int?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupStickerData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupStickerData.kt new file mode 100644 index 00000000..4345dc1a --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkGroupStickerData.kt @@ -0,0 +1,27 @@ +package dev.meloda.fast.model.api.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkGroupStickerDomain + +@JsonClass(generateAdapter = true) +data class VkGroupStickerData( + val id: Long, + val owner_id: Long, + val pack_id: Long?, + val status: String?, + val is_deleted: Boolean?, + val images: List? +): VkAttachmentData { + + @JsonClass(generateAdapter = true) + data class Image( + @Json(name = "width") val width: Int, + @Json(name = "height") val height: Int, + @Json(name = "url") val url: String + ) + + fun toDomain(): VkGroupStickerDomain = VkGroupStickerDomain( + id = id + ) +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt index fdd76b7d..fa393cdc 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt @@ -2,22 +2,23 @@ package dev.meloda.fast.model.api.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.FormatDataType import dev.meloda.fast.model.api.domain.VkMessage @JsonClass(generateAdapter = true) data class VkMessageData( - @Json(name = "id") val id: Int?, - @Json(name = "peer_id") val peerId: Int?, + @Json(name = "id") val id: Long?, + @Json(name = "peer_id") val peerId: Long?, @Json(name = "date") val date: Int, - @Json(name = "from_id") val fromId: Int, + @Json(name = "from_id") val fromId: Long, @Json(name = "out") val out: Int?, @Json(name = "text") val text: String, - @Json(name = "conversation_message_id") val conversationMessageId: Int, + @Json(name = "conversation_message_id") val cmId: Long, @Json(name = "fwd_messages") val fwdMessages: List? = emptyList(), - @Json(name = "important") val important: Boolean = false, - @Json(name = "random_id") val randomId: Int = 0, + @Json(name = "important") val important: Boolean?, + @Json(name = "random_id") val randomId: Long?, @Json(name = "attachments") val attachments: List = emptyList(), - @Json(name = "is_hidden") val isHidden: Boolean = false, + @Json(name = "is_hidden") val isHidden: Boolean?, @Json(name = "payload") val payload: String?, @Json(name = "geo") val geo: Geo?, @Json(name = "action") val action: Action?, @@ -25,7 +26,8 @@ data class VkMessageData( @Json(name = "reply_message") val replyMessage: VkMessageData?, @Json(name = "update_time") val updateTime: Int?, @Json(name = "is_pinned") val isPinned: Boolean?, - @Json(name = "pinned_at") val pinnedAt: Int? + @Json(name = "pinned_at") val pinnedAt: Int?, + @Json(name = "format_data") val formatData: FormatData? ) { @JsonClass(generateAdapter = true) @@ -52,29 +54,58 @@ data class VkMessageData( @JsonClass(generateAdapter = true) data class Action( @Json(name = "type") val type: String, - @Json(name = "member_id") val memberId: Int?, + @Json(name = "member_id") val memberId: Long?, @Json(name = "text") val text: String?, - @Json(name = "conversation_message_id") val conversationMessageId: Int?, + @Json(name = "conversation_message_id") val conversationMessageId: Long?, @Json(name = "message") val message: String? ) + + @JsonClass(generateAdapter = true) + data class FormatData( + @Json(name = "version") val version: String, + @Json(name = "items") val items: List + ) { + + @JsonClass(generateAdapter = true) + data class Item( + @Json(name = "offset") val offset: Int, + @Json(name = "length") val length: Int, + @Json(name = "type") val type: String, + @Json(name = "url") val url: String? + ) + + fun asDomain(): VkMessage.FormatData = VkMessage.FormatData( + version = version, + items = items.mapNotNull { item -> + FormatDataType.parse(item.type)?.let { type -> + VkMessage.FormatData.Item( + offset = item.offset, + length = item.length, + type = type, + url = item.url + ) + } + } + ) + } } fun VkMessageData.asDomain(): VkMessage = VkMessage( id = id ?: -1, - conversationMessageId = conversationMessageId, + cmId = cmId, text = text.ifBlank { null }, isOut = out == 1, peerId = peerId ?: -1, fromId = fromId, date = date, - randomId = randomId, + randomId = randomId ?: 0, action = VkMessage.Action.parse(action?.type), actionMemberId = action?.memberId, actionText = action?.text, actionConversationMessageId = action?.conversationMessageId, actionMessage = action?.message, geoType = geo?.type, - isImportant = important, + isImportant = important ?: false, updateTime = updateTime, forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain), attachments = attachments.map(VkAttachmentItemData::toDomain), @@ -84,5 +115,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage( actionUser = null, actionGroup = null, pinnedAt = pinnedAt, - isPinned = isPinned == true + isPinned = isPinned == true, + formatData = formatData?.asDomain(), + isSpam = false ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMiniAppData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMiniAppData.kt index 0d9fd9d4..b4153c93 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMiniAppData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMiniAppData.kt @@ -16,9 +16,9 @@ data class VkMiniAppData( @JsonClass(generateAdapter = true) data class App( @Json(name = "type") val type: String, - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "title") val title: String, - @Json(name = "author_owner_id") val authorOwnerId: Int, + @Json(name = "author_owner_id") val authorOwnerid: Long, @Json(name = "is_favorite") val isFavorite: Boolean, @Json(name = "share_url") val shareUrl: String, @Json(name = "webview_url") val webViewUrl: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkNarrativeData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkNarrativeData.kt index 1ca7d937..6b3897e5 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkNarrativeData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkNarrativeData.kt @@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkNarrativeData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "title") val title: String? ) : VkAttachmentData { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt index 1a7a0385..719fc8b7 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPhotoData.kt @@ -6,18 +6,18 @@ import dev.meloda.fast.model.api.domain.VkPhotoDomain @JsonClass(generateAdapter = true) data class VkPhotoData( - @Json(name = "album_id") val albumId: Int, + @Json(name = "album_id") val albumId: Long, @Json(name = "date") val date: Int?, - @Json(name = "id") val id: Int, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "id") val id: Long, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "has_tags") val hasTags: Boolean?, @Json(name = "access_key") val accessKey: String?, @Json(name = "sizes") val sizes: List, @Json(name = "text") val text: String?, - @Json(name = "user_id") val userId: Int?, + @Json(name = "user_id") val userId: Long?, @Json(name = "lat") val lat: Double?, @Json(name = "long") val long: Double?, - @Json(name = "post_id") val postId: Int? + @Json(name = "post_id") val postId: Long? ) : VkAttachmentData { @JsonClass(generateAdapter = true) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt index 2bf15c8d..82886752 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt @@ -1,21 +1,21 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkMessage import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkMessage @JsonClass(generateAdapter = true) data class VkPinnedMessageData( - @Json(name = "id") val id: Int?, - @Json(name = "peer_id") val peerId: Int?, + @Json(name = "id") val id: Long?, + @Json(name = "peer_id") val peerId: Long?, @Json(name = "date") val date: Int, - @Json(name = "from_id") val fromId: Int, + @Json(name = "from_id") val fromId: Long, @Json(name = "out") val out: Boolean?, @Json(name = "text") val text: String, - @Json(name = "conversation_message_id") val conversationMessageId: Int, + @Json(name = "conversation_message_id") val conversationMessageId: Long, @Json(name = "fwd_messages") val forwards: List?, @Json(name = "important") val important: Boolean = false, - @Json(name = "random_id") val randomId: Int = 0, + @Json(name = "random_id") val randomId: Long = 0, @Json(name = "attachments") val attachments: List?, @Json(name = "is_hidden") val isHidden: Boolean = false, @Json(name = "payload") val payload: String?, @@ -28,7 +28,7 @@ data class VkPinnedMessageData( fun mapToDomain(): VkMessage = VkMessage( id = id ?: -1, - conversationMessageId = conversationMessageId, + cmId = conversationMessageId, text = text.ifBlank { null }, isOut = out == true, peerId = peerId ?: -1, @@ -54,5 +54,7 @@ data class VkPinnedMessageData( actionGroup = null, pinnedAt = null, isPinned = true, + isSpam = false, + formatData = null, ) } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPodcastData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPodcastData.kt index c3410f56..2d18c7bc 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPodcastData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPodcastData.kt @@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkPodcastData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "title") val title: String, @Json(name = "artist") val artist: String, // ... other fields diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPollData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPollData.kt index f2542a5c..d2958827 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPollData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPollData.kt @@ -1,13 +1,13 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkPollDomain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkPollDomain @JsonClass(generateAdapter = true) data class VkPollData( val multiple: Boolean, - val id: Int, + val id: Long, val votes: Int, val anonymous: Boolean?, val closed: Boolean, @@ -18,24 +18,24 @@ data class VkPollData( @Json(name = "can_report") val canReport: Boolean, @Json(name = "can_share") val canShare: Boolean, val created: Int, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "owner_id") val ownerId: Long, val question: String, @Json(name = "disable_unvote") val disableUnvote: Boolean, val friends: List?, @Json(name = "embed_hash") val embedHash: String, val answers: List, - @Json(name = "author_id") val authorId: Int?, + @Json(name = "author_id") val authorId: Long?, val background: Background? ) { @JsonClass(generateAdapter = true) data class Friend( - val id: Int + val id: Long ) @JsonClass(generateAdapter = true) data class Answer( - val id: Int, + val id: Long, val rate: Double, val text: String, val votes: Int @@ -45,7 +45,7 @@ data class VkPollData( data class Background( val angle: Int, val color: String, - val id: Int, + val id: Long, val name: String, val type: String, val points: List diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStickerData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStickerData.kt index 5282f16a..4f0ff2f2 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStickerData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStickerData.kt @@ -1,15 +1,15 @@ package dev.meloda.fast.model.api.data -import dev.meloda.fast.model.api.domain.VkStickerDomain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkStickerDomain @JsonClass(generateAdapter = true) data class VkStickerData( - @Json(name = "product_id") val productId: Int, - @Json(name = "sticker_id") val stickerId: Int, - @Json(name = "images") val images: List, - @Json(name = "images_with_background") val imagesWithBackground: List, + @Json(name = "product_id") val productId: Long, + @Json(name = "sticker_id") val stickerId: Long, + @Json(name = "images") val images: List?, + @Json(name = "images_with_background") val imagesWithBackground: List?, @Json(name = "animation_url") val animationUrl: String?, @Json(name = "animations") val animations: List? ) { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStickerPackPreviewData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStickerPackPreviewData.kt new file mode 100644 index 00000000..649e77d7 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStickerPackPreviewData.kt @@ -0,0 +1,33 @@ +package dev.meloda.fast.model.api.data + +import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkStickerPackPreviewDomain + +@JsonClass(generateAdapter = true) +data class VkStickerPackPreviewData( + val id: Long, + val title: String, + val description: String?, + val author: String?, + val icon: Icon?, + val price: Price?, + val can_purchase: Boolean, + val url: String +) : VkAttachmentData { + + @JsonClass(generateAdapter = true) + data class Icon( + val base_url: String + ) + + @JsonClass(generateAdapter = true) + data class Price( + val current: Long, + val regular: Long + ) + + fun toDomain(): VkStickerPackPreviewDomain = VkStickerPackPreviewDomain( + id = id + ) +} + diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStoryData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStoryData.kt index 2341317a..6389bb06 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStoryData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkStoryData.kt @@ -5,8 +5,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkStoryData( - val id: Int, - val owner_id: Int, + val id: Long, + val owner_id: Long, val access_key: String?, val can_comment: Int?, val can_reply: Int?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkUserData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkUserData.kt index ef5a890e..d59b4439 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkUserData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkUserData.kt @@ -7,7 +7,7 @@ import dev.meloda.fast.model.api.domain.VkUser @JsonClass(generateAdapter = true) data class VkUserData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "first_name") val firstName: String, @Json(name = "last_name") val lastName: String, @Json(name = "can_access_closed") val canAccessClosed: Boolean, @@ -18,8 +18,8 @@ data class VkUserData( @Json(name = "photo_100") val photo100: String?, @Json(name = "photo_200") val photo200: String?, @Json(name = "photo_400_orig") val photo400Orig: String?, - @Json(name = "online") val online: Int?, @Json(name = "online_info") val onlineInfo: OnlineInfo?, + @Json(name = "last_seen") val lastSeen: LastSeen?, @Json(name = "screen_name") val screenName: String, @Json(name = "bdate") val birthday: String? //...other fields @@ -31,25 +31,26 @@ data class VkUserData( @Json(name = "status") val status: String?, @Json(name = "last_seen") val lastSeen: Int?, @Json(name = "is_online") val isOnline: Boolean?, - @Json(name = "online_mobile") val onlineMobile: Boolean?, - @Json(name = "app_id") val appId: Int? + @Json(name = "online_mobile") val isOnlineMobile: Boolean?, + @Json(name = "app_id") val appId: Long? + ) + + @JsonClass(generateAdapter = true) + data class LastSeen( + @Json(name = "platform") val platform: Int, + @Json(name = "time") val time: Int ) fun mapToDomain() = VkUser( id = id, firstName = firstName, lastName = lastName, - // TODO: 05/05/2024, Danil Nikolaev: improve - onlineStatus = when { - online != 1 -> OnlineStatus.Offline - onlineInfo?.onlineMobile == true -> { - OnlineStatus.OnlineMobile(appId = onlineInfo.appId) - } - - else -> { - OnlineStatus.Online(appId = onlineInfo?.appId) - } - }, + onlineStatus = parseUserOnlineState( + isOnline = onlineInfo?.isOnline, + isOnlineMobile = onlineInfo?.isOnlineMobile, + status = onlineInfo?.status, + appId = onlineInfo?.appId + ), photo50 = photo50, photo100 = photo100, photo200 = photo200, @@ -59,3 +60,26 @@ data class VkUserData( birthday = birthday ) } + +fun parseUserOnlineState( + isOnline: Boolean?, + isOnlineMobile: Boolean?, + status: String?, + appId: Long? +): OnlineStatus { + return when { + isOnlineMobile == true -> OnlineStatus.OnlineMobile(appId) + isOnline == true -> OnlineStatus.Online(appId) + + status != null -> { + when (status) { + "last_week" -> OnlineStatus.LastWeek + "last_month" -> OnlineStatus.LastMonth + + else -> OnlineStatus.Recently + } + } + + else -> OnlineStatus.Offline + } +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkVideoData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkVideoData.kt index 1053fda2..3774ff1b 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkVideoData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkVideoData.kt @@ -6,7 +6,7 @@ import dev.meloda.fast.model.api.domain.VkVideoDomain @JsonClass(generateAdapter = true) data class VkVideoData( - @Json(name = "id") val id: Int, + @Json(name = "id") val id: Long, @Json(name = "title") val title: String, @Json(name = "width") val width: Int?, @Json(name = "height") val height: Int?, @@ -19,7 +19,7 @@ data class VkVideoData( @Json(name = "type") val type: String, @Json(name = "views") val views: Int, @Json(name = "access_key") val accessKey: String?, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "owner_id") val ownerId: Long, @Json(name = "is_favorite") val isFavorite: Boolean?, @Json(name = "image") val image: List?, @Json(name = "first_frame") val firstFrame: List?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkVideoMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkVideoMessageData.kt new file mode 100644 index 00000000..3432eb23 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkVideoMessageData.kt @@ -0,0 +1,78 @@ +package dev.meloda.fast.model.api.data + +import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.domain.VkVideoMessageDomain + +@JsonClass(generateAdapter = true) +data class VkVideoMessageData( + val id: Long, + val access_key: String?, + val can_add: Int?, + val can_dislike: Int?, + val can_download: Int?, + val can_play_in_background: Int?, + val date: Int?, + val description: String?, + val duration: Int?, + val files: Files?, + val first_frame: List?, + val height: Int?, + val image: List?, + val is_author: Boolean?, + val is_favorite: Boolean?, + val is_from_message: Int?, + val need_mediascope_stat: Boolean?, + val ov_id: String?, + val owner_id: Long?, + val player: String?, + val processing: Int?, + val repeat: Int?, + val response_type: String?, + val shape_id: Long?, + val timeline_thumbs: TimelineThumbs?, + val title: String?, + val track_code: String?, + val transcript_state: String?, + val type: String?, + val views: Int?, + val width: Int?, +) : VkAttachmentData { + + @JsonClass(generateAdapter = true) + data class Files( + val failover_host: String?, + val mp4_240: String?, + val mp4_480: String?, + ) + + @JsonClass(generateAdapter = true) + data class FirstFrame( + val height: Int?, + val url: String?, + val width: Int?, + ) + + @JsonClass(generateAdapter = true) + data class Image( + val height: Int?, + val url: String?, + val width: Int?, + val with_padding: Int?, + ) + + @JsonClass(generateAdapter = true) + data class TimelineThumbs( + val count_per_image: Int?, + val count_per_row: Int?, + val count_total: Int?, + val frame_height: Int?, + val frame_width: Double?, + val frequency: Int?, + val is_uv: Boolean?, + val links: List?, + ) + + fun toDomain(): VkVideoMessageDomain = VkVideoMessageDomain( + id = id + ) +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallData.kt index d3c77fae..caab17e3 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallData.kt @@ -6,9 +6,9 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkWallData( - @Json(name = "id") val id: Int, - @Json(name = "from_id") val from_id: Int, - @Json(name = "to_id") val to_id: Int, + @Json(name = "id") val id: Long, + @Json(name = "from_id") val from_id: Long, + @Json(name = "to_id") val to_id: Long, @Json(name = "date") val date: Int, @Json(name = "text") val text: String, @Json(name = "attachments") val attachments: List?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallReplyData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallReplyData.kt index 8906e618..bb7ecee4 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallReplyData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWallReplyData.kt @@ -5,12 +5,12 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkWallReplyData( - val id: Int, - val from_id: Int, + val id: Long, + val from_id: Long, val date: Int, val text: String, - val post_id: Int, - val owner_id: Int, + val post_id: Long, + val owner_id: Long, val parents_stack: List, val likes: Likes, val reply_to_user: Int?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWidgetData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWidgetData.kt index 07dfe16a..f4cb5ab6 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWidgetData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkWidgetData.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkWidgetData( - val id: Int + val id: Long ) : VkAttachmentData { fun toDomain() = VkWidgetDomain(id) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/FormatDataType.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/FormatDataType.kt new file mode 100644 index 00000000..553614d6 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/FormatDataType.kt @@ -0,0 +1,10 @@ +package dev.meloda.fast.model.api.domain + +enum class FormatDataType { + BOLD, ITALIC, UNDERLINE, URL; + + companion object { + fun parse(value: String): FormatDataType? = + entries.firstOrNull { it.name.lowercase() == value } + } +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkArticleDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkArticleDomain.kt index 55301fa4..f6163a89 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkArticleDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkArticleDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkArticleDomain( - val id: Int + val id: Long ) : VkAttachment { override val type: AttachmentType = AttachmentType.ARTICLE diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAttachmentHistoryMessage.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAttachmentHistoryMessage.kt index 15c62990..a4617240 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAttachmentHistoryMessage.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAttachmentHistoryMessage.kt @@ -1,10 +1,10 @@ package dev.meloda.fast.model.api.domain data class VkAttachmentHistoryMessage( - val messageId: Int, - val conversationMessageId: Int, + val messageId: Long, + val conversationMessageId: Long, val date: Int, - val fromId: Int, + val fromId: Long, val position: Int, val attachment: VkAttachment ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioDomain.kt index 9e11d8ef..e34a3b7b 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioDomain.kt @@ -3,8 +3,8 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkAudioDomain( - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val title: String, val artist: String, val url: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioMessageDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioMessageDomain.kt index 3b3fa0f8..49cbf530 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioMessageDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioMessageDomain.kt @@ -3,8 +3,8 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkAudioMessageDomain( - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val duration: Int, val waveform: List, val linkOgg: String, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioPlaylistDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioPlaylistDomain.kt index 39a6b039..06c53952 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioPlaylistDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkAudioPlaylistDomain.kt @@ -3,8 +3,8 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkAudioPlaylistDomain( - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val title: String, val description: String, ) : VkAttachment { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCallDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCallDomain.kt index efced050..9c44216f 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCallDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCallDomain.kt @@ -3,8 +3,8 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkCallDomain( - val initiatorId: Int, - val receiverId: Int, + val initiatorId: Long, + val receiverId: Long, val state: String, val time: Int, val duration: Int, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatDomain.kt index a12860a2..aa062b2c 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatDomain.kt @@ -3,9 +3,9 @@ package dev.meloda.fast.model.api.domain data class VkChatDomain( val type: String, val title: String, - val adminId: Int, + val adminId: Long, val membersCount: Int, - val id: Int, + val id: Long, val members: List = emptyList(), val photo50: String, val photo100: String, @@ -13,7 +13,7 @@ data class VkChatDomain( val isDefaultPhoto: Boolean ) { data class ChatMember( - val id: Int, + val id: Long, val type: ChatMemberType, val isOnline: Boolean?, val lastSeen: Int?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatMemberDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatMemberDomain.kt index 7c045442..fbe71005 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatMemberDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkChatMemberDomain.kt @@ -1,7 +1,7 @@ package dev.meloda.fast.model.api.domain data class VkChatMemberDomain( - val memberId: Int, + val memberId: Long, val invitedBy: Int, val joinDate: Int, val isAdmin: Boolean, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkContactDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkContactDomain.kt index 37cea639..4fb26f00 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkContactDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkContactDomain.kt @@ -2,5 +2,5 @@ package dev.meloda.fast.model.api.domain data class VkContactDomain( val name: String, - val userId: Int + val userId: Long ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt index c54a6ad7..9b7fa495 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt @@ -4,31 +4,33 @@ import dev.meloda.fast.model.api.PeerType import dev.meloda.fast.model.database.VkConversationEntity data class VkConversation( - val id: Int, - val localId: Int, - val ownerId: Int?, + val id: Long, + val localId: Long, + val ownerId: Long?, val title: String?, val photo50: String?, val photo100: String?, val photo200: String?, val isCallInProgress: Boolean, val isPhantom: Boolean, - val lastConversationMessageId: Int, - val inReadCmId: Int, - val outReadCmId: Int, - val inRead: Int, - val outRead: Int, - val lastMessageId: Int?, + val lastCmId: Long, + val inReadCmId: Long, + val outReadCmId: Long, + val inRead: Long, + val outRead: Long, + val lastMessageId: Long?, val unreadCount: Int, val membersCount: Int?, val canChangePin: Boolean, val canChangeInfo: Boolean, val majorId: Int, val minorId: Int, - val pinnedMessageId: Int?, + val pinnedMessageId: Long?, val interactionType: Int, - val interactionIds: List, + val interactionIds: List, val peerType: PeerType, + val isArchived: Boolean, + val lastMessage: VkMessage?, val pinnedMessage: VkMessage?, val user: VkUser?, @@ -36,8 +38,20 @@ data class VkConversation( ) { fun isPinned(): Boolean = majorId > 0 - fun isInUnread() = inRead - (lastMessageId ?: 0) < 0 - fun isOutUnread() = outRead - (lastMessageId ?: 0) < 0 + + fun isInRead(cmId: Long? = null) = inReadCmId - (cmId ?: lastCmId) >= 0 + + fun isOutRead(cmId: Long? = null) = outReadCmId - (cmId ?: lastCmId) >= 0 + + fun isRead(lastMessage: VkMessage? = null): Boolean { + val message = lastMessage ?: this.lastMessage + + return when { + message == null -> true + message.isOut -> isOutRead(message.cmId) + else -> isInRead(message.cmId) + } + } companion object { val EMPTY: VkConversation = VkConversation( @@ -50,7 +64,7 @@ data class VkConversation( photo200 = null, isCallInProgress = false, isPhantom = false, - lastConversationMessageId = -1, + lastCmId = -1, inReadCmId = -1, outReadCmId = -1, inRead = -1, @@ -66,11 +80,12 @@ data class VkConversation( interactionType = -1, interactionIds = emptyList(), peerType = PeerType.USER, + isArchived = false, + lastMessage = null, pinnedMessage = null, user = null, group = null - ) } } @@ -84,7 +99,7 @@ fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity( photo100 = photo100, photo200 = photo200, isPhantom = isPhantom, - lastConversationMessageId = lastConversationMessageId, + lastConversationMessageId = lastCmId, inReadCmId = inReadCmId, outReadCmId = outReadCmId, inRead = inRead, @@ -98,4 +113,5 @@ fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity( minorId = minorId, pinnedMessageId = pinnedMessageId, peerType = peerType.value, + isArchived = isArchived ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCuratorDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCuratorDomain.kt index f94af5b0..82944f8f 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCuratorDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkCuratorDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkCuratorDomain( - val id: Int, + val id: Long, ) : VkAttachment { override val type: AttachmentType = AttachmentType.CURATOR diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkEventDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkEventDomain.kt index 24c8cefc..c2b678a0 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkEventDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkEventDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkEventDomain( - val id: Int + val id: Long ) : VkAttachment { override val type: AttachmentType = AttachmentType.EVENT diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkFileDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkFileDomain.kt index e9e17829..479e06c9 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkFileDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkFileDomain.kt @@ -1,13 +1,13 @@ package dev.meloda.fast.model.api.domain +import com.squareup.moshi.JsonClass import dev.meloda.fast.model.api.data.AttachmentType import dev.meloda.fast.model.api.data.VkFileData -import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkFileDomain( - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val title: String, val ext: String, val size: Int, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGiftDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGiftDomain.kt index cca67bb6..fa2c8680 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGiftDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGiftDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkGiftDomain( - val id: Int, + val id: Long, val thumb256: String?, val thumb96: String?, val thumb48: String diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGraffitiDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGraffitiDomain.kt index 95a60baf..821292e3 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGraffitiDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGraffitiDomain.kt @@ -3,8 +3,8 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkGraffitiDomain( - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val url: String, val width: Int, val height: Int, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupCallDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupCallDomain.kt index cb7b097b..300d00ac 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupCallDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupCallDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkGroupCallDomain( - val initiatorId: Int + val initiatorId: Long ) : VkAttachment { override val type: AttachmentType = AttachmentType.GROUP_CALL_IN_PROGRESS diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupDomain.kt index f40e264f..397c28af 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.database.VkGroupEntity data class VkGroupDomain( - val id: Int, + val id: Long, val name: String, val screenName: String, val photo50: String?, @@ -13,14 +13,14 @@ data class VkGroupDomain( ) { override fun toString() = name.trim() - - fun mapToDB(): VkGroupEntity = VkGroupEntity( - id = id, - name = name, - screenName = screenName, - photo50 = photo50, - photo100 = photo100, - photo200 = photo200, - membersCount = membersCount - ) } + +fun VkGroupDomain.asEntity(): VkGroupEntity = VkGroupEntity( + id = id, + name = name, + screenName = screenName, + photo50 = photo50, + photo100 = photo100, + photo200 = photo200, + membersCount = membersCount +) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupStickerDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupStickerDomain.kt new file mode 100644 index 00000000..4b0f98c9 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkGroupStickerDomain.kt @@ -0,0 +1,10 @@ +package dev.meloda.fast.model.api.domain + +import dev.meloda.fast.model.api.data.AttachmentType + +data class VkGroupStickerDomain( + val id: Long +) : VkAttachment { + + override val type: AttachmentType = AttachmentType.GROUP_CHAT_STICKER +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt index a1d46800..c86f0f46 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt @@ -5,30 +5,32 @@ import dev.meloda.fast.model.database.VkMessageEntity @Immutable data class VkMessage( - val id: Int, - val conversationMessageId: Int, + val id: Long, + val cmId: Long, val text: String?, val isOut: Boolean, - val peerId: Int, - val fromId: Int, + val peerId: Long, + val fromId: Long, val date: Int, - val randomId: Int, + val randomId: Long, val action: Action?, - val actionMemberId: Int?, + val actionMemberId: Long?, val actionText: String?, - val actionConversationMessageId: Int?, + val actionConversationMessageId: Long?, val actionMessage: String?, val updateTime: Int?, val pinnedAt: Int?, val isPinned: Boolean, - val isImportant: Boolean = false, - val isSpam: Boolean = false, + val isImportant: Boolean, + val isSpam: Boolean, val forwards: List?, val attachments: List?, val replyMessage: VkMessage?, + val formatData: FormatData?, + val geoType: String?, val user: VkUser?, val group: VkGroupDomain?, @@ -44,8 +46,7 @@ data class VkMessage( fun isRead(conversation: VkConversation): Boolean = when { id <= 0 -> false - isOut -> conversation.outRead - id >= 0 - else -> conversation.inRead - id >= 0 + else -> conversation.isRead(this) } fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty() @@ -80,11 +81,24 @@ data class VkMessage( fun parse(value: String?): Action? = entries.firstOrNull { it.value == value } } } + + data class FormatData( + val version: String, + val items: List + ) { + + data class Item( + val offset: Int, + val length: Int, + val type: FormatDataType, + val url: String? + ) + } } fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity( id = id, - conversationMessageId = conversationMessageId, + conversationMessageId = cmId, text = text, isOut = isOut, peerId = peerId, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkNarrativeDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkNarrativeDomain.kt index 26aac65b..895d7fb9 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkNarrativeDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkNarrativeDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkNarrativeDomain( - val id: Int, + val id: Long, val title: String? ) : VkAttachment { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt index 71ad3f5f..72f1da13 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPhotoDomain.kt @@ -7,15 +7,15 @@ import java.util.Stack // TODO: 11/04/2024, Danil Nikolaev: review data class VkPhotoDomain( - val albumId: Int, + val albumId: Long, val date: Int?, - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val hasTags: Boolean, val accessKey: String?, val sizes: List, val text: String?, - val userId: Int? + val userId: Long? ) : VkAttachment { override val type: AttachmentType = AttachmentType.PHOTO diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPodcastDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPodcastDomain.kt index fdb323eb..c2ac2202 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPodcastDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPodcastDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkPodcastDomain( - val id: Int, + val id: Long, val title: String, val artist: String ) : VkAttachment { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPollDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPollDomain.kt index dbdb5cc0..bbc3ad77 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPollDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkPollDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkPollDomain( - val id: Int + val id: Long ) : VkAttachment { override val type: AttachmentType = AttachmentType.POLL diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt index 57ff0d78..afccb0ee 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt @@ -4,10 +4,10 @@ import dev.meloda.fast.model.api.data.AttachmentType import dev.meloda.fast.model.api.data.VkStickerData data class VkStickerDomain( - val id: Int, - val productId: Int, - val images: List, - val backgroundImages: List + val id: Long, + val productId: Long, + val images: List?, + val backgroundImages: List? ) : VkAttachment { override val type: AttachmentType = AttachmentType.STICKER @@ -15,7 +15,7 @@ data class VkStickerDomain( val className: String = this::class.java.name fun urlForSize(size: Int): String? { - for (image in images) { + for (image in images.orEmpty()) { if (image.width == size) return image.url } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerPackPreviewDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerPackPreviewDomain.kt new file mode 100644 index 00000000..8c747e69 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerPackPreviewDomain.kt @@ -0,0 +1,10 @@ +package dev.meloda.fast.model.api.domain + +import dev.meloda.fast.model.api.data.AttachmentType + +data class VkStickerPackPreviewDomain( + val id: Long +): VkAttachment { + + override val type: AttachmentType = AttachmentType.STICKER_PACK_PREVIEW +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStoryDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStoryDomain.kt index 7b1ce7c0..1160d181 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStoryDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStoryDomain.kt @@ -3,8 +3,8 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkStoryDomain( - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val date: Int, val photo: VkPhotoDomain? ) : VkAttachment { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkUser.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkUser.kt index b7d8a876..6e4a9af3 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkUser.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkUser.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.database.VkUserEntity data class VkUser( - val id: Int, + val id: Long, val firstName: String, val lastName: String, val onlineStatus: OnlineStatus, @@ -20,9 +20,12 @@ data class VkUser( val fullName get() = "$firstName $lastName".trim() } -sealed class OnlineStatus(open val appId: Int?) { - data class Online(override val appId: Int?) : OnlineStatus(appId) - data class OnlineMobile(override val appId: Int?) : OnlineStatus(appId) +sealed class OnlineStatus(open val appId: Long?) { + data class Online(override val appId: Long? = null) : OnlineStatus(appId) + data class OnlineMobile(override val appId: Long? = null) : OnlineStatus(appId) + data object Recently : OnlineStatus(null) + data object LastWeek : OnlineStatus(null) + data object LastMonth : OnlineStatus(null) data object Offline : OnlineStatus(null) fun isOnline(): Boolean = this is Online || this is OnlineMobile diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkVideoDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkVideoDomain.kt index 4fdd8f37..30a44756 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkVideoDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkVideoDomain.kt @@ -1,13 +1,13 @@ package dev.meloda.fast.model.api.domain +import com.squareup.moshi.JsonClass import dev.meloda.fast.model.api.data.AttachmentType import dev.meloda.fast.model.api.data.VkVideoData -import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VkVideoDomain( - val id: Int, - val ownerId: Int, + val id: Long, + val ownerId: Long, val images: List, val firstFrames: List?, val accessKey: String?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkVideoMessageDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkVideoMessageDomain.kt new file mode 100644 index 00000000..a9a8dcba --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkVideoMessageDomain.kt @@ -0,0 +1,10 @@ +package dev.meloda.fast.model.api.domain + +import dev.meloda.fast.model.api.data.AttachmentType + +data class VkVideoMessageDomain( + val id: Long +) : VkAttachment { + + override val type: AttachmentType = AttachmentType.VIDEO_MESSAGE +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallDomain.kt index c2fdb0fd..a156e744 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallDomain.kt @@ -4,9 +4,9 @@ import dev.meloda.fast.model.api.data.AttachmentType import dev.meloda.fast.model.api.data.VkAttachmentItemData data class VkWallDomain( - val id: Int, - val fromId: Int, - val toId: Int, + val id: Long, + val fromId: Long, + val toId: Long, val date: Int, val text: String, val attachments: List?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallReplyDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallReplyDomain.kt index 8ca0b72c..26238656 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallReplyDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWallReplyDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkWallReplyDomain( - val id: Int + val id: Long ) : VkAttachment { override val type: AttachmentType = AttachmentType.WALL_REPLY diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWidgetDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWidgetDomain.kt index 8e3c459a..8543c4f9 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWidgetDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkWidgetDomain.kt @@ -3,7 +3,7 @@ package dev.meloda.fast.model.api.domain import dev.meloda.fast.model.api.data.AttachmentType data class VkWidgetDomain( - val id: Int + val id: Long ) : VkAttachment { override val type: AttachmentType = AttachmentType.WIDGET diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/ConversationsRequest.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/ConversationsRequest.kt index 62a91322..adeee04c 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/ConversationsRequest.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/ConversationsRequest.kt @@ -1,18 +1,20 @@ package dev.meloda.fast.model.api.requests +import dev.meloda.fast.model.ConversationsFilter + data class ConversationsGetRequest( val count: Int? = null, val offset: Int? = null, val fields: String = "", - val filter: String = "all", + val filter: ConversationsFilter = ConversationsFilter.ALL, val extended: Boolean? = true, - val startMessageId: Int? = null + val startMessageId: Long? = null ) { val map get() = mutableMapOf( "fields" to fields, - "filter" to filter + "filter" to filter.toString().lowercase() ).apply { count?.let { this["count"] = it.toString() } offset?.let { this["offset"] = it.toString() } @@ -20,15 +22,3 @@ data class ConversationsGetRequest( startMessageId?.let { this["start_message_id"] = it.toString() } } } - -data class ConversationsDeleteRequest(val peerId: Int) { - val map get() = mapOf("peer_id" to peerId.toString()) -} - -data class ConversationsPinRequest(val peerId: Int) { - val map get() = mapOf("peer_id" to peerId.toString()) -} - -data class ConversationsUnpinRequest(val peerId: Int) { - val map get() = mapOf("peer_id" to peerId.toString()) -} 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 d1ad9855..cb29fa8d 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 @@ -6,9 +6,9 @@ import dev.meloda.fast.model.api.domain.VkAttachment data class MessagesGetHistoryRequest( val count: Int? = null, val offset: Int? = null, - val peerId: Int, + val peerId: Long, val extended: Boolean? = null, - val startMessageId: Int? = null, + val startMessageId: Long? = null, val rev: Boolean? = null, val fields: String? = null, ) { @@ -28,13 +28,13 @@ data class MessagesGetHistoryRequest( } data class MessagesSendRequest( - val peerId: Int, - val randomId: Int = 0, + val peerId: Long, + val randomId: Long = 0, val message: String?, val lat: Int? = null, val lon: Int? = null, - val replyTo: Int? = null, - val stickerId: Int? = null, + val replyTo: Long? = null, + val stickerId: Long? = null, val disableMentions: Boolean? = null, val doNotParseLinks: Boolean? = null, val silent: Boolean? = null, @@ -65,8 +65,8 @@ data class MessagesSendRequest( } data class MessagesMarkAsReadRequest( - val peerId: Int, - val startMessageId: Int? + val peerId: Long, + val startMessageId: Long? ) { val map: Map @@ -78,7 +78,7 @@ data class MessagesMarkAsReadRequest( } data class MessagesMarkAsImportantRequest( - val messagesIds: List, + val messagesIds: List, val important: Boolean ) { @@ -104,9 +104,9 @@ data class MessagesGetLongPollServerRequest( data class MessagesPinMessageRequest( - val peerId: Int, - val messageId: Int? = null, - val conversationMessageId: Int? = null + val peerId: Long, + val messageId: Long? = null, + val conversationMessageId: Long? = null ) { val map: Map @@ -119,15 +119,15 @@ data class MessagesPinMessageRequest( } -data class MessagesUnpinMessageRequest(val peerId: Int) { +data class MessagesUnpinMessageRequest(val peerId: Long) { val map: Map get() = mapOf("peer_id" to peerId.toString()) } data class MessagesDeleteRequest( - val peerId: Int, - val messagesIds: List? = null, - val conversationsMessagesIds: List? = null, + val peerId: Long, + val messagesIds: List? = null, + val conversationsMessagesIds: List? = null, val isSpam: Boolean? = null, val deleteForAll: Boolean? = null ) { @@ -147,25 +147,27 @@ data class MessagesDeleteRequest( } data class MessagesEditRequest( - val peerId: Int, - val messageId: Int, - val message: String? = null, - val lat: Float? = null, - val long: Float? = null, - val attachments: List? = null, - val notParseLinks: Boolean = false, - val keepSnippets: Boolean = true, - val keepForwardedMessages: Boolean = true + val peerId: Long, + val cmId: Long?, + val messageId: Long?, + val message: String?, + val lat: Float?, + val long: Float?, + val attachments: List?, + val notParseLinks: Boolean, + val keepSnippets: Boolean, + val keepForwardedMessages: Boolean ) { val map: Map get() = mutableMapOf( "peer_id" to peerId.toString(), - "message_id" to messageId.toString(), "dont_parse_links" to notParseLinks.asInt().toString(), "keep_snippets" to keepSnippets.asInt().toString(), "keep_forward_messages" to keepForwardedMessages.asInt().toString() ).apply { + messageId?.let { this["message_id"] = it.toString() } + cmId?.let { this["cmid"] = it.toString() } message?.let { this["message"] = it } lat?.let { this["lat"] = it.toString() } long?.let { this["long"] = it.toString() } @@ -183,15 +185,20 @@ data class MessagesEditRequest( data class MessagesGetByIdRequest( - val messagesIds: List, + val peerCmIds: List?, + val peerId: Long?, + val messagesIds: List?, + val cmIds: List?, val extended: Boolean? = null, val fields: String? = null ) { val map: Map - get() = mutableMapOf( - "message_ids" to messagesIds.joinToString(), - ).apply { + get() = mutableMapOf().apply { + peerCmIds?.let { this["peer_cmids"] = it.joinToString() } + peerId?.let { this["peer_id"] = it.toString() } + messagesIds?.let { this["message_ids"] = it.joinToString() } + cmIds?.let { this["cmids"] = it.joinToString() } extended?.let { this["extended"] = it.asInt().toString() } fields?.let { this["fields"] = it } } @@ -199,7 +206,7 @@ data class MessagesGetByIdRequest( data class MessagesGetChatRequest( - val chatId: Int, + val chatId: Long, val fields: String? = null ) { @@ -213,7 +220,7 @@ data class MessagesGetChatRequest( data class MessagesGetConversationMembersRequest( - val peerId: Int, + val peerId: Long, val offset: Int? = null, val count: Int? = null, val extended: Boolean? = null, @@ -234,8 +241,8 @@ data class MessagesGetConversationMembersRequest( data class MessagesRemoveChatUserRequest( - val chatId: Int, - val memberId: Int + val chatId: Long, + val memberId: Long ) { val map: Map get() = mapOf( @@ -245,13 +252,13 @@ data class MessagesRemoveChatUserRequest( } data class MessagesGetHistoryAttachmentsRequest( - val peerId: Int, + val peerId: Long, val extended: Boolean?, val count: Int?, val offset: Int?, val preserveOrder: Boolean?, val attachmentTypes: List, - val conversationMessageId: Int, + val conversationMessageId: Long, val fields: String? ) { @@ -269,7 +276,7 @@ data class MessagesGetHistoryAttachmentsRequest( } data class MessagesCreateChatRequest( - val userIds: List?, + val userIds: List?, val title: String? ) { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/OAuthRequest.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/OAuthRequest.kt index c90bacee..e388db4e 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/OAuthRequest.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/OAuthRequest.kt @@ -59,3 +59,33 @@ data class AuthWithAppRequest( "sdk_fingerprint" to sdkFingerprint ) } + +data class GetAnonymTokenRequest( + val clientId: String, + val clientSecret: String +) { + + val map + get() = mapOf( + "client_id" to clientId, + "client_secret" to clientSecret + ) +} + +data class ExchangeSilentTokenRequest( + val anonymToken: String, + val silentToken: String, + val silentUuid: String +) { + + val map + get() = mapOf( + "access_token" to anonymToken, + "token" to silentToken, + "uuid" to silentUuid + ) +} + +data class GetExchangeTokenRequest(val accessToken: String) { + val map get() = mapOf("access_token" to accessToken) +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/UsersRequest.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/UsersRequest.kt index 1ae5ccad..b738fd9e 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/UsersRequest.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/UsersRequest.kt @@ -1,7 +1,7 @@ package dev.meloda.fast.model.api.requests data class UsersGetRequest( - val userIds: List? = null, + val userIds: List? = null, val fields: String? = null, val nomCase: String? = null ) { diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt index 927101a5..70c0fb93 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt @@ -35,5 +35,5 @@ data class ConversationsResponseItem( @JsonClass(generateAdapter = true) data class ConversationsDeleteResponse( - @Json(name = "last_deleted_id") val lastDeletedId: Int + @Json(name = "last_deleted_id") val lastDeletedId: Long ) 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 672295f5..4af64159 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 @@ -25,7 +25,8 @@ data class MessagesGetByIdResponse( val count: Int, val items: List = emptyList(), val profiles: List?, - val groups: List? + val groups: List?, + val contacts: List? ) @JsonClass(generateAdapter = true) @@ -33,7 +34,8 @@ data class MessagesGetConversationMembersResponse( val count: Int, val items: List?, val profiles: List?, - val groups: List? + val groups: List?, + val contacts: List? ) @JsonClass(generateAdapter = true) @@ -47,6 +49,12 @@ data class MessagesGetHistoryAttachmentsResponse( @JsonClass(generateAdapter = true) data class MessagesCreateChatResponse( - @Json(name = "chat_id") val chatId: Int, + @Json(name = "chat_id") val chatId: Long, @Json(name = "peer_ids") val peerIds: List ) + +@JsonClass(generateAdapter = true) +data class MessagesSendResponse( + @Json(name = "message_id") val messageId: Long, + @Json(name = "cmid") val cmId: Long +) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/OAuthResponse.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/OAuthResponse.kt index 99d893d9..3be65f94 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/OAuthResponse.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/OAuthResponse.kt @@ -2,18 +2,29 @@ package dev.meloda.fast.model.api.responses import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import dev.meloda.fast.model.api.responses.AuthDirectResponse.BanInfo @JsonClass(generateAdapter = true) data class AuthDirectErrorOnlyResponse( - @Json(name = "error") val error: String, + @Json(name = "cant_get_code_open_restore") val restoreIfCannotGetCode: Boolean?, + @Json(name = "error") val error: String?, @Json(name = "error_description") val errorDescription: String?, - @Json(name = "error_type") val errorType: String? + @Json(name = "error_type") val errorType: String?, + @Json(name = "ban_info") val banInfo: BanInfo?, + @Json(name = "captcha_sid") val captchaSid: String?, + @Json(name = "captcha_img") val captchaImage: String?, + @Json(name = "captcha_ts") val captchaTs: Double?, + @Json(name = "validation_sid") val validationSid: String?, + @Json(name = "validation_type") val validationType: String?, + @Json(name = "phone_mask") val phoneMask: String?, + @Json(name = "redirect_uri") val redirectUri: String?, + @Json(name = "validation_resend") val validationResend: String?, ) @JsonClass(generateAdapter = true) data class AuthDirectResponse( @Json(name = "access_token") val accessToken: String?, - @Json(name = "user_id") val userId: Int?, + @Json(name = "user_id") val userId: Long?, @Json(name = "trusted_hash") val validationHash: String?, @Json(name = "validation_sid") val validationSid: String?, @Json(name = "validation_type") val validationType: String?, @@ -44,7 +55,45 @@ data class AuthDirectResponse( } @JsonClass(generateAdapter = true) -data class GetAnonymousTokenResponse( - @Json(name = "token") val token: String, - @Json(name = "expired_at") val expiredAt: Int +data class GetSilentTokenResponse( + @Json(name = "silent_token") val silentToken: String, + @Json(name = "silent_token_uuid") val silentTokenUuid: String, + @Json(name = "silent_token_ttl") val silentTokenTtl: Int, + @Json(name = "trusted_hash") val trustedHash: String?, // Приходит при наличии 2fa, + @Json(name = "error") val error: Error? ) + +@JsonClass(generateAdapter = true) +data class Error( + @Json(name = "error_code") val errorCode: Int, + @Json(name = "error_msg") val errorMessage: Int, + @Json(name = "redirect_uri") val redirectUri: String? +) + +@JsonClass(generateAdapter = true) +data class GetAnonymTokenResponse( + @Json(name = "token") val token: String +) + +@JsonClass(generateAdapter = true) +data class ExchangeSilentTokenResponse( + @Json(name = "access_token") val accessToken: String, + @Json(name = "is_partial") val isPartial: Boolean, + @Json(name = "is_service") val isService: Boolean, + @Json(name = "additional_signup_required") val additionalSignupRequired: Boolean, + @Json(name = "user_id") val userId: Long, + @Json(name = "expires_in") val expiresIn: Long +) + +@JsonClass(generateAdapter = true) +data class GetExchangeTokenResponse( + @Json(name = "users_exchange_tokens") val usersTokens: List +) { + + @JsonClass(generateAdapter = true) + data class UserTokenInfo( + @Json(name = "user_id") val userId: Long, + @Json(name = "profile_type") val profileType: Int, + @Json(name = "common_token") val commonToken: String + ) +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/PhotosResponses.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/PhotosResponses.kt index 3138cc6b..9db1420e 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/PhotosResponses.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/PhotosResponses.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json data class PhotosGetMessagesUploadServerResponse( @Json(name = "album_id") - val albumId: Int, + val albumid: Long, @Json(name = "upload_url") val uploadUrl: String ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/VideosResponses.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/VideosResponses.kt index 49888a3b..b5f18379 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/VideosResponses.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/VideosResponses.kt @@ -5,17 +5,17 @@ import com.squareup.moshi.Json data class VideosSaveResponse( @Json(name = "access_key") val accessKey: String, val description: String, - @Json(name = "owner_id") val ownerId: Int, + @Json(name = "owner_id") val ownerid: Long, val title: String, @Json(name = "upload_url") val uploadUrl: String, - @Json(name = "video_id") val videoId: Int + @Json(name = "video_id") val videoid: Long ) data class VideosUploadResponse( @Json(name = "video_hash") val hash: String?, val size: Int, @Json(name = "direct_link") val directLink: String, - @Json(name = "owner_id") val ownerId: Int, - @Json(name = "video_id") val videoId: Int, + @Json(name = "owner_id") val ownerid: Long, + @Json(name = "video_id") val videoid: Long, val error: String? ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/database/AccountEntity.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/database/AccountEntity.kt index 65b9d2e5..38c4a519 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/database/AccountEntity.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/database/AccountEntity.kt @@ -6,8 +6,9 @@ import androidx.room.PrimaryKey @Entity(tableName = "accounts") data class AccountEntity( @PrimaryKey(autoGenerate = false) - val userId: Int, + val userId: Long, val accessToken: String, val fastToken: String?, - val trustedHash: String? + val trustedHash: String?, + val exchangeToken: String? ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkConversationEntity.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkConversationEntity.kt index 9f5f67dc..094e8e9e 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkConversationEntity.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkConversationEntity.kt @@ -7,28 +7,29 @@ import dev.meloda.fast.model.api.domain.VkConversation @Entity(tableName = "conversations") data class VkConversationEntity( - @PrimaryKey val id: Int, - val localId: Int, - val ownerId: Int?, + @PrimaryKey val id: Long, + val localId: Long, + val ownerId: Long?, val title: String?, val photo50: String?, val photo100: String?, val photo200: String?, val isPhantom: Boolean, - val lastConversationMessageId: Int, - val inReadCmId: Int, - val outReadCmId: Int, - val inRead: Int, - val outRead: Int, - val lastMessageId: Int?, + val lastConversationMessageId: Long, + val inReadCmId: Long, + val outReadCmId: Long, + val inRead: Long, + val outRead: Long, + val lastMessageId: Long?, val unreadCount: Int, val membersCount: Int?, val canChangePin: Boolean, val canChangeInfo: Boolean, val majorId: Int, val minorId: Int, - val pinnedMessageId: Int?, + val pinnedMessageId: Long?, val peerType: String, + val isArchived: Boolean ) fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation( @@ -41,7 +42,7 @@ fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation( photo200 = photo200, isCallInProgress = false, isPhantom = isPhantom, - lastConversationMessageId = lastConversationMessageId, + lastCmId = lastConversationMessageId, inReadCmId = inReadCmId, outReadCmId = outReadCmId, inRead = inRead, @@ -57,6 +58,8 @@ fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation( interactionType = -1, interactionIds = emptyList(), peerType = PeerType.parse(peerType), + isArchived = isArchived, + lastMessage = null,//lastMessage?.asExternalModel(), pinnedMessage = null,//pinnedMessage?.asExternalModel(), user = null, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkGroupEntity.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkGroupEntity.kt index b93df715..ff4faf17 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkGroupEntity.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkGroupEntity.kt @@ -2,10 +2,11 @@ package dev.meloda.fast.model.database import androidx.room.Entity import androidx.room.PrimaryKey +import dev.meloda.fast.model.api.domain.VkGroupDomain @Entity(tableName = "groups") data class VkGroupEntity( - @PrimaryKey val id: Int, + @PrimaryKey val id: Long, val name: String, val screenName: String, val photo50: String?, @@ -13,3 +14,13 @@ data class VkGroupEntity( val photo200: String?, val membersCount: Int? ) + +fun VkGroupEntity.asDomain(): VkGroupDomain = VkGroupDomain( + id = id, + name = name, + screenName = screenName, + photo50 = photo50, + photo100 = photo100, + photo200 = photo200, + membersCount = membersCount +) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt index 652b7336..6ff5d676 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt @@ -7,24 +7,24 @@ import dev.meloda.fast.model.api.domain.VkUnknownAttachment @Entity(tableName = "messages") data class VkMessageEntity( - @PrimaryKey val id: Int, - val conversationMessageId: Int, + @PrimaryKey val id: Long, + val conversationMessageId: Long, val text: String?, val isOut: Boolean, - val peerId: Int, - val fromId: Int, + val peerId: Long, + val fromId: Long, val date: Int, - val randomId: Int, + val randomId: Long, val action: String?, - val actionMemberId: Int?, + val actionMemberId: Long?, val actionText: String?, - val actionConversationMessageId: Int?, + val actionConversationMessageId: Long?, val actionMessage: String?, val updateTime: Int?, val important: Boolean, - val forwardIds: List?, + val forwardIds: List?, val attachments: List?, // TODO: 01/05/2024, Danil Nikolaev: how to store??? - val replyMessageId: Int?, + val replyMessageId: Long?, val geoType: String?, val pinnedAt: Int?, val isPinned: Boolean @@ -32,7 +32,7 @@ data class VkMessageEntity( fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( id = id, - conversationMessageId = conversationMessageId, + cmId = conversationMessageId, text = text, isOut = isOut, peerId = peerId, @@ -56,5 +56,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( actionUser = null, actionGroup = null, pinnedAt = pinnedAt, - isPinned = isPinned + isPinned = isPinned, + isSpam = false, + formatData = null, ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkUserEntity.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkUserEntity.kt index b5ea957a..d92e04d6 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkUserEntity.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkUserEntity.kt @@ -2,17 +2,17 @@ package dev.meloda.fast.model.database import androidx.room.Entity import androidx.room.PrimaryKey -import dev.meloda.fast.model.api.domain.OnlineStatus +import dev.meloda.fast.model.api.data.parseUserOnlineState import dev.meloda.fast.model.api.domain.VkUser @Entity(tableName = "users") data class VkUserEntity( - @PrimaryKey val id: Int, + @PrimaryKey val id: Long, val firstName: String, val lastName: String, val isOnline: Boolean, val isOnlineMobile: Boolean, - val onlineAppId: Int?, + val onlineAppId: Long?, val lastSeen: Int?, val lastSeenStatus: String?, val birthday: String?, @@ -26,11 +26,12 @@ fun VkUserEntity.asExternalModel(): VkUser = VkUser( id = id, firstName = firstName, lastName = lastName, - onlineStatus = when { - !isOnline -> OnlineStatus.Offline - !isOnlineMobile -> OnlineStatus.Online(onlineAppId) - else -> OnlineStatus.OnlineMobile(onlineAppId) - }, + onlineStatus = parseUserOnlineState( + isOnline = isOnline, + isOnlineMobile = isOnlineMobile, + status = lastSeenStatus, + appId = onlineAppId + ), photo50 = photo50, photo100 = photo100, photo200 = photo200, diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt index 936671dc..ec429e19 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt @@ -35,6 +35,7 @@ import org.koin.dsl.bind import org.koin.dsl.module import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.create import java.util.concurrent.TimeUnit val networkModule = module { @@ -44,53 +45,45 @@ val networkModule = module { single { ChuckerInterceptor.Builder(get()).collector(get()).build() } singleOf(::VersionInterceptor) singleOf(::LanguageInterceptor) - single { - OkHttpClient.Builder() - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .addInterceptor(get(named("token_interceptor")) as Interceptor) - .addInterceptor(get()) - .addInterceptor(get()) - .addInterceptor(get()) - .followRedirects(true) - .followSslRedirects(true) - .addInterceptor( - HttpLoggingInterceptor().apply { - level = - HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal] - } - ) - .build() + + single(named("auth")) { + buildHttpClient(true) } - single { - Retrofit.Builder() - .baseUrl("${AppConstants.URL_API}/") - .addConverterFactory(ApiResultConverterFactory) - .addCallAdapterFactory(ApiResultCallAdapterFactory) - .addConverterFactory(ResponseConverterFactory(get())) - .addConverterFactory(MoshiConverterFactory.create(get())) - .client(get()) - .build() + single { + buildHttpClient(false) + } + + single(named("auth")) { + buildRetrofit(get(named("auth"))) + } + single { + buildRetrofit(get()) } - singleOf(::OAuthResultCallFactory) single(named("oauth")) { Retrofit.Builder() .baseUrl("${AppConstants.URL_OAUTH}/") .addCallAdapterFactory(get()) .addConverterFactory(MoshiConverterFactory.create(get())) - .client(get()) + .client(get(named("auth"))) .build() } + single { + get(named("auth")).create() + } + single { + get(named("auth")).create() + } + + singleOf(::OAuthResultCallFactory) + single { service(AccountService::class.java) } single { service(AudiosService::class.java) } - single { service(AuthService::class.java) } single { service(ConversationsService::class.java) } single { service(FilesService::class.java) } single { service(LongPollService::class.java) } single { service(MessagesService::class.java) } - single { service(OAuthService::class.java) } single { service(PhotosService::class.java) } single { service(UsersService::class.java) } single { service(VideosService::class.java) } @@ -98,3 +91,37 @@ val networkModule = module { } private fun Scope.service(className: Class): T = get().create(className) + +private fun Scope.buildHttpClient(forAuth: Boolean): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .apply { + if (!forAuth) { + addInterceptor(get(named("token_interceptor")) as Interceptor) + } + } + .addInterceptor(get()) + .addInterceptor(get()) + .addInterceptor(get()) + .followRedirects(true) + .followSslRedirects(true) + .addInterceptor( + HttpLoggingInterceptor().apply { + level = + HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal] + } + ) + .build() +} + +private fun Scope.buildRetrofit(client: OkHttpClient): Retrofit { + return Retrofit.Builder() + .baseUrl("${AppConstants.URL_API}/") + .addConverterFactory(ApiResultConverterFactory) + .addCallAdapterFactory(ApiResultCallAdapterFactory) + .addConverterFactory(ResponseConverterFactory(get())) + .addConverterFactory(MoshiConverterFactory.create(get())) + .client(client) + .build() +} diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthService.kt index 18606900..149c98df 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthService.kt @@ -1,16 +1,28 @@ package dev.meloda.fast.network.service.auth +import com.slack.eithernet.ApiResult +import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse +import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse +import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse import dev.meloda.fast.model.api.responses.ValidateLoginResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse 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.GET +import retrofit2.http.POST import retrofit2.http.Query import retrofit2.http.QueryMap interface AuthService { + @GET(AuthUrls.LOGOUT) + suspend fun logout( + @Query("client_id") clientId: String, + @Query("client_secret") clientSecret: String + ): ApiResult, RestApiError> + @GET(AuthUrls.VALIDATE_PHONE) suspend fun validatePhone( @Query("sid") validationSid: String @@ -20,4 +32,22 @@ interface AuthService { suspend fun validateLogin( @QueryMap param: Map ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(AuthUrls.GET_ANONYM_TOKEN) + suspend fun getAnonymToken( + @FieldMap param: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(AuthUrls.EXCHANGE_SILENT_TOKEN) + suspend fun exchangeSilentToken( + @FieldMap param: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(AuthUrls.GET_EXCHANGE_TOKEN) + suspend fun getExchangeToken( + @FieldMap param: Map + ): ApiResult, RestApiError> } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthUrls.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthUrls.kt index 7f03b1ed..d8f82376 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthUrls.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/auth/AuthUrls.kt @@ -5,6 +5,12 @@ import dev.meloda.fast.common.AppConstants object AuthUrls { private const val URL = AppConstants.URL_API + const val LOGOUT = "$URL/auth.logout" + const val VALIDATE_PHONE = "$URL/auth.validatePhone" const val VALIDATE_LOGIN = "$URL/auth.validateLogin" + + const val GET_ANONYM_TOKEN = "$URL/auth.getAnonymToken" + const val EXCHANGE_SILENT_TOKEN = "$URL/auth.exchangeSilentAuthToken" + const val GET_EXCHANGE_TOKEN = "$URL/auth.getExchangeToken" } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt index e891472b..8fa267f5 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt @@ -32,13 +32,31 @@ interface ConversationsService { @FormUrlEncoded @POST(ConversationsUrls.PIN) - suspend fun pin(@FieldMap params: Map): ApiResult, RestApiError> + suspend fun pin( + @FieldMap params: Map + ): ApiResult, RestApiError> @FormUrlEncoded @POST(ConversationsUrls.UNPIN) - suspend fun unpin(@FieldMap params: Map): ApiResult, RestApiError> + suspend fun unpin( + @FieldMap params: Map + ): ApiResult, RestApiError> @FormUrlEncoded @POST(ConversationsUrls.REORDER_PINNED) - suspend fun reorderPinned(@FieldMap params: Map): ApiResult + suspend fun reorderPinned( + @FieldMap params: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(ConversationsUrls.ARCHIVE) + suspend fun archive( + @FieldMap params: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(ConversationsUrls.UNARCHIVE) + suspend fun unarchive( + @FieldMap params: Map + ): ApiResult, RestApiError> } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt index ba130cd3..c1230abf 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt @@ -4,10 +4,14 @@ import dev.meloda.fast.common.AppConstants object ConversationsUrls { - const val GET = "${AppConstants.URL_API}/messages.getConversations" - const val GET_BY_ID = "${AppConstants.URL_API}/messages.getConversationsById" - const val DELETE = "${AppConstants.URL_API}/messages.deleteConversation" - const val PIN = "${AppConstants.URL_API}/messages.pinConversation" - const val UNPIN = "${AppConstants.URL_API}/messages.unpinConversation" - const val REORDER_PINNED = "${AppConstants.URL_API}/messages.reorderPinnedConversations" + private const val URL = AppConstants.URL_API + + const val GET = "$URL/messages.getConversations" + const val GET_BY_ID = "$URL/messages.getConversationsById" + const val DELETE = "$URL/messages.deleteConversation" + const val PIN = "$URL/messages.pinConversation" + const val UNPIN = "$URL/messages.unpinConversation" + const val REORDER_PINNED = "$URL/messages.reorderPinnedConversations" + const val ARCHIVE = "$URL/messages.archiveConversation" + const val UNARCHIVE = "$URL/messages.unarchiveConversation" } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/friends/FriendsService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/friends/FriendsService.kt index 35069f7d..7618f673 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/friends/FriendsService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/friends/FriendsService.kt @@ -20,5 +20,5 @@ interface FriendsService { @POST(FriendsUrls.GET_ONLINE) suspend fun getOnlineFriends( @FieldMap params: Map - ): ApiResult>, RestApiError> + ): ApiResult>, RestApiError> } 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 45e5d517..d528af64 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,15 @@ package dev.meloda.fast.network.service.messages import com.slack.eithernet.ApiResult +import dev.meloda.fast.model.api.data.VkChatData import dev.meloda.fast.model.api.data.VkLongPollData import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse +import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse +import dev.meloda.fast.model.api.responses.MessagesSendResponse import dev.meloda.fast.network.ApiResponse import dev.meloda.fast.network.RestApiError import retrofit2.http.FieldMap @@ -31,7 +34,7 @@ interface MessagesService { @POST(MessagesUrls.SEND) suspend fun send( @FieldMap params: Map - ): ApiResult, RestApiError> + ): ApiResult, RestApiError> @FormUrlEncoded @POST(MessagesUrls.GET_LONG_POLL_SERVER) @@ -73,36 +76,35 @@ interface MessagesService { @POST(MessagesUrls.MARK_AS_IMPORTANT) suspend fun markAsImportant( @FieldMap params: Map - ): ApiResult>, RestApiError> + ): ApiResult>, RestApiError> @FormUrlEncoded @POST(MessagesUrls.DELETE) suspend fun delete( @FieldMap params: Map ): ApiResult>, RestApiError> -// -// @FormUrlEncoded -// @POST(MessagesUrls.Edit) -// suspend fun edit( -// @FieldMap params: Map -// ): ApiResult, RestApiError> -// -// -// @FormUrlEncoded -// @POST(MessagesUrls.GetChat) -// suspend fun getChat( -// @FieldMap params: Map -// ): ApiResult, RestApiError> -// -// @FormUrlEncoded -// @POST(MessagesUrls.GetConversationMembers) -// suspend fun getConversationMembers( -// @FieldMap params: Map -// ): ApiResult, RestApiError> -// -// @FormUrlEncoded -// @POST(MessagesUrls.RemoveChatUser) -// suspend fun removeChatUser( -// @FieldMap params: Map -// ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(MessagesUrls.EDIT) + suspend fun edit( + @FieldMap params: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(MessagesUrls.GET_CHAT) + suspend fun getChat( + @FieldMap params: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(MessagesUrls.GET_CONVERSATIONS_MEMBERS) + suspend fun getConversationMembers( + @FieldMap params: Map + ): ApiResult, RestApiError> + + @FormUrlEncoded + @POST(MessagesUrls.REMOVE_CHAT_USER) + suspend fun removeChatUser( + @FieldMap params: Map + ): ApiResult, RestApiError> } 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 2de07a9f..b4f196d4 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 @@ -3,21 +3,23 @@ package dev.meloda.fast.network.service.messages import dev.meloda.fast.common.AppConstants object MessagesUrls { - - const val GET_HISTORY = "${AppConstants.URL_API}/messages.getHistory" - const val SEND = "${AppConstants.URL_API}/messages.send" - const val MARK_AS_IMPORTANT = "${AppConstants.URL_API}/messages.markAsImportant" - const val GET_LONG_POLL_SERVER = "${AppConstants.URL_API}/messages.getLongPollServer" - const val GET_LONG_POLL_HISTORY = "${AppConstants.URL_API}/messages.getLongPollHistory" - const val PIN = "${AppConstants.URL_API}/messages.pin" - const val UNPIN = "${AppConstants.URL_API}/messages.unpin" - const val DELETE = "${AppConstants.URL_API}/messages.delete" - const val EDIT = "${AppConstants.URL_API}/messages.edit" - const val GET_BY_ID = "${AppConstants.URL_API}/messages.getById" - const val MARK_AS_READ = "${AppConstants.URL_API}/messages.markAsRead" - 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" - const val CREATE_CHAT = "${AppConstants.URL_API}/messages.createChat" + + private const val URL = AppConstants.URL_API + + const val GET_HISTORY = "$URL/messages.getHistory" + const val SEND = "$URL/messages.send" + const val MARK_AS_IMPORTANT = "$URL/messages.markAsImportant" + const val GET_LONG_POLL_SERVER = "$URL/messages.getLongPollServer" + const val GET_LONG_POLL_HISTORY = "$URL/messages.getLongPollHistory" + const val PIN = "$URL/messages.pin" + const val UNPIN = "$URL/messages.unpin" + const val DELETE = "$URL/messages.delete" + const val EDIT = "$URL/messages.edit" + const val GET_BY_ID = "$URL/messages.getById" + const val MARK_AS_READ = "$URL/messages.markAsRead" + const val GET_CHAT = "$URL/messages.getChat" + const val GET_CONVERSATIONS_MEMBERS = "$URL/messages.getConversationMembers" + const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser" + const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments" + const val CREATE_CHAT = "$URL/messages.createChat" } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthService.kt index 427258b8..cb717950 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthService.kt @@ -1,23 +1,24 @@ package dev.meloda.fast.network.service.oauth -import dev.meloda.fast.model.api.responses.AuthDirectResponse -import dev.meloda.fast.model.api.responses.GetAnonymousTokenResponse import com.slack.eithernet.ApiResult import com.slack.eithernet.DecodeErrorBody +import dev.meloda.fast.model.api.responses.AuthDirectErrorOnlyResponse +import dev.meloda.fast.model.api.responses.AuthDirectResponse +import dev.meloda.fast.model.api.responses.GetSilentTokenResponse import retrofit2.http.GET import retrofit2.http.QueryMap interface OAuthService { @DecodeErrorBody - @GET(OAuthUrls.DIRECT_AUTH) + @GET(OAuthUrls.GET_SILENT_TOKEN) suspend fun auth( @QueryMap param: Map - ): ApiResult + ): ApiResult @DecodeErrorBody - @GET(OAuthUrls.GET_ANONYMOUS_TOKEN) - suspend fun getAnonymousToken( + @GET(OAuthUrls.GET_SILENT_TOKEN) + suspend fun getSilentToken( @QueryMap param: Map - ): ApiResult + ): ApiResult } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthUrls.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthUrls.kt index c9a6c593..557ed263 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthUrls.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/oauth/OAuthUrls.kt @@ -5,6 +5,5 @@ import dev.meloda.fast.common.AppConstants object OAuthUrls { private const val URL = AppConstants.URL_OAUTH - const val DIRECT_AUTH = "$URL/token" - const val GET_ANONYMOUS_TOKEN = "$URL/get_anonym_token" + const val GET_SILENT_TOKEN = "$URL/token" } diff --git a/core/presentation/src/main/AndroidManifest.xml b/core/presentation/src/main/AndroidManifest.xml deleted file mode 100644 index 8bdb7e14..00000000 --- a/core/presentation/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/VkErrorView.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/VkErrorView.kt new file mode 100644 index 00000000..288c688e --- /dev/null +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/VkErrorView.kt @@ -0,0 +1,70 @@ +package dev.meloda.fast.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.ui.R + +@Composable +fun VkErrorView( + modifier: Modifier = Modifier, + baseError: BaseError, + onButtonClick: () -> Unit = {} +) { + when (baseError) { + is BaseError.SessionExpired -> { + ErrorView( + modifier = modifier, + text = stringResource(R.string.session_expired), + buttonText = stringResource(R.string.action_log_out), + onButtonClick = onButtonClick + ) + } + + is BaseError.SimpleError -> { + ErrorView( + modifier = modifier, + text = baseError.message, + buttonText = stringResource(R.string.try_again), + onButtonClick = onButtonClick + ) + } + + BaseError.AccountBlocked -> { + ErrorView( + modifier = modifier, + text = "Account blocked", + buttonText = stringResource(R.string.action_log_out), + onButtonClick = onButtonClick + ) + } + + BaseError.ConnectionError -> { + ErrorView( + modifier = modifier, + text = "Connection error", + buttonText = stringResource(R.string.try_again), + onButtonClick = onButtonClick + ) + } + + BaseError.InternalError -> { + ErrorView( + modifier = modifier, + text = "Internal error", + buttonText = stringResource(R.string.try_again), + onButtonClick = onButtonClick + ) + } + + BaseError.UnknownError -> { + ErrorView( + modifier = modifier, + text = "Unknown error", + buttonText = stringResource(R.string.try_again), + onButtonClick = onButtonClick + ) + } + } +} diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/extensions/SharedViewModel.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/extensions/SharedViewModel.kt index 2bfb3f95..3b7c3143 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/extensions/SharedViewModel.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/extensions/SharedViewModel.kt @@ -6,13 +6,27 @@ import androidx.lifecycle.ViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import org.koin.androidx.compose.koinViewModel -import org.koin.androidx.compose.navigation.koinNavViewModel +import org.koin.core.parameter.ParametersDefinition +import org.koin.core.qualifier.Qualifier @Composable -inline fun NavBackStackEntry.sharedViewModel(navController: NavController): T { - val navGraphRoute = destination.parent?.route ?: return koinViewModel() +inline fun NavBackStackEntry.sharedViewModel( + navController: NavController, + route: String? = null, + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null, +): T { + val navGraphRoute = route ?: destination.parent?.route ?: return koinViewModel( + qualifier = qualifier, + parameters = parameters + ) val parentEntry = remember(this) { navController.getBackStackEntry(navGraphRoute) } - return koinNavViewModel(viewModelStoreOwner = parentEntry) + + return koinViewModel( + viewModelStoreOwner = parentEntry, + qualifier = qualifier, + parameters = parameters + ) } 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 index 775a26a0..32689d70 100644 --- 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 @@ -28,4 +28,14 @@ sealed class ConversationOption( title = UiText.Resource(R.string.action_delete), icon = UiImage.Resource(R.drawable.round_delete_outline_24) ) + + data object Archive : ConversationOption( + title = UiText.Resource(R.string.conversation_context_action_archive), + icon = UiImage.Resource(R.drawable.outline_archive_24) + ) + + data object Unarchive : ConversationOption( + title = UiText.Resource(R.string.conversation_context_action_unarchive), + icon = UiImage.Resource(R.drawable.outline_unarchive_24) + ) } diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationsShowOptions.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationsShowOptions.kt deleted file mode 100644 index 66a77a8b..00000000 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/ConversationsShowOptions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.meloda.fast.ui.model.api - -data class ConversationsShowOptions( - val showDeleteDialog: Int?, - val showPinDialog: UiConversation? -) { - - companion object { - val EMPTY: ConversationsShowOptions = ConversationsShowOptions( - showDeleteDialog = null, - showPinDialog = null - ) - } -} diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiConversation.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiConversation.kt index 9944284f..3a725d6b 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiConversation.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiConversation.kt @@ -9,8 +9,8 @@ import dev.meloda.fast.ui.util.ImmutableList @Immutable data class UiConversation( - val id: Int, - val lastMessageId: Int?, + val id: Long, + val lastMessageId: Long?, val avatar: UiImage?, val title: String, val unreadCount: String?, @@ -27,5 +27,6 @@ data class UiConversation( val peerType: PeerType, val interactionText: String?, val isExpanded: Boolean, + val isArchived: Boolean, val options: ImmutableList, ) diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiFriend.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiFriend.kt index 3e64e990..ed4aa94c 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiFriend.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/model/api/UiFriend.kt @@ -6,7 +6,7 @@ import dev.meloda.fast.model.api.domain.OnlineStatus @Immutable data class UiFriend( - val userId: Int, + val userId: Long, val avatar: UiImage?, val firstName: String, val lastName: String, diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt index 7cb44f43..17134119 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/theme/AppTheme.kt @@ -2,6 +2,7 @@ package dev.meloda.fast.ui.theme import android.app.Activity import android.os.Build +import androidx.compose.animation.animateColorAsState import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme @@ -9,8 +10,10 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.SideEffect import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -20,6 +23,7 @@ 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 androidx.navigation.NavController import dev.chrisbanes.haze.HazeState import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.ui.R @@ -129,7 +133,14 @@ val LocalSizeConfig = compositionLocalOf { val LocalHazeState = compositionLocalOf { HazeState() } val LocalBottomPadding = compositionLocalOf { 0.dp } val LocalUser = compositionLocalOf { null } -val LocalScrollToTop = compositionLocalOf { mapOf() } +val LocalReselectedTab = compositionLocalOf { mapOf() } +val LocalNavRootController = compositionLocalOf { null } +val LocalNavController = compositionLocalOf { null } + +@Composable +fun ProvidableCompositionLocal.getOrThrow(): T { + return requireNotNull(current) +} @Composable fun AppTheme( @@ -141,9 +152,10 @@ fun AppTheme( selectedColorScheme: Int = 0, content: @Composable () -> Unit ) { + val context = LocalContext.current + val colorScheme: ColorScheme = when { useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } @@ -166,6 +178,10 @@ fun AppTheme( } } + val colorPrimary by animateColorAsState(colorScheme.primary) + val colorSurface by animateColorAsState(colorScheme.surface) + val colorBackground by animateColorAsState(colorScheme.background) + val typography = if (useSystemFont) { MaterialTheme.typography } else { @@ -198,7 +214,12 @@ fun AppTheme( } MaterialTheme( - colorScheme = predefinedColorScheme ?: colorScheme, + colorScheme = (predefinedColorScheme ?: colorScheme) + .copy( + primary = colorPrimary, + background = colorBackground, + surface = colorSurface + ), typography = typography, content = content ) diff --git a/core/ui/src/main/res/drawable/outline_archive_24.xml b/core/ui/src/main/res/drawable/outline_archive_24.xml new file mode 100644 index 00000000..61135264 --- /dev/null +++ b/core/ui/src/main/res/drawable/outline_archive_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/outline_unarchive_24.xml b/core/ui/src/main/res/drawable/outline_unarchive_24.xml new file mode 100644 index 00000000..8cf26a43 --- /dev/null +++ b/core/ui/src/main/res/drawable/outline_unarchive_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/core/ui/src/main/res/values-ru/strings.xml b/core/ui/src/main/res/values-ru/strings.xml index 7236dc0e..20927524 100644 --- a/core/ui/src/main/res/values-ru/strings.xml +++ b/core/ui/src/main/res/values-ru/strings.xml @@ -24,6 +24,7 @@ Пометить как спам Прочитать Удалить + Из архива Удалить Удалить чат? Выйти @@ -32,10 +33,12 @@ Закрепить Открепить чат? Закрепить чат? + Разархивировать чат? Закрепить Открепить Пометить Убрать пометку + Из архива Исходящий вызов Входящий вызов Закончился @@ -125,10 +128,13 @@ Подкаст Момент Статья + Видеосообщение + Стикер группы + Превью стикерпака Загрузка файла Загрузка фото Загрузка видео - Печатает + печатает Записывает %1$s печатают %1$s печатает @@ -175,6 +181,7 @@ Участники: %1$d Загрузка… Чаты + Архив Друзья Профиль Все @@ -253,4 +260,7 @@ Вы уверены, что хотите убрать пометку спама у этого сообщения? Закрепить сообщение Скопировано в буфер обмена + В архив + Архивировать чат? + В архив diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index db5bdafc..96f11fe1 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -104,11 +104,14 @@ Podcast Narrative Article + Video message + Group sticker + Sticker pack preview Uploading file Uploading photo Uploading video - Typing + typing Recording %1$s are typing @@ -149,6 +152,8 @@ Read Delete + Archive + Unarchive Delete Delete the conversation? Sign out @@ -157,10 +162,14 @@ Pin Unpin the conversation? Pin the conversation? + Archive the conversation? + Unarchive the conversation? Pin Unpin Mark Unmark + Archive + Unarchive Outgoing call Incoming call Ended @@ -236,6 +245,7 @@ Members: %1$d Loading… Conversations + Archive Friends Profile All diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt index b93d206b..74cb253b 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt @@ -59,23 +59,23 @@ fun NavGraphBuilder.authNavGraph( validationScreen( onBack = { - navController.navigateUp() navController.setValidationResult(null) + navController.navigateUp() }, onResult = { code -> - navController.popBackStack() navController.setValidationResult(code) + navController.popBackStack() } ) captchaScreen( onBack = { - navController.navigateUp() navController.setCaptchaResult(null) + navController.navigateUp() }, onResult = { code -> - navController.popBackStack() navController.setCaptchaResult(code) + navController.popBackStack() } ) diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/navigation/CaptchaNavigation.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/navigation/CaptchaNavigation.kt index b415a2fa..abfd7d12 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/navigation/CaptchaNavigation.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/navigation/CaptchaNavigation.kt @@ -34,7 +34,7 @@ fun NavController.navigateToCaptcha(captchaImageUrl: String) { } fun NavController.setCaptchaResult(code: String?) { - this.currentBackStackEntry + this.previousBackStackEntry ?.savedStateHandle ?.set("captcha_code", code) } diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/presentation/CaptchaScreen.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/presentation/CaptchaScreen.kt index 3b92f65f..459a6bfa 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/presentation/CaptchaScreen.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/presentation/CaptchaScreen.kt @@ -8,10 +8,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -24,6 +27,7 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -123,7 +127,9 @@ fun CaptchaScreen( val focusManager = LocalFocusManager.current - Scaffold { padding -> + Scaffold( + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime) + ) { padding -> Column( modifier = Modifier .fillMaxSize() diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt index 8b76e732..e91b2a45 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt @@ -15,18 +15,21 @@ import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue +import dev.meloda.fast.common.extensions.updateValue import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.data.State import dev.meloda.fast.data.UserConfig +import dev.meloda.fast.data.api.auth.AuthRepository import dev.meloda.fast.data.db.AccountsRepository import dev.meloda.fast.data.processState +import dev.meloda.fast.data.success import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.OAuthUseCase import dev.meloda.fast.model.database.AccountEntity import dev.meloda.fast.network.OAuthErrorDomain +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -39,13 +42,14 @@ interface LoginViewModel { val screenState: StateFlow val loginDialog: StateFlow - val validationCode: StateFlow val validationArguments: StateFlow - val captchaCode: StateFlow val captchaArguments: StateFlow val userBannedArguments: StateFlow val isNeedToOpenMain: StateFlow + val isNeedToClearCaptchaCode: StateFlow + val isNeedToClearValidationCode: StateFlow + fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) fun onDialogDismissed(dialog: LoginDialog) @@ -63,14 +67,15 @@ interface LoginViewModel { fun onNavigatedToCaptcha() fun onNavigatedToValidation() - fun onValidationCodeReceived(code: String) - fun onCaptchaCodeReceived(code: String) - - fun onLogoLongClicked() + fun onValidationCodeReceived(code: String?) + fun onValidationCodeCleared() + fun onCaptchaCodeReceived(code: String?) + fun onCaptchaCodeCleared() } class LoginViewModelImpl( private val oAuthUseCase: OAuthUseCase, + private val authRepository: AuthRepository, private val loadUserByIdUseCase: LoadUserByIdUseCase, private val accountsRepository: AccountsRepository, private val loginValidator: LoginValidator, @@ -80,27 +85,41 @@ class LoginViewModelImpl( override val screenState = MutableStateFlow(LoginScreenState.EMPTY) override val loginDialog = MutableStateFlow(null) - override val validationCode = MutableStateFlow(null) override val validationArguments = MutableStateFlow(null) - override val captchaCode = MutableStateFlow(null) override val captchaArguments = MutableStateFlow(null) override val userBannedArguments = MutableStateFlow(null) override val isNeedToOpenMain = MutableStateFlow(false) + override val isNeedToClearCaptchaCode = MutableStateFlow(false) + override val isNeedToClearValidationCode = MutableStateFlow(false) + private val validationState: StateFlow> = screenState.map(loginValidator::validate) .stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty)) + private val captchaSid = MutableStateFlow(null) + private val captchaCode = MutableStateFlow(null) + private val validationSid = MutableStateFlow(null) + private val validationCode = MutableStateFlow(null) + + init { + captchaCode.listenValue(viewModelScope) { + if (it != null) { + login() + } + } + validationCode.listenValue(viewModelScope) { + if (it != null) { + login() + } + } + } + override fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) { onDialogDismissed(dialog) when (dialog) { is LoginDialog.Error -> Unit - - LoginDialog.FastAuth -> { - val token = bundle.getString("token")?.trim() ?: return - fastAuth(token) - } } } @@ -161,68 +180,20 @@ class LoginViewModelImpl( validationArguments.update { null } } - override fun onValidationCodeReceived(code: String) { + override fun onValidationCodeReceived(code: String?) { validationCode.update { code } - - login() } - override fun onCaptchaCodeReceived(code: String) { + override fun onValidationCodeCleared() { + isNeedToClearValidationCode.update { false } + } + + override fun onCaptchaCodeReceived(code: String?) { captchaCode.update { code } - - login() } - override fun onLogoLongClicked() { - loginDialog.setValue { LoginDialog.FastAuth } - } - - private fun fastAuth(token: String) { - var currentAccount = AccountEntity( - userId = -1, - accessToken = token, - fastToken = null, - trustedHash = null - ).also { account -> - UserConfig.currentUserId = account.userId - UserConfig.userId = account.userId - UserConfig.accessToken = account.accessToken - UserConfig.fastToken = account.fastToken - UserConfig.trustedHash = account.trustedHash - } - - loadUserByIdUseCase( - userId = null, - fields = VkConstants.USER_FIELDS, - nomCase = null - ).listenValue(viewModelScope) { state -> - state.processState( - error = { - UserConfig.currentUserId = -1 - UserConfig.userId = -1 - UserConfig.accessToken = "" - - loginDialog.setValue { LoginDialog.Error() } - }, - success = { response -> - val actualUserId = requireNotNull(response).id - - currentAccount = currentAccount.copy(userId = actualUserId) - - UserConfig.userId = actualUserId - UserConfig.currentUserId = actualUserId - - startLongPoll() - - viewModelScope.launch(Dispatchers.IO) { - accountsRepository.storeAccounts(listOf(currentAccount)) - delay(350) - isNeedToOpenMain.update { true } - } - } - ) - screenState.setValue { old -> old.copy(isLoading = state.isLoading()) } - } + override fun onCaptchaCodeCleared() { + isNeedToClearCaptchaCode.update { false } } private fun login(forceSms: Boolean = false) { @@ -239,77 +210,120 @@ class LoginViewModelImpl( processValidation() if (!validationState.value.contains(LoginValidationResult.Valid)) return - oAuthUseCase.auth( + screenState.updateValue { copy(isLoading = false) } + + val currentValidationSid = validationSid.value + val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null } + val currentCaptchaSid = captchaSid.value + val currentCaptchaCode = captchaCode.value?.takeIf { currentCaptchaSid != null } + + oAuthUseCase.getSilentToken( login = currentState.login, password = currentState.password, forceSms = forceSms, - validationCode = validationCode.value, - captchaSid = captchaArguments.value?.captchaSid, - captchaKey = captchaCode.value + validationCode = currentValidationCode, + captchaSid = currentCaptchaSid, + captchaKey = currentCaptchaCode ).listenValue(viewModelScope) { state -> state.processState( error = { error -> Log.d("LoginViewModelImpl", "login: error: $error") - validationCode.update { null } - captchaCode.update { null } + screenState.updateValue { copy(isLoading = false) } + captchaSid.setValue { null } parseError(error) }, success = { response -> - val userId = response.userId - val accessToken = response.accessToken + val exceptionHandler = + CoroutineExceptionHandler { _, _ -> + screenState.updateValue { copy(isLoading = false) } + loginDialog.setValue { LoginDialog.Error() } + } - if (userId == null || accessToken == null) { - loginDialog.setValue { LoginDialog.Error() } - return@processState + viewModelScope.launch(Dispatchers.IO + exceptionHandler) { + val (anonymToken) = authRepository.getAnonymToken( + VkConstants.MESSENGER_APP_ID.toString(), + VkConstants.MESSENGER_APP_SECRET + ).success() + + val exchangeSilentTokenResponse = authRepository.exchangeSilentToken( + anonymToken = anonymToken, + silentToken = response.silentToken, + silentUuid = response.silentTokenUuid + ).success() + + + val getExchangeTokenResponse = + authRepository.getExchangeToken(exchangeSilentTokenResponse.accessToken) + .success() + + val exchangeToken = + getExchangeTokenResponse.usersTokens.firstOrNull { + it.userId == exchangeSilentTokenResponse.userId + } + + if (exchangeToken == null) { + screenState.updateValue { copy(isLoading = false) } + loginDialog.setValue { LoginDialog.Error() } + return@launch + } + + val userId = exchangeSilentTokenResponse.userId + val accessToken = exchangeSilentTokenResponse.accessToken + + // TODO: 30-Mar-25, Danil Nikolaev: get fast's app token + + val currentAccount = AccountEntity( + userId = userId, + accessToken = accessToken, + fastToken = null, + trustedHash = response.trustedHash, + exchangeToken = exchangeToken.commonToken + ).also { account -> + UserConfig.currentUserId = account.userId + UserConfig.userId = account.userId + UserConfig.accessToken = account.accessToken + UserConfig.fastToken = account.fastToken + UserConfig.trustedHash = account.trustedHash + UserConfig.exchangeToken = account.exchangeToken + } + + accountsRepository.storeAccounts(listOf(currentAccount)) + + startLongPoll() + + captchaSid.update { null } + validationSid.update { null } + + loadUserByIdUseCase( + userId = userId, + fields = VkConstants.USER_FIELDS, + nomCase = null + ).listenValue(viewModelScope) { state -> + state.processState( + any = { + screenState.updateValue { copy(isLoading = false) } + }, + error = ::parseError, + success = { user -> + if (user == null) { + loginDialog.update { LoginDialog.Error() } + } else { + screenState.updateValue { copy(login = "", password = "") } + isNeedToOpenMain.update { true } + } + } + ) + } } - - loadUserByIdUseCase( - userId = userId, - fields = VkConstants.USER_FIELDS, - nomCase = null - ) - - val currentAccount = AccountEntity( - userId = userId, - accessToken = accessToken, - fastToken = null, - trustedHash = response.validationHash - ).also { account -> - UserConfig.currentUserId = account.userId - UserConfig.userId = account.userId - UserConfig.accessToken = account.accessToken - UserConfig.fastToken = account.fastToken - UserConfig.trustedHash = account.trustedHash - } - - startLongPoll() - - accountsRepository.storeAccounts(listOf(currentAccount)) - - captchaArguments.update { null } - captchaCode.update { null } - - validationArguments.update { null } - validationCode.update { null } - - screenState.setValue { old -> - old.copy( - login = "", - password = "", - ) - } - - isNeedToOpenMain.update { true } } ) - screenState.emit(screenState.value.copy(isLoading = state.isLoading())) } } - private fun parseError(stateError: State.Error): Boolean { - return when (stateError) { + private fun parseError(stateError: State.Error) { + when (stateError) { is State.Error.OAuthError -> { when (val error = stateError.error) { is OAuthErrorDomain.ValidationRequiredError -> { @@ -321,6 +335,7 @@ class LoginViewModelImpl( canResendSms = error.validationResend == "sms" ) validationArguments.update { arguments } + validationSid.update { error.validationSid } } is OAuthErrorDomain.CaptchaRequiredError -> { @@ -329,6 +344,7 @@ class LoginViewModelImpl( captchaImageUrl = error.captchaImageUrl ) captchaArguments.update { arguments } + captchaSid.update { error.captchaSid } } OAuthErrorDomain.InvalidCredentialsError -> { @@ -348,12 +364,16 @@ class LoginViewModelImpl( } OAuthErrorDomain.WrongValidationCode -> { + isNeedToClearValidationCode.update { true } + validationCode.update { null } loginDialog.setValue { LoginDialog.Error(errorText = "Wrong validation code.") } } OAuthErrorDomain.WrongValidationCodeFormat -> { + isNeedToClearValidationCode.update { true } + validationCode.update { null } loginDialog.setValue { LoginDialog.Error(errorText = "Wrong validation code format.") } @@ -369,18 +389,9 @@ class LoginViewModelImpl( loginDialog.setValue { LoginDialog.Error() } } } - - true } - is State.Error.TestError -> { - loginDialog.setValue { - LoginDialog.Error(errorText = stateError.message) - } - true - } - - else -> false + else -> Unit } } diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt index ad6806cc..4eab5c24 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginDialog.kt @@ -5,8 +5,6 @@ import androidx.compose.runtime.Immutable @Immutable sealed class LoginDialog { - data object FastAuth : LoginDialog() - data class Error( val errorText: String? = null, val errorTextResId: Int? = null diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginError.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginError.kt deleted file mode 100644 index c3a6c84b..00000000 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/model/LoginError.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.meloda.fast.auth.login.model - -import androidx.compose.runtime.Immutable - -@Immutable -sealed class LoginError { - data object Unknown : LoginError() - data object WrongCredentials : LoginError() - data object TooManyTries : LoginError() - data object WrongValidationCode : LoginError() - data object WrongValidationCodeFormat : LoginError() - data class SimpleError(val message: String): LoginError() -} diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt index fc67491d..424f0216 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/navigation/LoginNavigation.kt @@ -1,5 +1,8 @@ package dev.meloda.fast.auth.login.navigation +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder @@ -27,6 +30,23 @@ fun NavGraphBuilder.loginScreen( val viewModel: LoginViewModel = backStackEntry.sharedViewModel(navController = navController) + val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle() + val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle() + + LaunchedEffect(clearValidationCode) { + if (clearValidationCode) { + backStackEntry.savedStateHandle["validation_code"] = null + viewModel.onValidationCodeCleared() + } + } + + LaunchedEffect(clearCaptchaCode) { + if (clearCaptchaCode) { + backStackEntry.savedStateHandle["captcha_code"] = null + viewModel.onCaptchaCodeCleared() + } + } + val validationCode = backStackEntry.getValidationResult() val captchaCode = backStackEntry.getCaptchaResult() diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt index 4a9229d8..3e11bd5f 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt @@ -4,7 +4,6 @@ import android.os.Bundle import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box @@ -35,7 +34,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.ContentType -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -56,16 +54,17 @@ import dev.meloda.fast.auth.login.LoginViewModel import dev.meloda.fast.auth.login.LoginViewModelImpl import dev.meloda.fast.auth.login.model.CaptchaArguments import dev.meloda.fast.auth.login.model.LoginDialog -import dev.meloda.fast.auth.login.model.LoginError import dev.meloda.fast.auth.login.model.LoginScreenState import dev.meloda.fast.auth.login.model.LoginUserBannedArguments import dev.meloda.fast.auth.login.model.LoginValidationArguments +import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.ui.components.MaterialDialog import dev.meloda.fast.ui.components.TextFieldErrorText import dev.meloda.fast.ui.theme.LocalSizeConfig import dev.meloda.fast.ui.util.handleEnterKey import dev.meloda.fast.ui.util.handleTabKey import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject import dev.meloda.fast.ui.R as UiR @Composable @@ -90,41 +89,36 @@ fun LoginRoute( onBack = viewModel::onBackPressed ) - LaunchedEffect( - isNeedToOpenMain, - userBannedArguments, - captchaArguments, - validationArguments, - validationCode, - captchaCode - ) { + LaunchedEffect(isNeedToOpenMain) { if (isNeedToOpenMain) { viewModel.onNavigatedToMain() onNavigateToMain() } + } + LaunchedEffect(userBannedArguments) { userBannedArguments?.let { arguments -> viewModel.onNavigatedToUserBanned() onNavigateToUserBanned(arguments) } - + } + LaunchedEffect(captchaArguments) { captchaArguments?.let { arguments -> viewModel.onNavigatedToCaptcha() onNavigateToCaptcha(arguments) } - + } + LaunchedEffect(validationArguments) { validationArguments?.let { arguments -> viewModel.onNavigatedToValidation() onNavigateToValidation(arguments) } - - if (validationCode != null) { - viewModel.onValidationCodeReceived(validationCode) - } - - if (captchaCode != null) { - viewModel.onCaptchaCodeReceived(captchaCode) - } + } + LaunchedEffect(validationCode) { + viewModel.onValidationCodeReceived(validationCode) + } + LaunchedEffect(captchaCode) { + viewModel.onCaptchaCodeReceived(captchaCode) } LoginScreen( @@ -134,8 +128,7 @@ fun LoginRoute( onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked, onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked, onPasswordFieldGoAction = viewModel::onSignInButtonClicked, - onSignInButtonClicked = viewModel::onSignInButtonClicked, - onLogoLongClicked = viewModel::onLogoLongClicked + onSignInButtonClicked = viewModel::onSignInButtonClicked ) HandleDialogs( @@ -153,8 +146,7 @@ fun LoginScreen( onPasswordFieldEnterKeyClicked: () -> Unit = {}, onPasswordVisibilityButtonClicked: () -> Unit = {}, onPasswordFieldGoAction: () -> Unit = {}, - onSignInButtonClicked: () -> Unit = {}, - onLogoLongClicked: () -> Unit = {} + onSignInButtonClicked: () -> Unit = {} ) { val size = LocalSizeConfig.current val focusManager = LocalFocusManager.current @@ -181,7 +173,7 @@ fun LoginScreen( enter = fadeIn(), exit = fadeOut() ) { - Logo(onLogoLongClicked = onLogoLongClicked) + Logo() } AnimatedVisibility( @@ -371,79 +363,8 @@ fun HandleDialogs( onDismissRequest = { onDismissed(loginDialog) }, title = stringResource(UiR.string.title_error), text = loginDialog.errorTextResId?.let { stringResource(it) } - ?: loginDialog.errorText.orEmpty(), - confirmText = stringResource(id = UiR.string.ok) - ) - } - - LoginDialog.FastAuth -> { - SignInAlert( - onDismissRequest = { onDismissed(loginDialog) }, - onConfirmClick = { onConfirmed(loginDialog, bundleOf("token" to it)) } - ) - } - } -} - -@Composable -fun HandleError( - onDismiss: () -> Unit, - error: LoginError?, -) { - when (error) { - null -> Unit - - LoginError.Unknown -> { - MaterialDialog( - onDismissRequest = onDismiss, - title = "Error", - text = "Unknown error", - confirmText = stringResource(id = UiR.string.ok) - ) - } - - LoginError.WrongCredentials -> { - MaterialDialog( - onDismissRequest = onDismiss, - title = "Error", - text = "Wrong login or password.", - confirmText = stringResource(id = UiR.string.ok) - ) - } - - LoginError.TooManyTries -> { - MaterialDialog( - onDismissRequest = onDismiss, - title = "Error", - text = "Too many tries. Try in another hour or later.", - confirmText = stringResource(id = UiR.string.ok) - ) - } - - - LoginError.WrongValidationCode -> { - MaterialDialog( - onDismissRequest = onDismiss, - title = "Error", - text = "Wrong validation code.", - confirmText = stringResource(id = UiR.string.ok) - ) - } - - LoginError.WrongValidationCodeFormat -> { - MaterialDialog( - onDismissRequest = onDismiss, - title = "Error", - text = "Wrong validation code format.", - confirmText = stringResource(id = UiR.string.ok) - ) - } - - is LoginError.SimpleError -> { - MaterialDialog( - onDismissRequest = onDismiss, - title = "Error", - text = error.message, + ?: loginDialog.errorText + ?: stringResource(UiR.string.unknown_error_occurred), confirmText = stringResource(id = UiR.string.ok) ) } diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt index fd328539..7bceedf4 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/Logo.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.auth.login.presentation +import android.os.Build import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateIntAsState import androidx.compose.foundation.ExperimentalFoundationApi @@ -24,21 +25,22 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.ui.R import dev.meloda.fast.ui.theme.LocalSizeConfig +import org.koin.compose.koinInject @OptIn(ExperimentalFoundationApi::class) @Composable -fun Logo( - modifier: Modifier = Modifier, - onLogoLongClicked: () -> Unit = {} -) { +fun Logo(modifier: Modifier = Modifier) { val size = LocalSizeConfig.current val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp) - val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 40 else 40) + val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 40) val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp) + val userSettings: UserSettings = koinInject() + Box( modifier = modifier .fillMaxSize() @@ -61,8 +63,14 @@ fun Logo( .combinedClickable( interactionSource = null, indication = null, - onLongClick = onLogoLongClicked, - onClick = {} + onLongClick = null, + onClick = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + userSettings.onEnableDynamicColorsChanged( + !userSettings.enableDynamicColors.value + ) + } + } ) ) diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/SignInAlert.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/SignInAlert.kt deleted file mode 100644 index e6f86aca..00000000 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/SignInAlert.kt +++ /dev/null @@ -1,51 +0,0 @@ -package dev.meloda.fast.auth.login.presentation - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import dev.meloda.fast.auth.BuildConfig -import dev.meloda.fast.ui.components.ActionInvokeDismiss -import dev.meloda.fast.ui.components.MaterialDialog - -import dev.meloda.fast.ui.R as UiR - -@Composable -fun SignInAlert( - onDismissRequest: () -> Unit = {}, - onConfirmClick: (token: String) -> Unit = {} -) { - var tokenText by rememberSaveable { - mutableStateOf(BuildConfig.debugToken) - } - - val maxWidthModifier = Modifier.fillMaxWidth() - - MaterialDialog( - onDismissRequest = onDismissRequest, - title = "Fast authorization", - confirmText = stringResource(id = UiR.string.action_authorize), - confirmAction = { onConfirmClick(tokenText) }, - cancelText = stringResource(id = UiR.string.cancel), - actionInvokeDismiss = ActionInvokeDismiss.Always - ) { - Column(modifier = maxWidthModifier) { - OutlinedTextField( - modifier = maxWidthModifier.padding(horizontal = 16.dp), - value = tokenText, - onValueChange = { tokenText = it }, - placeholder = { Text(text = "Access token") }, - label = { Text(text = "Access token") } - ) - } - } -} diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/navigation/ValidationNavigation.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/navigation/ValidationNavigation.kt index 891cbbb8..463e325f 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/navigation/ValidationNavigation.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/navigation/ValidationNavigation.kt @@ -38,7 +38,7 @@ fun NavController.navigateToValidation(arguments: ValidationArguments) { } fun NavController.setValidationResult(code: String?) { - this.currentBackStackEntry + this.previousBackStackEntry ?.savedStateHandle ?.set("validation_code", code) } diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/presentation/ValidationScreen.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/presentation/ValidationScreen.kt index ed30f9f1..0c2804c0 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/presentation/ValidationScreen.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/presentation/ValidationScreen.kt @@ -7,10 +7,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -23,6 +26,7 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -35,10 +39,13 @@ 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.autofill.ContentType import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue @@ -146,7 +153,9 @@ fun ValidationScreen( var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) } - Scaffold { padding -> + Scaffold( + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime) + ) { padding -> Column( modifier = Modifier .fillMaxSize() @@ -210,7 +219,8 @@ fun ValidationScreen( placeholder = { Text(text = "Code") }, modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)), + .clip(RoundedCornerShape(10.dp)) + .semantics { contentType = ContentType.SmsOtpCode }, leadingIcon = { Icon( painter = painterResource(id = UiR.drawable.round_qr_code_24), diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt index cf42aeb3..4da7a2e2 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/ChatMaterialsViewModel.kt @@ -17,7 +17,6 @@ import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.network.VkErrorCode import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update interface ChatMaterialsViewModel { val screenState: StateFlow @@ -54,7 +53,7 @@ class ChatMaterialsViewModelImpl( screenState.setValue { old -> old.copy( peerId = arguments.peerId, - conversationMessageId = arguments.conversationMessageId + cmId = arguments.conversationMessageId ) } @@ -85,7 +84,7 @@ class ChatMaterialsViewModelImpl( count = LOAD_COUNT, offset = offset, attachmentTypes = listOf(materialType.toString()), - conversationMessageId = screenState.value.conversationMessageId + cmId = screenState.value.cmId ).listenValue(viewModelScope) { state -> state.processState( error = ::handleError, @@ -100,11 +99,11 @@ class ChatMaterialsViewModelImpl( val newState = screenState.value.copy( isPaginationExhausted = paginationExhausted, - conversationMessageId = if (loadedMaterials.size + offset > 200) { + cmId = if (loadedMaterials.size + offset > 200) { currentOffset.setValue { 0 } loadedMaterials.lastOrNull()?.conversationMessageId ?: -1 } else { - screenState.value.conversationMessageId + screenState.value.cmId } ) diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/ChatMaterialsScreenState.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/ChatMaterialsScreenState.kt index 3d8ee96d..7e18500a 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/ChatMaterialsScreenState.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/ChatMaterialsScreenState.kt @@ -9,8 +9,8 @@ data class ChatMaterialsScreenState( val attachmentType: String, val isPaginating: Boolean, val isPaginationExhausted: Boolean, - val peerId: Int, - val conversationMessageId: Int + val peerId: Long, + val cmId: Long ) { companion object { @@ -21,7 +21,7 @@ data class ChatMaterialsScreenState( isPaginating = false, isPaginationExhausted = false, peerId = -1, - conversationMessageId = -1 + cmId = -1 ) } } diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt index 8238eaee..f63b8b6d 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/model/UiChatMaterial.kt @@ -1,16 +1,16 @@ package dev.meloda.fast.chatmaterials.model sealed class UiChatMaterial( - open val conversationMessageId: Int + open val conversationMessageId: Long ) { data class Photo( - override val conversationMessageId: Int, + override val conversationMessageId: Long, val previewUrl: String ) : UiChatMaterial(conversationMessageId) data class Video( - override val conversationMessageId: Int, + override val conversationMessageId: Long, val previewUrl: String?, val title: String, val views: Int, @@ -18,7 +18,7 @@ sealed class UiChatMaterial( ) : UiChatMaterial(conversationMessageId) data class Audio( - override val conversationMessageId: Int, + override val conversationMessageId: Long, val previewUrl: String?, val title: String, val artist: String, @@ -26,7 +26,7 @@ sealed class UiChatMaterial( ) : UiChatMaterial(conversationMessageId) data class File( - override val conversationMessageId: Int, + override val conversationMessageId: Long, val previewUrl: String?, val title: String, val size: String, @@ -34,7 +34,7 @@ sealed class UiChatMaterial( ) : UiChatMaterial(conversationMessageId) data class Link( - override val conversationMessageId: Int, + override val conversationMessageId: Long, val previewUrl: String?, val title: String?, val url: String, diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt index d16700eb..1d6c5033 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/navigation/ChatMaterialsNavigation.kt @@ -10,8 +10,8 @@ import kotlinx.serialization.Serializable @Serializable data class ChatMaterials( - val peerId: Int, - val conversationMessageId: Int + val peerId: Long, + val conversationMessageId: Long ) { companion object { fun from(savedStateHandle: SavedStateHandle) = @@ -31,7 +31,7 @@ fun NavGraphBuilder.chatMaterialsScreen( } } -fun NavController.navigateToChatMaterials(peerId: Int, conversationMessageId: Int) { +fun NavController.navigateToChatMaterials(peerId: Long, conversationMessageId: Long) { this.navigate( ChatMaterials( peerId = peerId, diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt index c5aa4740..74155846 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt @@ -53,9 +53,9 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import kotlinx.coroutines.Dispatchers @@ -102,23 +102,7 @@ fun AudioMaterialsScreen( 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 - ) - } - } + VkErrorView(baseError = baseError) } screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt index 9bc7d3aa..1d477c23 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt @@ -63,9 +63,9 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import kotlinx.coroutines.Dispatchers @@ -111,23 +111,7 @@ fun FileMaterialsScreen( when { baseError != null -> { - when (baseError) { - is BaseError.SessionExpired -> { - ErrorView( - text = stringResource(R.string.session_expired), - buttonText = stringResource(R.string.action_log_out), - onButtonClick = onSessionExpiredLogOutButtonClicked - ) - } - - is BaseError.SimpleError -> { - ErrorView( - text = baseError.message, - buttonText = stringResource(R.string.try_again), - onButtonClick = onRefresh - ) - } - } + VkErrorView(baseError = baseError) } screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt index 317b6fdb..05e193e1 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt @@ -63,9 +63,9 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import kotlinx.coroutines.Dispatchers @@ -111,23 +111,7 @@ fun LinkMaterialsScreen( when { baseError != null -> { - when (baseError) { - is BaseError.SessionExpired -> { - ErrorView( - text = stringResource(R.string.session_expired), - buttonText = stringResource(R.string.action_log_out), - onButtonClick = onSessionExpiredLogOutButtonClicked - ) - } - - is BaseError.SimpleError -> { - ErrorView( - text = baseError.message, - buttonText = stringResource(R.string.try_again), - onButtonClick = onRefresh - ) - } - } + VkErrorView(baseError = baseError) } screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt index 1b06e880..db04ffb5 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt @@ -46,9 +46,9 @@ import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState import dev.meloda.fast.chatmaterials.model.UiChatMaterial import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R -import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import kotlinx.coroutines.Dispatchers @@ -95,23 +95,7 @@ fun PhotoMaterialsScreen( when { baseError != null -> { - when (baseError) { - is BaseError.SessionExpired -> { - ErrorView( - text = stringResource(R.string.session_expired), - buttonText = stringResource(R.string.action_log_out), - onButtonClick = onSessionExpiredLogOutButtonClicked - ) - } - - is BaseError.SimpleError -> { - ErrorView( - text = baseError.message, - buttonText = stringResource(R.string.try_again), - onButtonClick = onRefresh - ) - } - } + VkErrorView(baseError = baseError) } screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt index 58b5d651..acd85816 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt @@ -56,9 +56,9 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import kotlinx.coroutines.Dispatchers @@ -104,23 +104,7 @@ fun VideoMaterialsScreen( when { baseError != null -> { - when (baseError) { - is BaseError.SessionExpired -> { - ErrorView( - text = stringResource(R.string.session_expired), - buttonText = stringResource(R.string.action_log_out), - onButtonClick = onSessionExpiredLogOutButtonClicked - ) - } - - is BaseError.SimpleError -> { - ErrorView( - text = baseError.message, - buttonText = stringResource(R.string.try_again), - onButtonClick = onRefresh - ) - } - } + VkErrorView(baseError = baseError) } screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() 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 d6b6cf2f..c2527088 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 @@ -2,19 +2,26 @@ package dev.meloda.fast.conversations import android.content.Context import android.content.res.Resources +import android.os.Bundle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import coil.ImageLoader import coil.request.ImageRequest import com.conena.nanokt.collections.indexOfFirstOrNull +import dev.meloda.fast.common.VkConstants 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.common.extensions.updateValue +import dev.meloda.fast.conversations.model.ConversationDialog +import dev.meloda.fast.conversations.model.ConversationNavigation import dev.meloda.fast.conversations.model.ConversationsScreenState +import dev.meloda.fast.conversations.model.InteractionJob +import dev.meloda.fast.conversations.model.NewInteractionException import dev.meloda.fast.conversations.util.asPresentation import dev.meloda.fast.conversations.util.extractAvatar -import dev.meloda.fast.data.State +import dev.meloda.fast.data.VkUtils import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.ConversationsUseCase @@ -22,15 +29,13 @@ import dev.meloda.fast.domain.LoadConversationsByIdUseCase import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.model.BaseError +import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.LongPollParsedEvent 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 dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -38,38 +43,49 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlin.coroutines.cancellation.CancellationException interface ConversationsViewModel { val screenState: StateFlow + val navigation: StateFlow + val dialog: StateFlow + + val conversations: StateFlow> + val uiConversations: StateFlow> + val baseError: StateFlow + val currentOffset: StateFlow val canPaginate: StateFlow + fun onNavigationConsumed() + + fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) + fun onDialogDismissed(dialog: ConversationDialog) + fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) + + fun onErrorButtonClicked() + fun onPaginationConditionsMet() - fun onDeleteDialogDismissed() - fun onDeleteDialogPositiveClick() + fun onOptionClicked(conversation: UiConversation, option: ConversationOption) fun onRefresh() - fun onConversationItemClick() + fun onConversationItemClick(conversation: UiConversation) fun onConversationItemLongClick(conversation: UiConversation) - fun onPinDialogDismissed() - fun onPinDialogPositiveClick() - - fun onOptionClicked(conversation: UiConversation, option: ConversationOption) - fun onErrorConsumed() fun setScrollIndex(index: Int) fun setScrollOffset(offset: Int) + + fun onCreateChatButtonClicked() } class ConversationsViewModelImpl( - updatesParser: LongPollUpdatesParser, + private val filter: ConversationsFilter, + private val updatesParser: LongPollUpdatesParser, private val conversationsUseCase: ConversationsUseCase, private val messagesUseCase: MessagesUseCase, private val resources: Resources, @@ -80,25 +96,31 @@ class ConversationsViewModelImpl( ) : ConversationsViewModel, ViewModel() { override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY) - override val baseError = MutableStateFlow(null) - override val currentOffset = MutableStateFlow(0) - override val canPaginate = MutableStateFlow(false) + override val navigation = MutableStateFlow(null) + override val dialog = MutableStateFlow(null) - private val useContactNames: Boolean get() = userSettings.useContactNames.value - - override fun onPaginationConditionsMet() { - currentOffset.update { screenState.value.conversations.size } - loadConversations() - } - - private val conversations = MutableStateFlow>(emptyList()) + override val conversations = MutableStateFlow>(emptyList()) + override val uiConversations = MutableStateFlow>(emptyList()) private val pinnedConversationsCount = conversations.map { conversations -> conversations.count(VkConversation::isPinned) }.stateIn(viewModelScope, SharingStarted.Eagerly, 0) + override val baseError = MutableStateFlow(null) + + override val currentOffset = MutableStateFlow(0) + override val canPaginate = MutableStateFlow(false) + + private val expandedConversationId = MutableStateFlow(0L) + + private val useContactNames: Boolean get() = userSettings.useContactNames.value + + private val interactionsTimers = hashMapOf() + init { - userSettings.useContactNames.listenValue(viewModelScope, ::updateConversationsNames) + screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) } + + loadConversations() updatesParser.onNewMessage(::handleNewMessage) updatesParser.onMessageEdited(::handleEditedMessage) @@ -108,86 +130,94 @@ class ConversationsViewModelImpl( updatesParser.onChatMajorChanged(::handleChatMajorChanged) updatesParser.onChatMinorChanged(::handleChatMinorChanged) updatesParser.onChatCleared(::handleChatClearing) + updatesParser.onChatArchived(::handleChatArchived) + userSettings.useContactNames.listenValue(viewModelScope) { + syncUiConversation() + } + } + + override fun onNavigationConsumed() { + navigation.setValue { null } + } + + override fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) { + onDialogDismissed(dialog) + + when (dialog) { + is ConversationDialog.ConversationDelete -> { + deleteConversation(dialog.conversationId) + } + + is ConversationDialog.ConversationPin -> { + pinConversation(dialog.conversationId, true) + } + + is ConversationDialog.ConversationUnpin -> { + pinConversation(dialog.conversationId, false) + } + + is ConversationDialog.ConversationArchive -> { + archiveConversation(dialog.conversationId, true) + } + + is ConversationDialog.ConversationUnarchive -> { + archiveConversation(dialog.conversationId, false) + } + } + + expandedConversationId.setValue { 0 } + syncUiConversation() + } + + override fun onDialogDismissed(dialog: ConversationDialog) { + this.dialog.setValue { null } + } + + override fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) { + when (dialog) { + is ConversationDialog.ConversationDelete -> Unit + is ConversationDialog.ConversationPin -> Unit + is ConversationDialog.ConversationUnpin -> Unit + is ConversationDialog.ConversationArchive -> Unit + is ConversationDialog.ConversationUnarchive -> Unit + } + } + + override fun onErrorButtonClicked() { + when (baseError.value) { + null -> Unit + + is BaseError.ConnectionError, + is BaseError.InternalError, + is BaseError.SimpleError, + is BaseError.UnknownError -> onRefresh() + + else -> Unit + } + } + + override fun onPaginationConditionsMet() { + currentOffset.update { conversations.value.size } loadConversations() } - override fun onDeleteDialogDismissed() { - emitShowOptions { old -> old.copy(showDeleteDialog = null) } - } - - override fun onDeleteDialogPositiveClick() { - val conversationId = screenState.value.showOptions.showDeleteDialog ?: return - deleteConversation(conversationId) - hideOptions(conversationId) - onDeleteDialogDismissed() - } - override fun onRefresh() { onErrorConsumed() loadConversations(offset = 0) } - override fun onConversationItemClick() { - screenState.setValue { old -> - old.copy( - conversations = old.conversations.map { item -> - item.copy(isExpanded = false) - } - ) - } + override fun onConversationItemClick(conversation: UiConversation) { + collapseConversations() + navigation.setValue { ConversationNavigation.MessagesHistory(peerId = conversation.id) } } override fun onConversationItemLongClick(conversation: UiConversation) { - val options = mutableListOf() - if (!conversation.isExpanded) { - conversation.lastMessage?.run { - if (conversation.isUnread && !this.isOut) { - options += ConversationOption.MarkAsRead - } - } - - val conversationsSize = screenState.value.conversations.size - val pinnedCount = pinnedConversationsCount.value - - val canPinOneMoreDialog = - conversationsSize > 4 && pinnedCount < 5 && !conversation.isPinned - - if (conversation.isPinned) { - options += ConversationOption.Unpin - } else if (canPinOneMoreDialog) { - options += ConversationOption.Pin - } - - options += ConversationOption.Delete + expandedConversationId.setValue { + if (conversation.isExpanded) 0 + else conversation.id } - - screenState.setValue { old -> - old.copy( - conversations = old.conversations.map { item -> - item.copy( - isExpanded = - if (item.id == conversation.id) { - !item.isExpanded - } else { - false - }, - options = ImmutableList.copyOf(options) - ) - } - ) - } - } - - override fun onPinDialogDismissed() { - emitShowOptions { old -> old.copy(showPinDialog = null) } - } - - override fun onPinDialogPositiveClick() { - val conversation = screenState.value.showOptions.showPinDialog ?: return - pinConversation(conversation.id, !conversation.isPinned) - hideOptions(conversation.id) - onPinDialogDismissed() + syncUiConversation() } override fun onOptionClicked( @@ -196,9 +226,7 @@ class ConversationsViewModelImpl( ) { when (option) { ConversationOption.Delete -> { - emitShowOptions { old -> - old.copy(showDeleteDialog = conversation.id) - } + dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) } } ConversationOption.MarkAsRead -> { @@ -207,13 +235,24 @@ class ConversationsViewModelImpl( peerId = conversation.id, startMessageId = lastMessageId ) - hideOptions(conversation.id) + collapseConversations() } } - ConversationOption.Pin, + ConversationOption.Pin -> { + dialog.setValue { ConversationDialog.ConversationPin(conversation.id) } + } + ConversationOption.Unpin -> { - emitShowOptions { old -> old.copy(showPinDialog = conversation) } + dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) } + } + + ConversationOption.Archive -> { + dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) } + } + + ConversationOption.Unarchive -> { + dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) } } } } @@ -230,124 +269,74 @@ class ConversationsViewModelImpl( screenState.setValue { old -> old.copy(scrollOffset = offset) } } - private fun hideOptions(conversationId: Int) { - screenState.setValue { old -> - old.copy( - conversations = old.conversations.map { item -> - if (item.id == conversationId) { - item.copy(isExpanded = false) - } else item - } - ) - } + override fun onCreateChatButtonClicked() { + navigation.setValue { ConversationNavigation.CreateChat } } - private fun emitShowOptions(function: (ConversationsShowOptions) -> ConversationsShowOptions) { - val newShowOptions = function.invoke(screenState.value.showOptions) - screenState.setValue { old -> old.copy(showOptions = newShowOptions) } + private fun collapseConversations() { + expandedConversationId.setValue { 0 } + syncUiConversation() } private fun loadConversations( offset: Int = currentOffset.value ) { - conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset) - .listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { response -> - val itemsCountSufficient = response.size == LOAD_COUNT - canPaginate.setValue { itemsCountSufficient } + conversationsUseCase.getConversations( + count = LOAD_COUNT, + offset = offset, + filter = filter + ).listenValue(viewModelScope) { state -> + state.processState( + error = { error -> + val newBaseError = VkUtils.parseError(error) + baseError.update { newBaseError } + }, + success = { response -> + val conversations = response + val fullConversations = if (offset == 0) { + conversations + } else { + this.conversations.value.plus(conversations) + } - val paginationExhausted = !itemsCountSufficient && - screenState.value.conversations.isNotEmpty() + val itemsCountSufficient = response.size == LOAD_COUNT - val imagesToPreload = - response.mapNotNull { it.extractAvatar().extractUrl() } + val paginationExhausted = !itemsCountSufficient && + this.conversations.value.isNotEmpty() - imagesToPreload.forEach { url -> - imageLoader.enqueue( - ImageRequest.Builder(applicationContext) - .data(url) - .build() - ) - } + screenState.updateValue { + copy(isPaginationExhausted = paginationExhausted) + } - conversationsUseCase.storeConversations(response) + val imagesToPreload = + response.mapNotNull { it.extractAvatar().extractUrl() } - val loadedConversations = response.map { - it.asPresentation( - resources, - userSettings.useContactNames.value - ) - } - - val newState = screenState.value.copy( - isPaginationExhausted = paginationExhausted + imagesToPreload.forEach { url -> + imageLoader.enqueue( + ImageRequest.Builder(applicationContext) + .data(url) + .build() ) - if (offset == 0) { - conversations.emit(response) - screenState.setValue { - newState.copy(conversations = loadedConversations) - } - } else { - conversations.emit(conversations.value.plus(response)) - screenState.setValue { - newState.copy( - conversations = newState.conversations.plus(loadedConversations) - ) - } - } } + + conversationsUseCase.storeConversations(response) + + this.conversations.emit(fullConversations) + syncUiConversation() + canPaginate.setValue { itemsCountSufficient } + } + ) + + screenState.setValue { old -> + old.copy( + isLoading = offset == 0 && state.isLoading(), + isPaginating = offset > 0 && state.isLoading() ) - - screenState.setValue { old -> - old.copy( - isLoading = offset == 0 && state.isLoading(), - isPaginating = offset > 0 && state.isLoading() - ) - } } - } - - 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 } } - private fun deleteConversation(peerId: Int) { + private fun deleteConversation(peerId: Long) { conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state -> state.processState( error = {}, @@ -358,15 +347,15 @@ class ConversationsViewModelImpl( ?: return@processState newConversations.removeAt(conversationIndex) - conversations.update { newConversations } - sortConversations() + conversations.update { newConversations.sorted() } + syncUiConversation() } ) screenState.emit(screenState.value.copy(isLoading = state.isLoading())) } } - private fun pinConversation(peerId: Int, pin: Boolean) { + private fun pinConversation(peerId: Long, pin: Boolean) { conversationsUseCase.changePinState(peerId, pin) .listenValue(viewModelScope) { state -> state.processState( @@ -389,6 +378,26 @@ class ConversationsViewModelImpl( } } + private fun archiveConversation(peerId: Long, archive: Boolean) { + conversationsUseCase.changeArchivedState(peerId, archive) + .listenValue(viewModelScope) { state -> + state.processState( + error = {}, + success = { + conversations.value.find { it.id == peerId }?.let { conversation -> + handleChatArchived( + LongPollParsedEvent.ChatArchived( + conversation = conversation, + archived = archive + ) + ) + } + } + ) + } + } + + // TODO: 03-Apr-25, Danil Nikolaev: handle business messages private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message @@ -397,26 +406,31 @@ class ConversationsViewModelImpl( newConversations.indexOfFirstOrNull { it.id == message.peerId } if (conversationIndex == null) { - loadConversationsByIdUseCase(peerIds = listOf(message.peerId)) - .listenValue(viewModelScope) { state -> - state.processState( - error = {}, - success = { response -> - val conversation = (response.firstOrNull() ?: return@listenValue) - .copy(lastMessage = message) + if (event.inArchive != (filter == ConversationsFilter.ARCHIVE)) return - newConversations.add(pinnedConversationsCount.value, conversation) - conversations.update { newConversations } - sortConversations() - } - ) - } + loadConversationsByIdUseCase( + peerIds = listOf(message.peerId), + extended = true, + fields = VkConstants.ALL_FIELDS + ).listenValue(viewModelScope) { state -> + state.processState( + error = {}, + success = { response -> + val conversation = (response.firstOrNull() ?: return@listenValue) + .copy(lastMessage = message) + + newConversations.add(pinnedConversationsCount.value, conversation) + conversations.update { newConversations.sorted() } + syncUiConversation() + } + ) + } } else { val conversation = newConversations[conversationIndex] var newConversation = conversation.copy( lastMessage = message, lastMessageId = message.id, - lastConversationMessageId = -1, + lastCmId = message.cmId, unreadCount = if (message.isOut) conversation.unreadCount else conversation.unreadCount + 1 ) @@ -447,18 +461,8 @@ class ConversationsViewModelImpl( newConversations.add(toPosition, newConversation) } - conversations.update { newConversations } - - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + conversations.update { newConversations.sorted() } + syncUiConversation() } } @@ -474,20 +478,10 @@ class ConversationsViewModelImpl( newConversations[conversationIndex] = conversation.copy( lastMessage = message, lastMessageId = message.id, - lastConversationMessageId = -1 + lastCmId = message.cmId ) conversations.update { newConversations } - - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + syncUiConversation() } } @@ -502,22 +496,12 @@ class ConversationsViewModelImpl( } else { newConversations[conversationIndex] = newConversations[conversationIndex].copy( - inRead = event.messageId, + inReadCmId = event.cmId, unreadCount = event.unreadCount ) conversations.update { newConversations } - - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + syncUiConversation() } } @@ -532,144 +516,15 @@ class ConversationsViewModelImpl( } else { newConversations[conversationIndex] = newConversations[conversationIndex].copy( - outRead = event.messageId, + outReadCmId = event.cmId, unreadCount = event.unreadCount ) conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + syncUiConversation() } } - private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) { - val newConversations = conversations.value.toMutableList() - val conversationIndex = - newConversations.indexOfFirstOrNull { it.id == event.peerId } - - if (conversationIndex == null) { // диалога нет в списке - // pizdets - } else { - newConversations[conversationIndex] = - newConversations[conversationIndex].copy(majorId = event.majorId) - - conversations.setValue { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } - sortConversations() - } - } - - private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) { - val newConversations = conversations.value.toMutableList() - val conversationIndex = - newConversations.indexOfFirstOrNull { it.id == event.peerId } - - if (conversationIndex == null) { // диалога нет в списке - // pizdets - } else { - newConversations[conversationIndex] = - newConversations[conversationIndex].copy(minorId = event.minorId) - - conversations.setValue { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } - sortConversations() - } - } - - private fun sortConversations() { - val newConversations = conversations.value.toMutableList() - val pinnedConversations = newConversations - .filter(VkConversation::isPinned) - .sortedWith { c1, c2 -> - val diff = c2.majorId - c1.majorId - - if (diff == 0) { - c2.minorId - c1.minorId - } else { - diff - } - } - - newConversations.removeAll(pinnedConversations) - newConversations.sortWith { c1, c2 -> - (c2.lastMessage?.date ?: 0) - (c1.lastMessage?.date ?: 0) - } - - newConversations.addAll(0, pinnedConversations) - - conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } - } - - private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) { - val newConversations = conversations.value.toMutableList() - - val conversationIndex = newConversations.indexOfFirstOrNull { it.id == event.peerId } - - if (conversationIndex == null) { // диалога нет в списке - // pizdets - } else { - newConversations.removeAt(conversationIndex) - - conversations.setValue { newConversations } - - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } - } - } - - private val interactionsTimers = hashMapOf() - - private data class InteractionJob( - val interactionType: InteractionType, - val timerJob: Job - ) - - private class NewInteractionException : CancellationException() - private fun handleInteraction(event: LongPollParsedEvent.Interaction) { val interactionType = event.interactionType val peerId = event.peerId @@ -687,17 +542,7 @@ class ConversationsViewModelImpl( ) conversations.update { newConversations } - - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + syncUiConversation() interactionsTimers[peerId]?.let { interactionJob -> if (interactionJob.interactionType == interactionType) { @@ -708,7 +553,7 @@ class ConversationsViewModelImpl( var timeoutAction: (() -> Unit)? = null val timerJob = createTimerFlow( - time = 5, + time = 6, onTimeoutAction = { timeoutAction?.invoke() } ).launchIn(viewModelScope) @@ -725,7 +570,7 @@ class ConversationsViewModelImpl( } } - private fun stopInteraction(peerId: Int, interactionJob: InteractionJob) { + private fun stopInteraction(peerId: Long, interactionJob: InteractionJob) { interactionsTimers[peerId] ?: return val newConversations = conversations.value.toMutableList() @@ -739,22 +584,98 @@ class ConversationsViewModelImpl( ) conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + syncUiConversation() interactionJob.timerJob.cancel() interactionsTimers[peerId] = null } - private fun readConversation(peerId: Int, startMessageId: Int) { + private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) { + val newConversations = conversations.value.toMutableList() + val conversationIndex = + newConversations.indexOfFirstOrNull { it.id == event.peerId } + + if (conversationIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations[conversationIndex] = + newConversations[conversationIndex].copy(majorId = event.majorId) + + conversations.setValue { newConversations.sorted() } + syncUiConversation() + } + } + + private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) { + val newConversations = conversations.value.toMutableList() + val conversationIndex = + newConversations.indexOfFirstOrNull { it.id == event.peerId } + + if (conversationIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations[conversationIndex] = + newConversations[conversationIndex].copy(minorId = event.minorId) + + conversations.setValue { newConversations.sorted() } + syncUiConversation() + } + } + + private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) { + val newConversations = conversations.value.toMutableList() + + val conversationIndex = newConversations.indexOfFirstOrNull { it.id == event.peerId } + + if (conversationIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations.removeAt(conversationIndex) + + conversations.setValue { newConversations.sorted() } + syncUiConversation() + } + } + + private fun handleChatArchived(event: LongPollParsedEvent.ChatArchived) { + val conversation = event.conversation + + val newConversations = conversations.value.toMutableList() + + when (filter) { + ConversationsFilter.BUSINESS_NOTIFY -> Unit + + ConversationsFilter.ARCHIVE -> { + if (event.archived) { + newConversations.add(0, conversation) + } else { + val index = newConversations.indexOfFirstOrNull { it.id == conversation.id } + if (index == null) return + + newConversations.removeAt(index) + } + + conversations.update { newConversations } + syncUiConversation() + } + + else -> { + if (event.archived) { + val index = newConversations.indexOfFirstOrNull { it.id == conversation.id } + if (index == null) return + + newConversations.removeAt(index) + } else { + newConversations.add(pinnedConversationsCount.value, conversation) + } + + conversations.update { newConversations.sorted() } + syncUiConversation() + } + } + } + + private fun readConversation(peerId: Long, startMessageId: Long) { messagesUseCase.markAsRead( peerId = peerId, startMessageId = startMessageId @@ -771,36 +692,83 @@ class ConversationsViewModelImpl( newConversations[conversationIndex].copy(inRead = startMessageId) conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + syncUiConversation() } ) } } - private fun updateConversationsNames(useContactNames: Boolean) { + private fun List.sorted(): List { + val newConversations = toMutableList() + + val pinnedConversations = newConversations + .filter(VkConversation::isPinned) + .sortedWith { c1, c2 -> + val diff = c2.majorId - c1.majorId + + if (diff == 0) { + c2.minorId - c1.minorId + } else { + diff + } + } + + newConversations.removeAll(pinnedConversations) + newConversations.sortWith { c1, c2 -> + (c2.lastMessage?.date ?: 0) - (c1.lastMessage?.date ?: 0) + } + + newConversations.addAll(0, pinnedConversations) + return newConversations + } + + private fun syncUiConversation(): List { val conversations = conversations.value - if (conversations.isEmpty()) return - val uiConversations = conversations.map { conversation -> - conversation.asPresentation(resources, useContactNames) - } + val newUiConversations = conversations.map { conversation -> + val options = mutableListOf() + conversation.lastMessage?.run { + if (!conversation.isRead() && !this.isOut) { + options += ConversationOption.MarkAsRead + } + } - screenState.setValue { old -> - old.copy(conversations = uiConversations) + val conversationsSize = this.conversations.value.size + val pinnedCount = pinnedConversationsCount.value + + val canPinOneMoreDialog = + conversationsSize > 4 && pinnedCount < 5 && !conversation.isPinned() + + if (conversation.isPinned()) { + options += ConversationOption.Unpin + } else if (canPinOneMoreDialog) { + options += ConversationOption.Pin + } + + when (filter) { + ConversationsFilter.ARCHIVE -> ConversationOption.Unarchive + + ConversationsFilter.UNREAD, + ConversationsFilter.ALL -> ConversationOption.Archive + + ConversationsFilter.BUSINESS_NOTIFY -> null + }?.let(options::add) + + options += ConversationOption.Delete + + conversation.asPresentation( + resources = resources, + useContactName = useContactNames, + isExpanded = expandedConversationId.value == conversation.id, + options = options.toImmutableList() + ) } + uiConversations.setValue { newUiConversations } + + return newUiConversations } companion object { const val LOAD_COUNT = 30 } } - diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt index 15b4edf4..cb3fe4dc 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt @@ -3,12 +3,35 @@ package dev.meloda.fast.conversations.di import dev.meloda.fast.conversations.ConversationsViewModelImpl import dev.meloda.fast.domain.ConversationsUseCase import dev.meloda.fast.domain.ConversationsUseCaseImpl +import dev.meloda.fast.model.ConversationsFilter import org.koin.core.module.dsl.singleOf -import org.koin.core.module.dsl.viewModelOf +import org.koin.core.module.dsl.viewModel +import org.koin.core.qualifier.named +import org.koin.core.scope.Scope import org.koin.dsl.bind import org.koin.dsl.module val conversationsModule = module { + viewModel(named(ConversationsFilter.ALL)) { + createConversationsViewModel(ConversationsFilter.ALL) + } + viewModel(named(ConversationsFilter.ARCHIVE)) { + createConversationsViewModel(ConversationsFilter.ARCHIVE) + } + singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class - viewModelOf(::ConversationsViewModelImpl) +} + +private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModelImpl { + return ConversationsViewModelImpl( + filter = filter, + updatesParser = get(), + conversationsUseCase = get(), + messagesUseCase = get(), + resources = get(), + userSettings = get(), + imageLoader = get(), + applicationContext = get(), + loadConversationsByIdUseCase = get() + ) } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationDialog.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationDialog.kt new file mode 100644 index 00000000..779a71cc --- /dev/null +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationDialog.kt @@ -0,0 +1,12 @@ +package dev.meloda.fast.conversations.model + +import androidx.compose.runtime.Immutable + +@Immutable +sealed class ConversationDialog { + data class ConversationPin(val conversationId: Long) : ConversationDialog() + data class ConversationUnpin(val conversationId: Long) : ConversationDialog() + data class ConversationDelete(val conversationId: Long) : ConversationDialog() + data class ConversationArchive(val conversationId: Long) : ConversationDialog() + data class ConversationUnarchive(val conversationId: Long) : ConversationDialog() +} diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationNavigation.kt new file mode 100644 index 00000000..d409284e --- /dev/null +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationNavigation.kt @@ -0,0 +1,11 @@ +package dev.meloda.fast.conversations.model + +import androidx.compose.runtime.Immutable + +@Immutable +sealed class ConversationNavigation { + + data class MessagesHistory(val peerId: Long) : ConversationNavigation() + + data object CreateChat : ConversationNavigation() +} 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 da955b90..a0ab9319 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,31 +1,28 @@ 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( - val showOptions: ConversationsShowOptions, - val conversations: List, val isLoading: Boolean, val isPaginating: Boolean, val isPaginationExhausted: Boolean, val profileImageUrl: String?, val scrollIndex: Int, val scrollOffset: Int, + val isArchive: Boolean ) { companion object { val EMPTY: ConversationsScreenState = ConversationsScreenState( - showOptions = ConversationsShowOptions.EMPTY, - conversations = emptyList(), isLoading = true, isPaginating = false, isPaginationExhausted = false, profileImageUrl = null, scrollIndex = 0, scrollOffset = 0, + isArchive = false ) } } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/InteractionJob.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/InteractionJob.kt new file mode 100644 index 00000000..b0b6f1ac --- /dev/null +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/InteractionJob.kt @@ -0,0 +1,9 @@ +package dev.meloda.fast.conversations.model + +import dev.meloda.fast.model.InteractionType +import kotlinx.coroutines.Job + +data class InteractionJob( + val interactionType: InteractionType, + val timerJob: Job +) diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/NewInteractionException.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/NewInteractionException.kt new file mode 100644 index 00000000..ca01ea53 --- /dev/null +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/NewInteractionException.kt @@ -0,0 +1,5 @@ +package dev.meloda.fast.conversations.model + +import kotlinx.coroutines.CancellationException + +class NewInteractionException : CancellationException() 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 de39e945..435eccc7 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 @@ -1,35 +1,73 @@ package dev.meloda.fast.conversations.navigation -import androidx.navigation.NavController +import androidx.activity.compose.LocalActivity +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import dev.meloda.fast.conversations.ConversationsViewModel +import androidx.navigation.navigation import dev.meloda.fast.conversations.ConversationsViewModelImpl import dev.meloda.fast.conversations.presentation.ConversationsRoute import dev.meloda.fast.model.BaseError -import dev.meloda.fast.ui.extensions.sharedViewModel +import dev.meloda.fast.model.ConversationsFilter +import dev.meloda.fast.ui.theme.LocalNavController +import dev.meloda.fast.ui.theme.getOrThrow import kotlinx.serialization.Serializable +import org.koin.androidx.compose.koinViewModel +import org.koin.core.qualifier.named + +@Serializable +object ConversationsGraph @Serializable object Conversations -fun NavGraphBuilder.conversationsScreen( - onError: (BaseError) -> Unit, - onConversationItemClicked: (id: Int) -> Unit, - onCreateChatClicked: () -> Unit, - onScrolledToTop: () -> Unit, - navController: NavController, -) { - composable { - val viewModel: ConversationsViewModel = - it.sharedViewModel(navController = navController) +@Serializable +object Archive - ConversationsRoute( - onError = onError, - onConversationItemClicked = onConversationItemClicked, - onCreateChatButtonClicked = onCreateChatClicked, - onScrolledToTop = onScrolledToTop, - viewModel = viewModel - ) +fun NavGraphBuilder.conversationsGraph( + onError: (BaseError) -> Unit, + onNavigateToMessagesHistory: (id: Long) -> Unit, + onNavigateToCreateChat: () -> Unit, + onScrolledToTop: () -> Unit +) { + navigation( + startDestination = Conversations + ) { + composable { + val context = LocalContext.current + val navController = LocalNavController.getOrThrow() + + val viewModel: ConversationsViewModelImpl = koinViewModel( + qualifier = named(ConversationsFilter.ALL), + viewModelStoreOwner = context as AppCompatActivity + ) + + ConversationsRoute( + viewModel = viewModel, + onError = onError, + onNavigateToMessagesHistory = onNavigateToMessagesHistory, + onNavigateToCreateChat = onNavigateToCreateChat, + onNavigateToArchive = { navController.navigate(Archive) }, + onScrolledToTop = onScrolledToTop + ) + } + composable { + val context = LocalContext.current + val navController = LocalNavController.getOrThrow() + + val viewModel: ConversationsViewModelImpl = koinViewModel( + qualifier = named(ConversationsFilter.ARCHIVE), + viewModelStoreOwner = context as AppCompatActivity + ) + + ConversationsRoute( + viewModel = viewModel, + onBack = navController::navigateUp, + onError = onError, + onNavigateToMessagesHistory = onNavigateToMessagesHistory, + onScrolledToTop = onScrolledToTop + ) + } } } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationDialogs.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationDialogs.kt new file mode 100644 index 00000000..882fff06 --- /dev/null +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationDialogs.kt @@ -0,0 +1,74 @@ +package dev.meloda.fast.conversations.presentation + +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.core.os.bundleOf +import dev.meloda.fast.conversations.model.ConversationDialog +import dev.meloda.fast.conversations.model.ConversationsScreenState +import dev.meloda.fast.ui.components.MaterialDialog + +import dev.meloda.fast.ui.R as UiR + +@Composable +fun HandleDialogs( + screenState: ConversationsScreenState, + dialog: ConversationDialog?, + onConfirmed: (ConversationDialog, Bundle) -> Unit = { _, _ -> }, + onDismissed: (ConversationDialog) -> Unit = {}, + onItemPicked: (ConversationDialog, Bundle) -> Unit = { _, _ -> } +) { + when (dialog) { + null -> Unit + + is ConversationDialog.ConversationArchive -> { + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = stringResource(id = UiR.string.confirm_archive_conversation), + confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmText = stringResource(id = UiR.string.action_archive), + cancelText = stringResource(id = UiR.string.cancel) + ) + } + + is ConversationDialog.ConversationUnarchive -> { + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = stringResource(id = UiR.string.confirm_unarchive_conversation), + confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmText = stringResource(id = UiR.string.action_unarchive), + cancelText = stringResource(id = UiR.string.cancel) + ) + } + + is ConversationDialog.ConversationDelete -> { + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = stringResource(id = UiR.string.confirm_delete_conversation), + confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmText = stringResource(id = UiR.string.action_delete), + cancelText = stringResource(id = UiR.string.cancel) + ) + } + + is ConversationDialog.ConversationPin -> { + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = stringResource(id = UiR.string.confirm_pin_conversation), + confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmText = stringResource(id = UiR.string.action_pin), + cancelText = stringResource(id = UiR.string.cancel) + ) + } + + is ConversationDialog.ConversationUnpin -> { + MaterialDialog( + onDismissRequest = { onDismissed(dialog) }, + title = stringResource(id = UiR.string.confirm_unpin_conversation), + confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmText = stringResource(id = UiR.string.action_unpin), + cancelText = stringResource(id = UiR.string.cancel) + ) + } + } +} 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 81ee7f02..d8bd35bc 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 @@ -7,8 +7,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -20,7 +18,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ElevatedAssistChip @@ -61,7 +60,7 @@ val BirthdayColor = Color(0xffb00b69) @OptIn(ExperimentalFoundationApi::class) @Composable fun ConversationItem( - onItemClick: (Int) -> Unit, + onItemClick: (UiConversation) -> Unit, onItemLongClick: (conversation: UiConversation) -> Unit, onOptionClicked: (UiConversation, ConversationOption) -> Unit, maxLines: Int, @@ -80,7 +79,7 @@ fun ConversationItem( modifier = modifier .fillMaxWidth() .combinedClickable( - onClick = { onItemClick(conversation.id) }, + onClick = { onItemClick(conversation) }, onLongClick = { onItemLongClick(conversation) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) @@ -241,7 +240,7 @@ fun ConversationItem( text = conversation.title, minLines = 1, maxLines = maxLines, - style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp) + style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp), ) Row { @@ -329,9 +328,13 @@ fun ConversationItem( Box( modifier = Modifier .clip(CircleShape) - .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) + .defaultMinSize( + minWidth = 20.dp, + minHeight = 20.dp + ) .background(MaterialTheme.colorScheme.primary) .align(Alignment.CenterHorizontally) + .padding(horizontal = if (count.length > 1) 2.dp else 0.dp) ) { Text( modifier = Modifier @@ -352,18 +355,19 @@ fun ConversationItem( Column( modifier = Modifier .fillMaxWidth() + .height(60.dp) .padding(start = 8.dp) ) { Spacer(modifier = Modifier.height(12.dp)) HorizontalDivider() + Spacer(modifier = Modifier.height(4.dp)) - Row( + LazyRow( modifier = Modifier - .horizontalScroll(rememberScrollState()) - .padding(horizontal = 10.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp) + .fillMaxWidth() + .padding(horizontal = 10.dp) ) { - conversation.options.forEach { option -> + items(conversation.options.toList()) { option -> ElevatedAssistChip( onClick = { onOptionClicked(conversation, option) }, leadingIcon = { @@ -379,6 +383,7 @@ fun ConversationItem( Text(text = option.title.getString().orEmpty()) } ) + Spacer(Modifier.width(8.dp)) } } } @@ -393,5 +398,3 @@ fun ConversationItem( } } } - - 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 f3d302bb..3e15067e 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 @@ -28,13 +28,15 @@ 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.LocalThemeConfig +import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun ConversationsList( modifier: Modifier = Modifier, - onConversationsClick: (Int) -> Unit, + conversations: ImmutableList, + onConversationsClick: (UiConversation) -> Unit, onConversationsLongClick: (UiConversation) -> Unit, screenState: ConversationsScreenState, state: LazyListState, @@ -54,7 +56,7 @@ fun ConversationsList( Spacer(modifier = Modifier.height(8.dp)) } items( - items = screenState.conversations, + items = conversations.values, key = UiConversation::id, ) { conversation -> val isUserAccount by remember(conversation) { diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsRoute.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsRoute.kt new file mode 100644 index 00000000..076a0e9b --- /dev/null +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsRoute.kt @@ -0,0 +1,80 @@ +package dev.meloda.fast.conversations.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.meloda.fast.conversations.ConversationsViewModel +import dev.meloda.fast.conversations.model.ConversationNavigation +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList + +@Composable +fun ConversationsRoute( + viewModel: ConversationsViewModel, + onBack: (() -> Unit)? = null, + onError: (BaseError) -> Unit, + onNavigateToMessagesHistory: (conversationId: Long) -> Unit, + onNavigateToCreateChat: (() -> Unit)? = null, + onNavigateToArchive: (() -> Unit)? = null, + onScrolledToTop: () -> Unit, +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val navigationEvent by viewModel.navigation.collectAsStateWithLifecycle() + val conversations by viewModel.uiConversations.collectAsStateWithLifecycle() + val dialog by viewModel.dialog.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + + LaunchedEffect(navigationEvent) { + val shouldBeConsumed: Boolean = when (val navigation = navigationEvent) { + null -> false + + is ConversationNavigation.CreateChat -> { + onNavigateToCreateChat?.invoke() + true + } + + is ConversationNavigation.MessagesHistory -> { + onNavigateToMessagesHistory(navigation.peerId) + true + } + } + + if (shouldBeConsumed) viewModel.onNavigationConsumed() + } + + ConversationsScreen( + onBack = { onBack?.invoke() }, + screenState = screenState, + conversations = conversations.toImmutableList(), + baseError = baseError, + canPaginate = canPaginate, + onConversationItemClicked = viewModel::onConversationItemClick, + onConversationItemLongClicked = viewModel::onConversationItemLongClick, + onOptionClicked = viewModel::onOptionClicked, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet, + onRefreshDropdownItemClicked = viewModel::onRefresh, + onRefresh = viewModel::onRefresh, + onCreateChatButtonClicked = viewModel::onCreateChatButtonClicked, + onArchiveActionClicked = { onNavigateToArchive?.invoke() }, + setScrollIndex = viewModel::setScrollIndex, + setScrollOffset = viewModel::setScrollOffset, + onConsumeReselection = onScrolledToTop, + onErrorViewButtonClicked = { + if (baseError in listOf(BaseError.AccountBlocked, BaseError.SessionExpired)) { + onError(requireNotNull(baseError)) + } else { + viewModel.onErrorButtonClicked() + } + } + ) + + HandleDialogs( + screenState = screenState, + dialog = dialog, + onConfirmed = viewModel::onDialogConfirmed, + onDismissed = viewModel::onDialogDismissed, + onItemPicked = viewModel::onDialogItemPicked + ) +} 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 92c31bcd..22a11715 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 @@ -19,7 +19,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -56,68 +57,30 @@ 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.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.ConversationsScreenState import dev.meloda.fast.conversations.navigation.Conversations 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.components.VkErrorView 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.LocalScrollToTop +import dev.meloda.fast.ui.theme.LocalReselectedTab import dev.meloda.fast.ui.theme.LocalThemeConfig +import dev.meloda.fast.ui.util.ImmutableList +import dev.meloda.fast.ui.util.emptyImmutableList import dev.meloda.fast.ui.util.isScrollingUp import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce + import dev.meloda.fast.ui.R as UiR -@Composable -fun ConversationsRoute( - onError: (BaseError) -> Unit, - onConversationItemClicked: (conversationId: Int) -> Unit, - onCreateChatButtonClicked: () -> Unit, - onScrolledToTop: () -> Unit, - viewModel: ConversationsViewModel -) { - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - - ConversationsScreen( - screenState = screenState, - baseError = baseError, - canPaginate = canPaginate, - onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, - onConversationItemClicked = { id -> - onConversationItemClicked(id) - viewModel.onConversationItemClick() - }, - onConversationItemLongClicked = viewModel::onConversationItemLongClick, - onOptionClicked = viewModel::onOptionClicked, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet, - onRefreshDropdownItemClicked = viewModel::onRefresh, - onRefresh = viewModel::onRefresh, - onCreateChatButtonClicked = onCreateChatButtonClicked, - setScrollIndex = viewModel::setScrollIndex, - setScrollOffset = viewModel::setScrollOffset, - onScrolledToTop = onScrolledToTop - ) - - HandleDialogs( - screenState = screenState, - viewModel = viewModel - ) -} - @OptIn( ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, @@ -125,19 +88,22 @@ fun ConversationsRoute( @Composable fun ConversationsScreen( screenState: ConversationsScreenState = ConversationsScreenState.EMPTY, + conversations: ImmutableList = emptyImmutableList(), baseError: BaseError? = null, canPaginate: Boolean = false, - onSessionExpiredLogOutButtonClicked: () -> Unit = {}, - onConversationItemClicked: (conversationId: Int) -> Unit = {}, + onBack: () -> Unit = {}, + onConversationItemClicked: (conversation: UiConversation) -> Unit = {}, onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {}, onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> }, onPaginationConditionsMet: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {}, onRefresh: () -> Unit = {}, onCreateChatButtonClicked: () -> Unit = {}, + onArchiveActionClicked: () -> Unit = {}, setScrollIndex: (Int) -> Unit = {}, setScrollOffset: (Int) -> Unit = {}, - onScrolledToTop: () -> Unit = {} + onConsumeReselection: () -> Unit = {}, + onErrorViewButtonClicked: () -> Unit = {} ) { val currentTheme = LocalThemeConfig.current @@ -150,14 +116,18 @@ fun ConversationsScreen( initialFirstVisibleItemScrollOffset = screenState.scrollOffset ) - val scrollToTop = LocalScrollToTop.current[Conversations] ?: false - LaunchedEffect(scrollToTop) { - if (scrollToTop) { - if (listState.firstVisibleItemIndex > 14) { - listState.scrollToItem(14) + val currentTabReselected = LocalReselectedTab.current[Conversations] ?: false + LaunchedEffect(currentTabReselected) { + if (currentTabReselected) { + if (screenState.isArchive) { + onBack.invoke() + } else { + if (listState.firstVisibleItemIndex > 14) { + listState.scrollToItem(14) + } + listState.animateScrollToItem(0) + onConsumeReselection() } - listState.animateScrollToItem(0) - onScrolledToTop() } } @@ -218,22 +188,40 @@ fun ConversationsScreen( title = { Text( text = stringResource( - id = if (screenState.isLoading) UiR.string.title_loading - else UiR.string.title_conversations + id = when { + screenState.isLoading -> UiR.string.title_loading + screenState.isArchive -> UiR.string.title_archive + else -> UiR.string.title_conversations + } ), maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.headlineSmall ) }, - actions = { - IconButton( - onClick = { - dropDownMenuExpanded = true + navigationIcon = { + if (screenState.isArchive) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null + ) } - ) { + } + }, + actions = { + if (!screenState.isArchive) { + IconButton(onClick = onArchiveActionClicked) { + Icon( + painter = painterResource(UiR.drawable.outline_archive_24), + contentDescription = null + ) + } + } + + IconButton(onClick = { dropDownMenuExpanded = true }) { Icon( - imageVector = Icons.Default.MoreVert, + imageVector = Icons.Rounded.MoreVert, contentDescription = null ) } @@ -279,7 +267,7 @@ fun ConversationsScreen( ) val showHorizontalProgressBar by remember(screenState) { - derivedStateOf { screenState.isLoading && screenState.conversations.isNotEmpty() } + derivedStateOf { screenState.isLoading && conversations.isNotEmpty() } } AnimatedVisibility(showHorizontalProgressBar) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) @@ -290,49 +278,38 @@ fun ConversationsScreen( } }, floatingActionButton = { - val offsetY by animateIntAsState( - targetValue = if (listState.isScrollingUp()) 0 else 600 - ) + if (!screenState.isArchive) { + val offsetY by animateIntAsState( + targetValue = if (listState.isScrollingUp()) 0 else 600 + ) - Column { - FloatingActionButton( - onClick = onCreateChatButtonClicked, - modifier = Modifier.offset { - IntOffset(0, offsetY) + Column { + FloatingActionButton( + onClick = onCreateChatButtonClicked, + modifier = Modifier.offset { + IntOffset(0, offsetY) + } + ) { + Icon( + painter = painterResource(id = UiR.drawable.round_create_24), + contentDescription = "Add chat button" + ) } - ) { - Icon( - painter = painterResource(id = UiR.drawable.round_create_24), - contentDescription = "Add chat button" - ) - } - Spacer(modifier = Modifier.height(LocalBottomPadding.current)) + Spacer(modifier = Modifier.height(LocalBottomPadding.current)) + } } } ) { 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 - ) - } - } + VkErrorView( + baseError = baseError, + onButtonClick = onErrorViewButtonClicked + ) } - screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader() + screenState.isLoading && conversations.isEmpty() -> FullScreenLoader() else -> { val pullToRefreshState = rememberPullToRefreshState() @@ -357,6 +334,7 @@ fun ConversationsScreen( } ) { ConversationsList( + conversations = conversations, onConversationsClick = onConversationItemClicked, onConversationsLongClick = onConversationItemLongClicked, screenState = screenState, @@ -371,7 +349,7 @@ fun ConversationsScreen( padding = padding ) - if (screenState.conversations.isEmpty()) { + if (conversations.isEmpty()) { NoItemsView( buttonText = stringResource(UiR.string.action_refresh), onButtonClick = onRefresh @@ -382,38 +360,3 @@ fun ConversationsScreen( } } } - -// TODO: 26.08.2023, Danil Nikolaev: remove usage of viewModel -@Composable -fun HandleDialogs( - screenState: ConversationsScreenState, - viewModel: ConversationsViewModel -) { - val showOptions = screenState.showOptions - - if (showOptions.showDeleteDialog != null) { - MaterialDialog( - onDismissRequest = viewModel::onDeleteDialogDismissed, - title = stringResource(id = UiR.string.confirm_delete_conversation), - confirmAction = viewModel::onDeleteDialogPositiveClick, - confirmText = stringResource(id = UiR.string.action_delete), - cancelText = stringResource(id = UiR.string.cancel) - ) - } - - showOptions.showPinDialog?.let { conversation -> - MaterialDialog( - onDismissRequest = viewModel::onPinDialogDismissed, - title = stringResource( - id = if (conversation.isPinned) UiR.string.confirm_unpin_conversation - else UiR.string.confirm_pin_conversation - ), - confirmAction = viewModel::onPinDialogPositiveClick, - confirmText = stringResource( - id = if (conversation.isPinned) UiR.string.action_unpin - else UiR.string.action_pin - ), - cancelText = stringResource(id = UiR.string.cancel) - ) - } -} 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 56634b9d..baca6a66 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 @@ -23,8 +23,10 @@ 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.ConversationOption import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.util.ImmutableList +import dev.meloda.fast.ui.util.emptyImmutableList import java.util.Calendar import java.util.Locale import kotlin.math.ln @@ -33,7 +35,9 @@ import dev.meloda.fast.ui.R as UiR fun VkConversation.asPresentation( resources: Resources, - useContactName: Boolean + useContactName: Boolean, + isExpanded: Boolean = false, + options: ImmutableList = emptyImmutableList() ): UiConversation = UiConversation( id = id, lastMessageId = lastMessageId, @@ -47,14 +51,15 @@ fun VkConversation.asPresentation( isPinned = majorId > 0, actionImageId = ActionState.parse(isPhantom, isCallInProgress).getResourceId(), isBirthday = extractBirthday(this), - isUnread = extractReadCondition(this, lastMessage), + isUnread = !isRead(), isAccount = isAccount(id), isOnline = !isAccount(id) && user?.onlineStatus?.isOnline() == true, lastMessage = lastMessage, peerType = peerType, interactionText = extractInteractionText(resources, this), - isExpanded = false, - options = ImmutableList.empty() + isExpanded = isExpanded, + isArchived = isArchived, + options = options ) fun VkConversation.extractAvatar() = when (peerType) { @@ -101,7 +106,7 @@ private fun extractUnreadCount( lastMessage: VkMessage?, conversation: VkConversation ): String? = when { - lastMessage?.isOut == false && !conversation.isInUnread() -> null + lastMessage?.isOut == false && conversation.isInRead() -> null conversation.unreadCount == 0 -> null conversation.unreadCount < 1000 -> conversation.unreadCount.toString() else -> { @@ -121,7 +126,7 @@ private fun extractUnreadCount( private fun extractMessage( resources: Resources, lastMessage: VkMessage?, - peerId: Int, + peerId: Long, peerType: PeerType ): AnnotatedString { val youPrefix = UiText.Resource(UiR.string.you_message_prefix) @@ -210,7 +215,12 @@ private fun extractMessage( .replace("
", " ") .replace("–", "-") .trim() - .let { text -> getTextWithVisualizedMentions(text, Color.Red) } + .let { text -> + extractTextWithVisualizedMentions( + isOut = lastMessage?.isOut == true, + originalText = text + ) + } .let { text -> prefix + text } } @@ -612,6 +622,9 @@ private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? { AttachmentType.PODCAST -> null AttachmentType.NARRATIVE -> null AttachmentType.ARTICLE -> null + AttachmentType.VIDEO_MESSAGE -> null + AttachmentType.GROUP_CHAT_STICKER -> UiR.drawable.ic_attachment_sticker + AttachmentType.STICKER_PACK_PREVIEW -> null }?.let(UiImage::Resource) } @@ -649,10 +662,9 @@ private fun extractForwardsText( else -> null } - -private fun getTextWithVisualizedMentions( - originalText: String, - mentionColor: Color, +fun extractTextWithVisualizedMentions( + isOut: Boolean, + originalText: String ): AnnotatedString = buildAnnotatedString { val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex() @@ -676,7 +688,7 @@ private fun getTextWithVisualizedMentions( replacements.add(indexRange to replaced) mentions += MentionIndex( - id = id.toIntOrNull() ?: -1, + id = id.toLongOrNull() ?: -1, idPrefix = idPrefix, indexRange = indexRange ) @@ -693,7 +705,7 @@ private fun getTextWithVisualizedMentions( val endIndex = mention.indexRange.last addStyle( - style = SpanStyle(color = mentionColor), + style = SpanStyle(color = Color.Red), start = startIndex, end = endIndex ) @@ -707,7 +719,7 @@ private fun getTextWithVisualizedMentions( } data class MentionIndex( - val id: Int, + val id: Long, val idPrefix: String, val indexRange: IntRange ) @@ -755,6 +767,9 @@ private fun getAttachmentUiText( AttachmentType.PODCAST -> UiR.string.message_attachments_podcast AttachmentType.NARRATIVE -> UiR.string.message_attachments_narrative AttachmentType.ARTICLE -> UiR.string.message_attachments_article + AttachmentType.VIDEO_MESSAGE -> UiR.string.message_attachments_video_message + AttachmentType.GROUP_CHAT_STICKER -> UiR.string.message_attachments_group_sticker + AttachmentType.STICKER_PACK_PREVIEW -> UiR.string.message_attachments_sticker_pack_preview }.let(UiText::Resource) } @@ -796,10 +811,9 @@ private fun extractBirthday(conversation: VkConversation): Boolean { private fun extractReadCondition( conversation: VkConversation, lastMessage: VkMessage? -): Boolean = (lastMessage?.isOut == true && conversation.isOutUnread()) || - (lastMessage?.isOut == false && conversation.isInUnread()) +): Boolean = !conversation.isRead(lastMessage) -private fun isAccount(peerId: Int) = peerId == UserConfig.userId +private fun isAccount(peerId: Long) = peerId == UserConfig.userId private fun extractInteractionText( resources: Resources, 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 index c9847dbe..b4a43fde 100644 --- a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/CreateChatViewModel.kt +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/CreateChatViewModel.kt @@ -33,13 +33,13 @@ interface CreateChatViewModel { val currentOffset: StateFlow val canPaginate: StateFlow - val isChatCreated: StateFlow + val isChatCreated: StateFlow fun onPaginationConditionsMet() fun onRefresh() fun onErrorConsumed() - fun toggleFriendSelection(userId: Int) + fun toggleFriendSelection(userId: Long) fun onTitleTextInputChanged(newTitle: String) @@ -62,7 +62,7 @@ class CreateChatViewModelImpl( override val currentOffset = MutableStateFlow(0) override val canPaginate = MutableStateFlow(false) - override val isChatCreated = MutableStateFlow(null) + override val isChatCreated = MutableStateFlow(null) private val useContactNames: Boolean = userSettings.useContactNames.value @@ -84,7 +84,7 @@ class CreateChatViewModelImpl( baseError.setValue { null } } - override fun toggleFriendSelection(userId: Int) { + override fun toggleFriendSelection(userId: Long) { val newSelectionList = screenState.value.selectedFriendsIds.toMutableList() if (newSelectionList.contains(userId)) { 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 index 03abef2c..10a6591c 100644 --- 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 @@ -9,7 +9,7 @@ data class CreateChatScreenState( val isPaginating: Boolean, val isPaginationExhausted: Boolean, val friends: List, - val selectedFriendsIds: List, + val selectedFriendsIds: List, val chatTitle: String ) { companion object { 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 index 3a863420..81ef6ef0 100644 --- 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 @@ -1,24 +1,28 @@ package dev.meloda.fast.conversations.navigation +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.LocalContext 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 +import org.koin.compose.viewmodel.koinViewModel @Serializable object CreateChat fun NavGraphBuilder.createChatScreen( - onChatCreated: (Int) -> Unit, + onChatCreated: (Long) -> Unit, navController: NavController, ) { composable { - val viewModel: CreateChatViewModel = - it.sharedViewModel(navController = navController) + val context = LocalContext.current + val viewModel: CreateChatViewModel = koinViewModel( + viewModelStoreOwner = context as AppCompatActivity + ) CreateChatRoute( onError = { 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 index e5ff6457..226ffc87 100644 --- 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 @@ -34,7 +34,7 @@ fun CreateChatItem( friend: UiFriend, maxLines: Int, isSelected: Boolean, - onItemClicked: (Int) -> Unit + onItemClicked: (Long) -> Unit ) { Row( modifier = modifier 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 index ff2c00f2..425ffc30 100644 --- 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 @@ -39,7 +39,7 @@ fun CreateChatList( maxLines: Int, modifier: Modifier, padding: PaddingValues, - onItemClicked: (Int) -> Unit, + onItemClicked: (Long) -> Unit, onTitleTextInputChanged: (String) -> Unit ) { val coroutineScope = rememberCoroutineScope() 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 index 4bd67313..7b1b9cc7 100644 --- 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 @@ -68,6 +68,7 @@ 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.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.isScrollingUp @@ -77,7 +78,7 @@ import dev.meloda.fast.ui.R as UiR fun CreateChatRoute( onError: (BaseError) -> Unit, onBack: () -> Unit, - onChatCreated: (Int) -> Unit, + onChatCreated: (Long) -> Unit, viewModel: CreateChatViewModel ) { val screenState by viewModel.screenState.collectAsStateWithLifecycle() @@ -87,7 +88,7 @@ fun CreateChatRoute( LaunchedEffect(isChatCreated) { if (isChatCreated != null) { - onChatCreated(isChatCreated ?: -1) + onChatCreated(isChatCreated ?: -1L) viewModel.onNavigatedBack() } } @@ -120,7 +121,7 @@ fun CreateChatScreen( onBack: () -> Unit = {}, onRefresh: () -> Unit = {}, onCreateChatButtonClicked: () -> Unit = {}, - onItemClicked: (Int) -> Unit = {}, + onItemClicked: (Long) -> Unit = {}, onTitleTextInputChanged: (String) -> Unit = {} ) { val currentTheme = LocalThemeConfig.current @@ -267,23 +268,7 @@ fun CreateChatScreen( ) { 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 - ) - } - } + VkErrorView(baseError = baseError) } screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader() diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt index 6eddecc8..17158dfd 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt @@ -12,7 +12,7 @@ object Friends fun NavGraphBuilder.friendsScreen( onError: (BaseError) -> Unit, onPhotoClicked: (url: String) -> Unit, - onMessageClicked: (userId: Int) -> Unit, + onMessageClicked: (userid: Long) -> Unit, onScrolledToTop: () -> Unit ) { composable { 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 78e40aff..debe5891 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 @@ -36,7 +36,7 @@ fun FriendItem( friend: UiFriend, maxLines: Int, onPhotoClicked: (url: String) -> Unit, - onMessageClicked: (userId: Int) -> Unit + onMessageClicked: (userid: Long) -> Unit ) { Row( modifier = modifier.fillMaxWidth(), 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 48765b06..35762f62 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 @@ -37,7 +37,7 @@ fun FriendsList( maxLines: Int, padding: PaddingValues, onPhotoClicked: (url: String) -> Unit, - onMessageClicked: (userId: Int) -> Unit, + onMessageClicked: (userid: Long) -> Unit, setCanScrollBackward: (Boolean) -> Unit ) { LaunchedEffect(listState) { diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt index 58e40e0b..44da2f00 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt @@ -1,6 +1,7 @@ package dev.meloda.fast.friends.presentation import android.content.Context +import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding @@ -30,13 +31,12 @@ import dev.meloda.fast.friends.FriendsViewModel import dev.meloda.fast.friends.FriendsViewModelImpl import dev.meloda.fast.friends.OnlineFriendsViewModelImpl import dev.meloda.fast.friends.navigation.Friends -import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R -import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.FullScreenLoader import dev.meloda.fast.ui.components.NoItemsView +import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState -import dev.meloda.fast.ui.theme.LocalScrollToTop +import dev.meloda.fast.ui.theme.LocalReselectedTab import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.flow.collectLatest @@ -52,16 +52,16 @@ fun FriendsScreen( tabIndex: Int, onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {}, - onMessageClicked: (userId: Int) -> Unit = {}, + onMessageClicked: (userid: Long) -> Unit = {}, setCanScrollBackward: (Boolean) -> Unit = {}, onScrolledToTop: () -> Unit = {} ) { val context: Context = LocalContext.current val viewModel: FriendsViewModel = if (tabIndex == 0) { - koinViewModel() + koinViewModel(viewModelStoreOwner = context as AppCompatActivity) } else { - koinViewModel() + koinViewModel(viewModelStoreOwner = context as AppCompatActivity) } LaunchedEffect(orderType) { @@ -96,7 +96,7 @@ fun FriendsScreen( initialFirstVisibleItemScrollOffset = screenState.scrollOffset ) - val scrollToTop = LocalScrollToTop.current[Friends] ?: false + val scrollToTop = LocalReselectedTab.current[Friends] ?: false LaunchedEffect(scrollToTop) { if (scrollToTop) { if (listState.firstVisibleItemIndex > 14) { @@ -136,24 +136,7 @@ fun FriendsScreen( val hazeState = LocalHazeState.current baseError?.let { error -> - when (error) { - is BaseError.SessionExpired -> { - ErrorView( - text = stringResource(R.string.session_expired), - buttonText = stringResource(R.string.action_log_out), - onButtonClick = onSessionExpiredLogOutButtonClicked - ) - } - - is BaseError.SimpleError -> { - ErrorView( - text = error.message, - buttonText = stringResource(R.string.try_again), - onButtonClick = viewModel::onRefresh - ) - } - } - + VkErrorView(baseError = error) return } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt index c7fe88b1..7ba8e209 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt @@ -57,7 +57,7 @@ import dev.meloda.fast.ui.R as UiR fun FriendsRoute( onError: (BaseError) -> Unit, onPhotoClicked: (url: String) -> Unit, - onMessageClicked: (userId: Int) -> Unit, + onMessageClicked: (userid: Long) -> Unit, onScrolledToTop: () -> Unit ) { val scope = rememberCoroutineScope() 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 d5b552bf..56759c9b 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 @@ -16,6 +16,7 @@ import androidx.lifecycle.viewModelScope import com.conena.nanokt.collections.indexOfFirstOrNull import com.conena.nanokt.text.isEmptyOrBlank import com.conena.nanokt.text.isNotEmptyOrBlank +import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.orDots import dev.meloda.fast.common.extensions.setValue @@ -32,6 +33,7 @@ import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.messageshistory.model.ActionMode import dev.meloda.fast.messageshistory.model.MessageDialog +import dev.meloda.fast.messageshistory.model.MessageNavigation import dev.meloda.fast.messageshistory.model.MessageOption import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState import dev.meloda.fast.messageshistory.model.UiItem @@ -51,15 +53,15 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.random.Random - import dev.meloda.fast.ui.R as UiR interface MessagesHistoryViewModel { val screenState: StateFlow + val navigation: StateFlow val messages: StateFlow> val uiMessages: StateFlow> - val messageDialog: StateFlow + val dialog: StateFlow val selectedMessages: StateFlow> val isNeedToScrollToIndex: StateFlow @@ -70,6 +72,10 @@ interface MessagesHistoryViewModel { val currentOffset: StateFlow val canPaginate: StateFlow + fun onNavigationConsumed() + + fun onTopBarClicked() + fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) fun onDialogDismissed(dialog: MessageDialog) fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) @@ -85,10 +91,10 @@ interface MessagesHistoryViewModel { fun onPaginationConditionsMet() - fun onMessageClicked(messageId: Int) - fun onMessageLongClicked(messageId: Int) + fun onMessageClicked(messageId: Long) + fun onMessageLongClicked(messageId: Long) - fun onPinnedMessageClicked(messageId: Int) + fun onPinnedMessageClicked(messageId: Long) fun onUnpinMessageClicked() fun onDeleteSelectedMessagesClicked() @@ -106,7 +112,8 @@ class MessagesHistoryViewModelImpl( ) : MessagesHistoryViewModel, ViewModel() { override val screenState = MutableStateFlow(MessagesHistoryScreenState.EMPTY) - override val messageDialog = MutableStateFlow(null) + override val navigation = MutableStateFlow(null) + override val dialog = MutableStateFlow(null) override val selectedMessages = MutableStateFlow>(emptyList()) override val isNeedToScrollToIndex = MutableStateFlow(null) @@ -149,6 +156,21 @@ class MessagesHistoryViewModelImpl( } } + override fun onNavigationConsumed() { + navigation.setValue { null } + } + + override fun onTopBarClicked() { + val cmId = messages.value.firstOrNull()?.cmId ?: return + + navigation.setValue { + MessageNavigation.ChatMaterials( + peerId = screenState.value.conversationId, + cmId = cmId + ) + } + } + override fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) { onDialogDismissed(dialog) @@ -223,7 +245,7 @@ class MessagesHistoryViewModelImpl( } override fun onDialogDismissed(dialog: MessageDialog) { - messageDialog.setValue { null } + this.dialog.setValue { null } } override fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) { @@ -241,13 +263,13 @@ class MessagesHistoryViewModelImpl( MessageOption.Forward -> {} MessageOption.Pin -> { - messageDialog.setValue { + this.dialog.setValue { MessageDialog.MessagePin(dialog.message.id) } } MessageOption.Unpin -> { - messageDialog.setValue { + this.dialog.setValue { MessageDialog.MessageUnpin(dialog.message.id) } } @@ -262,7 +284,7 @@ class MessagesHistoryViewModelImpl( MessageOption.MarkAsImportant, MessageOption.UnmarkAsImportant -> { - messageDialog.setValue { + this.dialog.setValue { MessageDialog.MessageMarkImportance( message = dialog.message, isImportant = option is MessageOption.MarkAsImportant @@ -272,7 +294,7 @@ class MessagesHistoryViewModelImpl( MessageOption.MarkAsSpam, MessageOption.UnmarkAsSpam -> { - messageDialog.setValue { + this.dialog.setValue { MessageDialog.MessageSpam( message = dialog.message, isSpam = option is MessageOption.MarkAsSpam @@ -283,7 +305,7 @@ class MessagesHistoryViewModelImpl( MessageOption.Edit -> {} MessageOption.Delete -> { - messageDialog.setValue { + this.dialog.setValue { MessageDialog.MessageDelete(dialog.message) } } @@ -362,7 +384,7 @@ class MessagesHistoryViewModelImpl( loadMessagesHistory() } - override fun onMessageClicked(messageId: Int) { + override fun onMessageClicked(messageId: Long) { val currentMessage = messages.value.firstOrNull { it.id == messageId } ?: return if (selectedMessages.value.isNotEmpty()) { @@ -379,13 +401,13 @@ class MessagesHistoryViewModelImpl( } syncUiMessages() } else { - messageDialog.setValue { + dialog.setValue { MessageDialog.MessageOptions(currentMessage) } } } - override fun onMessageLongClicked(messageId: Int) { + override fun onMessageLongClicked(messageId: Long) { val currentMessage = messages.value.firstOrNull { it.id == messageId } ?: return val isSelected = selectedMessages.value.contains(currentMessage) @@ -399,7 +421,7 @@ class MessagesHistoryViewModelImpl( syncUiMessages() } - override fun onPinnedMessageClicked(messageId: Int) { + override fun onPinnedMessageClicked(messageId: Long) { val uiMessages = uiMessages.value val messageIndex = uiMessages.indexOfFirstOrNull { it is UiItem.Message && it.id == messageId @@ -414,13 +436,13 @@ class MessagesHistoryViewModelImpl( override fun onUnpinMessageClicked() { val pinnedMessageId = screenState.value.pinnedMessage?.id ?: return - messageDialog.setValue { + dialog.setValue { MessageDialog.MessageUnpin(pinnedMessageId) } } override fun onDeleteSelectedMessagesClicked() { - messageDialog.setValue { + dialog.setValue { MessageDialog.MessagesDelete(selectedMessages.value) } } @@ -434,7 +456,7 @@ class MessagesHistoryViewModelImpl( if (messages.value.indexOfFirstOrNull { it.id == message.id } != null) return val randomIds = messages.value.map(VkMessage::randomId) - if (message.randomId != 0 && message.randomId in randomIds) return + if (message.randomId != 0L && message.randomId in randomIds) return val newMessages = messages.value.toMutableList() newMessages.add(0, message) @@ -463,13 +485,13 @@ class MessagesHistoryViewModelImpl( if (event.peerId != screenState.value.conversationId) return val messages = messages.value - val index = messages.indexOfFirstOrNull { it.id == event.messageId } + val index = messages.indexOfFirstOrNull { it.cmId == event.cmId } if (index == null) { // диалога нет в списке // pizdets } else { val newConversation = screenState.value.conversation.copy( - inRead = event.messageId + inReadCmId = event.cmId ) screenState.setValue { old -> @@ -484,13 +506,13 @@ class MessagesHistoryViewModelImpl( if (event.peerId != screenState.value.conversationId) return val messages = messages.value - val index = messages.indexOfFirstOrNull { it.id == event.messageId } + val index = messages.indexOfFirstOrNull { it.cmId == event.cmId } if (index == null) { // сообщения нет в списке // pizdets } else { val newConversation = screenState.value.conversation.copy( - outRead = event.messageId + outReadCmId = event.cmId ) screenState.setValue { old -> @@ -505,7 +527,7 @@ class MessagesHistoryViewModelImpl( if (event.peerId != screenState.value.conversationId) return val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == event.messageId } + val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId } if (index == null) { // сообщения нет в списке // pizdets @@ -520,10 +542,12 @@ class MessagesHistoryViewModelImpl( if (event.message.peerId != screenState.value.conversationId) return val newMessages = messages.value.toMutableList() - val maxDate = newMessages.maxOf(VkMessage::date) val minDate = newMessages.minOf(VkMessage::date) - if (event.message.date !in minDate..maxDate) return + if (event.message.date < minDate) { // сообщения не должно быть в списке + // pizdets + return + } newMessages.add(event.message) messages.setValue { newMessages.sorted() } @@ -534,7 +558,7 @@ class MessagesHistoryViewModelImpl( if (event.peerId != screenState.value.conversationId) return val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == event.messageId } + val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId } if (index == null) { // сообщения нет в списке // pizdets @@ -550,7 +574,7 @@ class MessagesHistoryViewModelImpl( if (event.peerId != screenState.value.conversationId) return val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == event.messageId } + val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId } if (index == null) { // сообщения нет в списке // pizdets @@ -578,30 +602,33 @@ class MessagesHistoryViewModelImpl( private fun loadConversation() { Log.d("MessagesHistoryViewModelImpl", "loadConversation()") - loadConversationsByIdUseCase(listOf(screenState.value.conversationId)) - .listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { response -> - val conversation = response.firstOrNull() ?: return@listenValue - val title = conversation.extractTitle( - useContactName = AppSettings.General.useContactNames, - resources = resourceProvider.resources + loadConversationsByIdUseCase( + peerIds = listOf(screenState.value.conversationId), + extended = true, + fields = VkConstants.ALL_FIELDS + ).listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { response -> + val conversation = response.firstOrNull() ?: return@listenValue + val title = conversation.extractTitle( + useContactName = AppSettings.General.useContactNames, + resources = resourceProvider.resources + ) + val avatar = conversation.extractAvatar() + + screenState.setValue { old -> + old.copy( + conversation = conversation, + title = title, + avatar = avatar ) - val avatar = conversation.extractAvatar() - - screenState.setValue { old -> - old.copy( - conversation = conversation, - title = title, - avatar = avatar - ) - } - - conversation.pinnedMessage?.let(::handlePinnedMessage) } - ) - } + + conversation.pinnedMessage?.let(::handlePinnedMessage) + } + ) + } } private fun handlePinnedMessage(pinnedMessage: VkMessage?) { @@ -736,7 +763,7 @@ class MessagesHistoryViewModelImpl( dateDiff } else { val idDiff = m2.id - m1.id - idDiff + idDiff.toInt() } } } @@ -745,14 +772,14 @@ class MessagesHistoryViewModelImpl( lastMessageText = screenState.value.message.text val newMessage = VkMessage( - id = -1 - sendingMessages.size, - conversationMessageId = -1, + id = -1L - sendingMessages.size, + cmId = -1L - sendingMessages.size, text = lastMessageText, isOut = true, peerId = screenState.value.conversationId, fromId = UserConfig.userId, date = (System.currentTimeMillis() / 1000).toInt(), - randomId = Random.nextInt(), + randomId = Random.nextInt().toLong(), action = null, actionMemberId = null, actionText = null, @@ -769,7 +796,11 @@ class MessagesHistoryViewModelImpl( actionUser = null, actionGroup = null, isPinned = false, - pinnedAt = null + isSpam = false, + pinnedAt = null, + + // TODO: 04-Apr-25, Danil Nikolaev: implement + formatData = null, ) sendingMessages += newMessage messages.setValue { old -> listOf(newMessage).plus(old) } @@ -792,7 +823,7 @@ class MessagesHistoryViewModelImpl( state.processState( any = { sendingMessages.remove(newMessage) }, error = { error -> - val failedId = -500_000 - failedMessages.size + val failedId = -500_000L - failedMessages.size val newFailedMessage = newMessage.copy(id = failedId) failedMessages += newFailedMessage @@ -801,11 +832,13 @@ class MessagesHistoryViewModelImpl( messages.setValue { newMessages } syncUiMessages() }, - success = { messageId -> + success = { response -> val newMessages = messages.value.toMutableList() - newMessages[newMessages.indexOf(newMessage)] = newMessage.copy(id = messageId) + newMessages[newMessages.indexOf(newMessage)] = newMessage.copy( + id = response.messageId, + cmId = response.cmId + ) messages.setValue { newMessages } - syncUiMessages() } ) @@ -813,7 +846,7 @@ class MessagesHistoryViewModelImpl( } private fun markAsImportant( - messageIds: List, + messageIds: List, important: Boolean, ) { messagesUseCase.markAsImportant( @@ -841,7 +874,7 @@ class MessagesHistoryViewModelImpl( } private fun deleteMessage( - messageIds: List, + messageIds: List, spam: Boolean = false, deleteForAll: Boolean = false, onSuccess: () -> Unit = {} @@ -866,39 +899,48 @@ class MessagesHistoryViewModelImpl( } } - private fun pinMessage(messageId: Int) { + private fun pinMessage(messageId: Long) { messagesUseCase.pin( peerId = screenState.value.conversationId, messageId = messageId, - conversationMessageId = null + cmId = null ).listenValue(viewModelScope) { state -> state.processState( error = ::handleError, success = { pinnedMessage -> handlePinnedMessage(pinnedMessage) - val newMessages = messages.value - .toMutableList() - .map { message -> - message.copy(isPinned = message.id == messageId) - } - messages.setValue { newMessages } - syncUiMessages() + + val newMessages = messages.value.toMutableList() + val index = newMessages.indexOfFirstOrNull { it.id == messageId } + + if (index == null) {// сообщения нет в списке + // pizdets + } else { + newMessages[index] = pinnedMessage + messages.setValue { newMessages } + syncUiMessages() + } } ) } } - private fun unpinMessage(messageId: Int) { + private fun unpinMessage(messageId: Long) { messagesUseCase.unpin(screenState.value.conversationId) .listenValue(viewModelScope) { state -> state.processState( error = ::handleError, success = { val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirst { it.id == messageId } - newMessages[index] = newMessages[index].copy(isPinned = false) - messages.setValue { newMessages } - syncUiMessages() + val index = newMessages.indexOfFirstOrNull { it.id == messageId } + + if (index == null) { // сообщения нет в списке + // pizdets + } else { + newMessages[index] = newMessages[index].copy(isPinned = false) + messages.setValue { newMessages } + syncUiMessages() + } handlePinnedMessage(null) } @@ -908,8 +950,8 @@ class MessagesHistoryViewModelImpl( fun editMessage( originalMessage: VkMessage, - peerId: Int, - messageId: Int, + peerid: Long, + messageid: Long, newText: String? = null, attachments: List? = null, ) { @@ -1001,7 +1043,7 @@ class MessagesHistoryViewModelImpl( // TODO: 25.08.2023, Danil Nikolaev: this and down below - rewrite // suspend fun uploadPhoto( -// peerId: Int, +// peerid: Long, // photo: File, // name: String, // ) { @@ -1021,7 +1063,7 @@ class MessagesHistoryViewModelImpl( // } // } -// private suspend fun getPhotoMessageUploadServer(peerId: Int) { +// private suspend fun getPhotoMessageUploadServer(peerid: Long) { // suspendCoroutine { continuation -> // viewModelScope.launch { // sendRequestNotNull( @@ -1218,7 +1260,7 @@ class MessagesHistoryViewModelImpl( // } // suspend fun uploadFile( -// peerId: Int, +// peerid: Long, // file: File, // name: String, // type: FilesRepository.FileType, @@ -1235,7 +1277,7 @@ class MessagesHistoryViewModelImpl( // } // private suspend fun getFileMessageUploadServer( -// peerId: Int, +// peerid: Long, // type: FilesRepository.FileType, // ) { // suspendCoroutine { continuation -> @@ -1314,14 +1356,14 @@ class MessagesHistoryViewModelImpl( // //object MessagesUnpinEvent : VkEvent() // -//data class MessagesDeleteEvent(val peerId: Int, val messagesIds: List) : VkEvent() +//data class MessagesDeleteEvent(val peerid: Long, val messagesIds: List) : VkEvent() // //data class MessagesEditEvent(val message: VkMessageDomain) : VkEvent() // //data class MessagesReadEvent( // val isOut: Boolean, -// val peerId: Int, -// val messageId: Int, +// val peerid: Long, +// val messageid: Long, //) : VkEvent() // //data class MessagesNewEvent( diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt index fed0ab64..a932bca1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageDialog.kt @@ -6,8 +6,8 @@ import dev.meloda.fast.model.api.domain.VkMessage @Immutable sealed class MessageDialog { data class MessageOptions(val message: VkMessage) : MessageDialog() - data class MessagePin(val messageId: Int) : MessageDialog() - data class MessageUnpin(val messageId: Int) : MessageDialog() + data class MessagePin(val messageId: Long) : MessageDialog() + data class MessageUnpin(val messageId: Long) : MessageDialog() data class MessageDelete(val message: VkMessage) : MessageDialog() data class MessagesDelete(val messages: List) : MessageDialog() diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageNavigation.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageNavigation.kt new file mode 100644 index 00000000..8870306b --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessageNavigation.kt @@ -0,0 +1,12 @@ +package dev.meloda.fast.messageshistory.model + +import androidx.compose.runtime.Immutable + +@Immutable +sealed class MessageNavigation { + + data class ChatMaterials( + val peerId: Long, + val cmId: Long + ) : MessageNavigation() +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryArguments.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryArguments.kt index 2ecd656d..8e68b618 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryArguments.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryArguments.kt @@ -6,4 +6,4 @@ import kotlinx.serialization.Serializable @Parcelize @Serializable -data class MessagesHistoryArguments(val conversationId: Int) : Parcelable +data class MessagesHistoryArguments(val conversationId: Long) : Parcelable diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt index 14162f0b..f4d8caf0 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt @@ -10,7 +10,7 @@ import dev.meloda.fast.model.api.domain.VkMessage @Immutable data class MessagesHistoryScreenState( - val conversationId: Int, + val conversationId: Long, val title: String, val status: String?, val avatar: UiImage, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt index ac01f266..f7fab77e 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt @@ -4,18 +4,18 @@ import androidx.compose.ui.text.AnnotatedString import dev.meloda.fast.common.model.UiImage sealed class UiItem( - open val id: Int, - val cmId: Int + open val id: Long, + val cmId: Long ) { data class Message( - override val id: Int, - val conversationMessageId: Int, - val text: String?, + override val id: Long, + val conversationMessageId: Long, + val text: AnnotatedString?, val isOut: Boolean, - val fromId: Int, + val fromId: Long, val date: String, - val randomId: Int, + val randomId: Long, val isInChat: Boolean, val name: String, val showDate: Boolean, @@ -31,9 +31,9 @@ sealed class UiItem( ) : UiItem(id, conversationMessageId) data class ActionMessage( - override val id: Int, - val conversationMessageId: Int, + override val id: Long, + val conversationMessageId: Long, val text: AnnotatedString, - val actionCmId: Int? + val actionCmId: Long? ) : UiItem(id, conversationMessageId) } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt index 4de8bb72..40c4c273 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt @@ -27,17 +27,17 @@ data class MessagesHistory(val arguments: MessagesHistoryArguments) { fun NavGraphBuilder.messagesHistoryScreen( onError: (BaseError) -> Unit, onBack: () -> Unit, - onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit + onNavigateToChatMaterials: (peerId: Long, cmId: Long) -> Unit ) { composable(typeMap = MessagesHistory.typeMap) { MessagesHistoryRoute( onError = onError, onBack = onBack, - onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked, + onNavigateToChatMaterials = onNavigateToChatMaterials ) } } -fun NavController.navigateToMessagesHistory(conversationId: Int) { +fun NavController.navigateToMessagesHistory(conversationId: Long) { this.navigate(MessagesHistory(MessagesHistoryArguments(conversationId))) } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt index 046b36d6..de257b74 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -30,6 +31,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import dev.meloda.fast.messageshistory.model.SendingStatus import dev.meloda.fast.ui.theme.LocalThemeConfig @@ -38,7 +40,7 @@ import dev.meloda.fast.ui.R as UiR @Composable fun MessageBubble( modifier: Modifier = Modifier, - text: String?, + text: AnnotatedString?, isOut: Boolean, date: String?, edited: Boolean, @@ -55,122 +57,156 @@ fun MessageBubble( MaterialTheme.colorScheme.primaryContainer } - val textColor = if (!isOut) { + val contentColor = if (!isOut) { MaterialTheme.colorScheme.onSurface } else { MaterialTheme.colorScheme.onPrimaryContainer } - Box( - modifier = modifier - .widthIn(min = 56.dp) - .clip(RoundedCornerShape(24.dp)) - .background(backgroundColor) - .padding( - horizontal = 8.dp, - vertical = 6.dp - ) - .then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier), - ) { - val minDateContainerWidth by remember(edited, isOut, pinned, important) { - derivedStateOf { - val mainPart = if (edited) 50.dp else 30.dp - val readIndicatorPart = if (isOut) 14.dp else 0.dp - val pinnedIndicatorPart = if (pinned) 14.dp else 0.dp - val importantIndicatorPart = if (important) 14.dp else 0.dp - - mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart - } - } - - val dateContainerWidth by animateDpAsState( - targetValue = minDateContainerWidth, - label = "dateContainerWidth" - ) - - if (text != null) { - val textLambda: @Composable () -> Unit = remember { - { - Text( - text = text, - modifier = Modifier - .padding(2.dp) - .align(Alignment.Center) - .padding(end = 4.dp) - .padding(end = dateContainerWidth) - .padding(end = 4.dp) - .then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier), - color = textColor - ) - } - } - - if (isSelected) { - SelectionContainer { - textLambda.invoke() - } - } else { - textLambda.invoke() - } - } - - Row( - modifier = Modifier - .align(Alignment.BottomEnd) - .defaultMinSize(minWidth = dateContainerWidth) + CompositionLocalProvider(LocalContentColor provides contentColor) { + Box( + modifier = modifier + .widthIn(min = 56.dp) + .clip(RoundedCornerShape(24.dp)) + .background(backgroundColor) + .padding( + horizontal = 8.dp, + vertical = 6.dp + ) .then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier), ) { - if (important) { - Icon( - painter = painterResource(UiR.drawable.round_star_24), - contentDescription = null, - modifier = Modifier.size(14.dp) - ) - Spacer(modifier = Modifier.width(4.dp)) - } - if (pinned) { - Icon( - painter = painterResource(UiR.drawable.ic_round_push_pin_24), - contentDescription = null, - modifier = Modifier - .size(14.dp) - .rotate(45f) - ) - Spacer(modifier = Modifier.width(4.dp)) - } - if (edited) { - Icon( - imageVector = Icons.Rounded.Create, - contentDescription = null, - modifier = Modifier.size(14.dp) - ) - Spacer(modifier = Modifier.width(4.dp)) + val minDateContainerWidth by remember(edited, isOut, pinned, important) { + derivedStateOf { + val mainPart = if (edited) 50.dp else 30.dp + val readIndicatorPart = if (isOut) 14.dp else 0.dp + val pinnedIndicatorPart = if (pinned) 14.dp else 0.dp + val importantIndicatorPart = if (important) 14.dp else 0.dp + + mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart + } } - Text( - text = date.orEmpty(), - style = MaterialTheme.typography.labelSmall, + val dateContainerWidth by animateDpAsState( + targetValue = minDateContainerWidth, + label = "dateContainerWidth" ) - Spacer(modifier = Modifier.width(4.dp)) - if (isOut) { - Icon( - modifier = Modifier.size(14.dp), - painter = painterResource( - when (sendingStatus) { - SendingStatus.SENDING -> UiR.drawable.round_access_time_24 - SendingStatus.SENT -> { - if (isRead) UiR.drawable.round_done_all_24 - else UiR.drawable.ic_round_done_24 - } + if (text != null) { + val textLambda: @Composable () -> Unit = remember(text, theme, dateContainerWidth) { + { + Text( + text = kotlin.run { + val builder = AnnotatedString.Builder(text) - SendingStatus.FAILED -> UiR.drawable.round_error_outline_24 - } - ), - tint = if (sendingStatus == SendingStatus.FAILED) Color.Red - else LocalContentColor.current, - contentDescription = null + text.spanStyles.map { spanStyleRange -> + val updatedSpanStyle = + if (spanStyleRange.item.color == Color.Red) { + spanStyleRange.item.copy(color = + if (isOut) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.primary + } + ) + } else { + spanStyleRange.item + } + + builder.addStyle( + style = updatedSpanStyle, + start = spanStyleRange.start, + end = spanStyleRange.end + ) + } + + text.paragraphStyles.forEach { style -> + builder.addStyle( + style = style.item, + start = style.start, + end = style.end + ) + } + + builder.toAnnotatedString() + }, + modifier = Modifier + .padding(2.dp) + .align(Alignment.Center) + .padding(end = 4.dp) + .padding(end = dateContainerWidth) + .padding(end = 4.dp) + .then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier) + ) + } + } + + if (isSelected) { + SelectionContainer { + textLambda.invoke() + } + } else { + textLambda.invoke() + } + } + + Row( + modifier = Modifier + .align(Alignment.BottomEnd) + .defaultMinSize(minWidth = dateContainerWidth) + .then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier), + ) { + if (important) { + Icon( + painter = painterResource(UiR.drawable.round_star_24), + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + } + if (pinned) { + Icon( + painter = painterResource(UiR.drawable.ic_round_push_pin_24), + contentDescription = null, + modifier = Modifier + .size(14.dp) + .rotate(45f) + ) + Spacer(modifier = Modifier.width(4.dp)) + } + if (edited) { + Icon( + imageVector = Icons.Rounded.Create, + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + } + + Text( + text = date.orEmpty(), + style = MaterialTheme.typography.labelSmall ) + Spacer(modifier = Modifier.width(4.dp)) + + if (isOut) { + Icon( + modifier = Modifier.size(14.dp), + painter = painterResource( + when (sendingStatus) { + SendingStatus.SENDING -> UiR.drawable.round_access_time_24 + SendingStatus.SENT -> { + if (isRead) UiR.drawable.round_done_all_24 + else UiR.drawable.ic_round_done_24 + } + + SendingStatus.FAILED -> UiR.drawable.round_error_outline_24 + } + ), + tint = if (sendingStatus == SendingStatus.FAILED) MaterialTheme.colorScheme.error + else LocalContentColor.current, + contentDescription = null + ) + } } } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryDialogs.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryDialogs.kt new file mode 100644 index 00000000..00bb158e --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryDialogs.kt @@ -0,0 +1,323 @@ +package dev.meloda.fast.messageshistory.presentation + +import android.os.Bundle +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.dp +import androidx.core.os.bundleOf +import dev.meloda.fast.data.UserConfig +import dev.meloda.fast.messageshistory.model.MessageDialog +import dev.meloda.fast.messageshistory.model.MessageOption +import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState +import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.basic.ContentAlpha +import dev.meloda.fast.ui.basic.LocalContentAlpha +import dev.meloda.fast.ui.components.MaterialDialog +import java.util.concurrent.TimeUnit + +@Composable +fun HandleDialogs( + screenState: MessagesHistoryScreenState, + dialog: MessageDialog?, + onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> }, + onDismissed: (MessageDialog) -> Unit = {}, + onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> } +) { + when (dialog) { + null -> Unit + + is MessageDialog.MessageOptions -> { + MessageOptionsDialog( + screenState = screenState, + message = dialog.message, + onDismissed = { onDismissed(dialog) }, + onItemPicked = { bundle -> onItemPicked(dialog, bundle) } + ) + } + + is MessageDialog.MessageDelete -> { + MessageDeleteDialog( + messages = listOf(dialog.message), + onConfirmed = { onConfirmed(dialog, it) }, + onDismissed = { onDismissed(dialog) } + ) + } + + is MessageDialog.MessagesDelete -> { + MessageDeleteDialog( + messages = dialog.messages, + onConfirmed = { onConfirmed(dialog, it) }, + onDismissed = { onDismissed(dialog) } + ) + } + + is MessageDialog.MessagePin, + is MessageDialog.MessageUnpin -> { + MessagePinStateDialog( + pin = dialog is MessageDialog.MessagePin, + onConfirmed = { onConfirmed(dialog, bundleOf()) }, + onDismissed = { onDismissed(dialog) } + ) + } + + is MessageDialog.MessageMarkImportance -> { + MessageImportanceDialog( + important = dialog.isImportant, + onConfirmed = { onConfirmed(dialog, bundleOf()) }, + onDismissed = { onDismissed(dialog) } + ) + } + + is MessageDialog.MessageSpam -> { + MessageSpamDialog( + spam = dialog.isSpam, + onConfirmed = { onConfirmed(dialog, bundleOf()) }, + onDismissed = { onDismissed(dialog) } + ) + } + } +} + + +@Composable +fun MessageOptionsDialog( + screenState: MessagesHistoryScreenState, + message: VkMessage, + onDismissed: () -> Unit = {}, + onItemPicked: (Bundle) -> Unit +) { + val options = mutableListOf() + if (message.isFailed()) { + options += MessageOption.Retry + } else { + options += MessageOption.Reply + options += MessageOption.ForwardHere + options += MessageOption.Forward + + if (message.isPeerChat() && screenState.conversation.canChangePin) { + options += if (message.isPinned) MessageOption.Unpin else MessageOption.Pin + } + + if (!message.isRead(screenState.conversation)) { + options += MessageOption.Read + } + + options += MessageOption.Copy + + if (message.isOut) { + val diff = System.currentTimeMillis() - message.date * 1000L + if (diff - TimeUnit.DAYS.toMillis(1) <= 0) { + options += MessageOption.Edit + } + } + + options += if (message.isImportant) MessageOption.UnmarkAsImportant + else MessageOption.MarkAsImportant + + + if (!message.isOut) { + options += if (message.isSpam) MessageOption.UnmarkAsSpam + else MessageOption.MarkAsSpam + } + } + + options += MessageOption.Delete + + val messageOptions = options.map { option -> + Triple( + stringResource(option.titleResId), + painterResource(option.iconResId), + when { + option in listOf( + MessageOption.Delete, + MessageOption.MarkAsSpam + ) -> MaterialTheme.colorScheme.error + + else -> MaterialTheme.colorScheme.primary + } + ) + } + + MaterialDialog(onDismissRequest = onDismissed) { + messageOptions + .forEachIndexed { index, (title, painter, tintColor) -> + DropdownMenuItem( + text = { + Row { + Text(text = title) + Spacer(modifier = Modifier.width(8.dp)) + } + }, + leadingIcon = { + Row { + Spacer(modifier = Modifier.width(8.dp)) + Icon( + painter = painter, + contentDescription = null, + tint = tintColor + ) + } + }, + onClick = { + onDismissed() + val pickedOption = options[index] + onItemPicked(bundleOf("option" to pickedOption)) + } + ) + } + } +} + +@Composable +fun MessageDeleteDialog( + messages: List, + onConfirmed: (Bundle) -> Unit = {}, + onDismissed: () -> Unit = {}, +) { + var forEveryone by remember { + mutableStateOf( + !messages.any { it.peerId == UserConfig.userId } + && messages.all(VkMessage::isOut) + ) + } + + val shouldBeDisabled by remember(messages) { + mutableStateOf( + messages.any { it.peerId == UserConfig.userId } + || messages.all(VkMessage::isFailed) + || !messages.all(VkMessage::isOut) + ) + } + + MaterialDialog( + onDismissRequest = onDismissed, + title = stringResource(R.string.delete_message_title), + confirmText = stringResource(R.string.action_delete), + confirmAction = { + onConfirmed( + bundleOf("everyone" to if (messages.all(VkMessage::isOut)) forEveryone else false) + ) + }, + cancelText = stringResource(R.string.cancel), + ) { + Row( + modifier = Modifier + .then( + if (!shouldBeDisabled) { + Modifier.clickable { forEveryone = !forEveryone } + } else Modifier) + .fillMaxWidth() + .minimumInteractiveComponentSize() + .padding(start = 24.dp, end = 16.dp) + ) { + Checkbox( + checked = forEveryone, + onCheckedChange = null, + enabled = !shouldBeDisabled + ) + + Spacer(modifier = Modifier.width(8.dp)) + + LocalContentAlpha( + alpha = if (shouldBeDisabled) ContentAlpha.disabled + else ContentAlpha.high + ) { + Text(text = stringResource(R.string.delete_message_for_everyone)) + } + } + } +} + +@Composable +fun MessagePinStateDialog( + pin: Boolean, + onConfirmed: () -> Unit = {}, + onDismissed: () -> Unit = {}, +) { + MaterialDialog( + onDismissRequest = onDismissed, + title = stringResource( + if (pin) R.string.pin_message_title + else R.string.unpin_message_title + ), + text = stringResource( + if (pin) R.string.pin_message_text + else R.string.unpin_message_text + ), + confirmText = stringResource( + if (pin) R.string.action_pin + else R.string.action_unpin + ), + confirmAction = onConfirmed, + cancelText = stringResource(R.string.cancel) + ) +} + +@Composable +fun MessageImportanceDialog( + important: Boolean, + onConfirmed: () -> Unit = {}, + onDismissed: () -> Unit = {}, +) { + MaterialDialog( + onDismissRequest = onDismissed, + title = stringResource( + if (important) R.string.important_message_title + else R.string.unimportant_message_title + ), + text = stringResource( + if (important) R.string.important_message_text + else R.string.unimportant_message_text + ), + confirmText = stringResource( + if (important) R.string.action_mark + else R.string.action_unmark + ), + confirmAction = onConfirmed, + cancelText = stringResource(R.string.cancel) + ) +} + +@Composable +fun MessageSpamDialog( + spam: Boolean, + onConfirmed: () -> Unit = {}, + onDismissed: () -> Unit = {}, +) { + MaterialDialog( + onDismissRequest = onDismissed, + title = stringResource( + if (spam) R.string.spam_message_title + else R.string.unspam_message_title + ), + text = stringResource( + if (spam) R.string.spam_message_text + else R.string.unspam_message_text + ), + confirmText = stringResource( + if (spam) R.string.action_mark + else R.string.action_unmark + ), + confirmAction = onConfirmed, + cancelText = stringResource(R.string.cancel) + ) +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt new file mode 100644 index 00000000..776dfe65 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt @@ -0,0 +1,83 @@ +package dev.meloda.fast.messageshistory.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.meloda.fast.datastore.UserSettings +import dev.meloda.fast.messageshistory.MessagesHistoryViewModel +import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl +import dev.meloda.fast.messageshistory.model.MessageNavigation +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList +import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject + +@Composable +fun MessagesHistoryRoute( + onError: (BaseError) -> Unit, + onBack: () -> Unit, + onNavigateToChatMaterials: (peerId: Long, conversationMessageId: Long) -> Unit, + viewModel: MessagesHistoryViewModel = koinViewModel() +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + val navigationEvent by viewModel.navigation.collectAsStateWithLifecycle() + val messages by viewModel.messages.collectAsStateWithLifecycle() + val uiMessages by viewModel.uiMessages.collectAsStateWithLifecycle() + val dialog by viewModel.dialog.collectAsStateWithLifecycle() + val selectedMessages by viewModel.selectedMessages.collectAsStateWithLifecycle() + val baseError by viewModel.baseError.collectAsStateWithLifecycle() + val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle() + + val userSettings: UserSettings = koinInject() + val showEmojiButton by userSettings.showEmojiButton.collectAsStateWithLifecycle() + + LaunchedEffect(navigationEvent) { + val needToConsume = when (val navigation = navigationEvent) { + null -> false + + is MessageNavigation.ChatMaterials -> { + val (peerId, cmId) = navigation + onNavigateToChatMaterials(peerId, cmId) + true + } + } + if (needToConsume) viewModel.onNavigationConsumed() + } + + MessagesHistoryScreen( + screenState = screenState, + messages = messages.toImmutableList(), + uiMessages = uiMessages.toImmutableList(), + scrollIndex = scrollIndex, + selectedMessages = selectedMessages.toImmutableList(), + baseError = baseError, + canPaginate = canPaginate, + showEmojiButton = showEmojiButton, + onBack = onBack, + onClose = viewModel::onCloseButtonClicked, + onScrolledToIndex = viewModel::onScrolledToIndex, + onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, + onTopBarClicked = viewModel::onTopBarClicked, + onRefresh = viewModel::onRefresh, + onPaginationConditionsMet = viewModel::onPaginationConditionsMet, + onMessageInputChanged = viewModel::onMessageInputChanged, + onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked, + onActionButtonClicked = viewModel::onActionButtonClicked, + onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked, + onMessageClicked = viewModel::onMessageClicked, + onMessageLongClicked = viewModel::onMessageLongClicked, + onPinnedMessageClicked = viewModel::onPinnedMessageClicked, + onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked, + onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked + ) + + HandleDialogs( + screenState = screenState, + dialog = dialog, + onConfirmed = viewModel::onDialogConfirmed, + onDismissed = viewModel::onDialogDismissed, + onItemPicked = viewModel::onDialogItemPicked + ) +} 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 e0ad52da..628abf2a 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 @@ -1,6 +1,5 @@ package dev.meloda.fast.messageshistory.presentation -import android.os.Bundle import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState @@ -41,7 +40,6 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -56,7 +54,6 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -82,9 +79,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf import androidx.core.view.HapticFeedbackConstantsCompat -import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect @@ -93,377 +88,21 @@ import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.common.extensions.orDots import dev.meloda.fast.data.UserConfig import dev.meloda.fast.datastore.AppSettings -import dev.meloda.fast.datastore.UserSettings -import dev.meloda.fast.messageshistory.MessagesHistoryViewModel -import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl import dev.meloda.fast.messageshistory.model.ActionMode -import dev.meloda.fast.messageshistory.model.MessageDialog -import dev.meloda.fast.messageshistory.model.MessageOption import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState import dev.meloda.fast.messageshistory.model.UiItem -import dev.meloda.fast.messageshistory.util.firstMessage import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.api.domain.VkMessage -import dev.meloda.fast.ui.basic.ContentAlpha -import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.IconButton -import dev.meloda.fast.ui.components.MaterialDialog +import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList -import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList import dev.meloda.fast.ui.util.emptyImmutableList import dev.meloda.fast.ui.util.getImage import kotlinx.coroutines.launch -import org.koin.androidx.compose.koinViewModel -import org.koin.compose.koinInject -import java.util.concurrent.TimeUnit import dev.meloda.fast.ui.R as UiR -@Composable -fun MessagesHistoryRoute( - onError: (BaseError) -> Unit, - onBack: () -> Unit, - onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit, - viewModel: MessagesHistoryViewModel = koinViewModel() -) { - val screenState by viewModel.screenState.collectAsStateWithLifecycle() - val messages by viewModel.messages.collectAsStateWithLifecycle() - val uiMessages by viewModel.uiMessages.collectAsStateWithLifecycle() - val messageDialog by viewModel.messageDialog.collectAsStateWithLifecycle() - val selectedMessages by viewModel.selectedMessages.collectAsStateWithLifecycle() - val baseError by viewModel.baseError.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() - val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle() - - val userSettings: UserSettings = koinInject() - val showEmojiButton by userSettings.showEmojiButton.collectAsStateWithLifecycle() - - MessagesHistoryScreen( - screenState = screenState, - messages = messages.toImmutableList(), - uiMessages = uiMessages.toImmutableList(), - scrollIndex = scrollIndex, - selectedMessages = selectedMessages.toImmutableList(), - baseError = baseError, - canPaginate = canPaginate, - showEmojiButton = showEmojiButton, - onBack = onBack, - onClose = viewModel::onCloseButtonClicked, - onScrolledToIndex = viewModel::onScrolledToIndex, - onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, - onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked, - onRefresh = viewModel::onRefresh, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet, - onMessageInputChanged = viewModel::onMessageInputChanged, - onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked, - onActionButtonClicked = viewModel::onActionButtonClicked, - onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked, - onMessageClicked = viewModel::onMessageClicked, - onMessageLongClicked = viewModel::onMessageLongClicked, - onPinnedMessageClicked = viewModel::onPinnedMessageClicked, - onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked, - onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked - ) - - HandleDialogs( - screenState = screenState, - messageDialog = messageDialog, - onConfirmed = viewModel::onDialogConfirmed, - onDismissed = viewModel::onDialogDismissed, - onItemPicked = viewModel::onDialogItemPicked - ) -} - -@Composable -fun HandleDialogs( - screenState: MessagesHistoryScreenState, - messageDialog: MessageDialog?, - onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> }, - onDismissed: (MessageDialog) -> Unit = {}, - onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> } -) { - when (messageDialog) { - null -> Unit - - is MessageDialog.MessageOptions -> { - MessageOptionsDialog( - screenState = screenState, - message = messageDialog.message, - onDismissed = { onDismissed(messageDialog) }, - onItemPicked = { bundle -> onItemPicked(messageDialog, bundle) } - ) - } - - is MessageDialog.MessageDelete -> { - MessageDeleteDialog( - messages = listOf(messageDialog.message), - onConfirmed = { onConfirmed(messageDialog, it) }, - onDismissed = { onDismissed(messageDialog) } - ) - } - - is MessageDialog.MessagesDelete -> { - MessageDeleteDialog( - messages = messageDialog.messages, - onConfirmed = { onConfirmed(messageDialog, it) }, - onDismissed = { onDismissed(messageDialog) } - ) - } - - is MessageDialog.MessagePin, - is MessageDialog.MessageUnpin -> { - MessagePinStateDialog( - pin = messageDialog is MessageDialog.MessagePin, - onConfirmed = { onConfirmed(messageDialog, bundleOf()) }, - onDismissed = { onDismissed(messageDialog) } - ) - } - - is MessageDialog.MessageMarkImportance -> { - MessageImportanceDialog( - important = messageDialog.isImportant, - onConfirmed = { onConfirmed(messageDialog, bundleOf()) }, - onDismissed = { onDismissed(messageDialog) } - ) - } - - is MessageDialog.MessageSpam -> { - MessageSpamDialog( - spam = messageDialog.isSpam, - onConfirmed = { onConfirmed(messageDialog, bundleOf()) }, - onDismissed = { onDismissed(messageDialog) } - ) - } - } -} - - -@Composable -fun MessageOptionsDialog( - screenState: MessagesHistoryScreenState, - message: VkMessage, - onDismissed: () -> Unit = {}, - onItemPicked: (Bundle) -> Unit -) { - val options = mutableListOf() - if (message.isFailed()) { - options += MessageOption.Retry - } else { - options += MessageOption.Reply - options += MessageOption.ForwardHere - options += MessageOption.Forward - - if (message.isPeerChat() && screenState.conversation.canChangePin) { - options += if (message.isPinned) MessageOption.Unpin else MessageOption.Pin - } - - if (!message.isRead(screenState.conversation)) { - options += MessageOption.Read - } - - options += MessageOption.Copy - - if (message.isOut) { - val diff = System.currentTimeMillis() - message.date * 1000L - if (diff - TimeUnit.DAYS.toMillis(1) <= 0) { - options += MessageOption.Edit - } - } - - options += if (message.isImportant) MessageOption.UnmarkAsImportant - else MessageOption.MarkAsImportant - - - if (!message.isOut) { - options += if (message.isSpam) MessageOption.UnmarkAsSpam - else MessageOption.MarkAsSpam - } - } - - options += MessageOption.Delete - - val messageOptions = options.map { option -> - Triple( - stringResource(option.titleResId), - painterResource(option.iconResId), - when { - option in listOf( - MessageOption.Delete, - MessageOption.MarkAsSpam - ) -> MaterialTheme.colorScheme.error - - else -> MaterialTheme.colorScheme.primary - } - ) - } - - MaterialDialog(onDismissRequest = onDismissed) { - messageOptions - .forEachIndexed { index, (title, painter, tintColor) -> - DropdownMenuItem( - text = { - Row { - Text(text = title) - Spacer(modifier = Modifier.width(8.dp)) - } - }, - leadingIcon = { - Row { - Spacer(modifier = Modifier.width(8.dp)) - Icon( - painter = painter, - contentDescription = null, - tint = tintColor - ) - } - }, - onClick = { - onDismissed() - val pickedOption = options[index] - onItemPicked(bundleOf("option" to pickedOption)) - } - ) - } - } -} - -@Composable -fun MessageDeleteDialog( - messages: List, - onConfirmed: (Bundle) -> Unit = {}, - onDismissed: () -> Unit = {}, -) { - var forEveryone by remember { - mutableStateOf( - !messages.any { it.peerId == UserConfig.userId } - && messages.all(VkMessage::isOut) - ) - } - - val shouldBeDisabled by remember(messages) { - mutableStateOf( - messages.any { it.peerId == UserConfig.userId } - || messages.all(VkMessage::isFailed) - || !messages.all(VkMessage::isOut) - ) - } - - MaterialDialog( - onDismissRequest = onDismissed, - title = stringResource(UiR.string.delete_message_title), - confirmText = stringResource(UiR.string.action_delete), - confirmAction = { - onConfirmed( - bundleOf("everyone" to if (messages.all(VkMessage::isOut)) forEveryone else false) - ) - }, - cancelText = stringResource(UiR.string.cancel), - ) { - Row( - modifier = Modifier - .then( - if (!shouldBeDisabled) { - Modifier.clickable { forEveryone = !forEveryone } - } else Modifier) - .fillMaxWidth() - .minimumInteractiveComponentSize() - .padding(start = 24.dp, end = 16.dp) - ) { - Checkbox( - checked = forEveryone, - onCheckedChange = null, - enabled = !shouldBeDisabled - ) - - Spacer(modifier = Modifier.width(8.dp)) - - LocalContentAlpha( - alpha = if (shouldBeDisabled) ContentAlpha.disabled - else ContentAlpha.high - ) { - Text(text = stringResource(UiR.string.delete_message_for_everyone)) - } - } - } -} - -@Composable -fun MessagePinStateDialog( - pin: Boolean, - onConfirmed: () -> Unit = {}, - onDismissed: () -> Unit = {}, -) { - MaterialDialog( - onDismissRequest = onDismissed, - title = stringResource( - if (pin) UiR.string.pin_message_title - else UiR.string.unpin_message_title - ), - text = stringResource( - if (pin) UiR.string.pin_message_text - else UiR.string.unpin_message_text - ), - confirmText = stringResource( - if (pin) UiR.string.action_pin - else UiR.string.action_unpin - ), - confirmAction = onConfirmed, - cancelText = stringResource(UiR.string.cancel) - ) -} - -@Composable -fun MessageImportanceDialog( - important: Boolean, - onConfirmed: () -> Unit = {}, - onDismissed: () -> Unit = {}, -) { - MaterialDialog( - onDismissRequest = onDismissed, - title = stringResource( - if (important) UiR.string.important_message_title - else UiR.string.unimportant_message_title - ), - text = stringResource( - if (important) UiR.string.important_message_text - else UiR.string.unimportant_message_text - ), - confirmText = stringResource( - if (important) UiR.string.action_mark - else UiR.string.action_unmark - ), - confirmAction = onConfirmed, - cancelText = stringResource(UiR.string.cancel) - ) -} - -@Composable -fun MessageSpamDialog( - spam: Boolean, - onConfirmed: () -> Unit = {}, - onDismissed: () -> Unit = {}, -) { - MaterialDialog( - onDismissRequest = onDismissed, - title = stringResource( - if (spam) UiR.string.spam_message_title - else UiR.string.unspam_message_title - ), - text = stringResource( - if (spam) UiR.string.spam_message_text - else UiR.string.unspam_message_text - ), - confirmText = stringResource( - if (spam) UiR.string.action_mark - else UiR.string.action_unmark - ), - confirmAction = onConfirmed, - cancelText = stringResource(UiR.string.cancel) - ) -} - @OptIn( ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, @@ -483,16 +122,16 @@ fun MessagesHistoryScreen( onClose: () -> Unit = {}, onScrolledToIndex: () -> Unit = {}, onSessionExpiredLogOutButtonClicked: () -> Unit = {}, - onChatMaterialsDropdownItemClicked: (peerId: Int, conversationMessageId: Int) -> Unit = { _, _ -> }, + onTopBarClicked: () -> Unit = {}, onRefresh: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {}, onMessageInputChanged: (TextFieldValue) -> Unit = {}, onAttachmentButtonClicked: () -> Unit = {}, onActionButtonClicked: () -> Unit = {}, onEmojiButtonLongClicked: () -> Unit = {}, - onMessageClicked: (Int) -> Unit = {}, - onMessageLongClicked: (Int) -> Unit = {}, - onPinnedMessageClicked: (Int) -> Unit = {}, + onMessageClicked: (Long) -> Unit = {}, + onMessageLongClicked: (Long) -> Unit = {}, + onPinnedMessageClicked: (Long) -> Unit = {}, onUnpinMessageButtonClicked: () -> Unit = {}, onDeleteSelectedButtonClicked: () -> Unit = {} ) { @@ -516,12 +155,7 @@ fun MessagesHistoryScreen( onBack = onClose ) - val pinnedMessage by remember(screenState) { - derivedStateOf { - screenState.conversation.pinnedMessage - } - } - + val pinnedMessage = screenState.pinnedMessage val paginationConditionMet by remember(canPaginate, listState) { derivedStateOf { @@ -598,7 +232,13 @@ fun MessagesHistoryScreen( ) } else Modifier ) - .fillMaxWidth(), + .fillMaxWidth() + .then( + if (screenState.isLoading && messages.isEmpty()) Modifier + else Modifier.clickable { + onTopBarClicked() + } + ), title = { Row( modifier = Modifier.weight(1f), @@ -606,23 +246,41 @@ fun MessagesHistoryScreen( ) { if (selectedMessages.isEmpty()) { val avatar = screenState.avatar.getImage() - if (avatar is Painter) { - Image( - painter = avatar, - contentDescription = null, + if (screenState.conversationId == UserConfig.userId) { + Box( modifier = Modifier .size(36.dp) .clip(CircleShape) - ) + .background(MaterialTheme.colorScheme.primary) + ) { + Icon( + modifier = Modifier + .align(Alignment.Center) + .size(24.dp), + painter = painterResource(id = UiR.drawable.ic_round_bookmark_border_24), + contentDescription = "Favorites icon", + tint = MaterialTheme.colorScheme.onPrimary + ) + } } else { - AsyncImage( - model = screenState.avatar.getImage(), - contentDescription = "Profile Image", - modifier = Modifier - .size(36.dp) - .clip(CircleShape), - placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut), - ) + if (avatar is Painter) { + Image( + painter = avatar, + contentDescription = null, + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + ) + } else { + AsyncImage( + model = screenState.avatar.getImage(), + contentDescription = "Profile Image", + modifier = Modifier + .size(36.dp) + .clip(CircleShape), + placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut), + ) + } } Spacer(modifier = Modifier.width(12.dp)) @@ -705,9 +363,6 @@ fun MessagesHistoryScreen( ) } } else { - if (screenState.isLoading) { - return@TopAppBar - } IconButton( onClick = { dropDownMenuExpanded = true } ) { @@ -725,28 +380,6 @@ fun MessagesHistoryScreen( }, offset = DpOffset(x = (-4).dp, y = (-60).dp) ) { - DropdownMenuItem( - onClick = { - dropDownMenuExpanded = false - - // TODO: 11/07/2024, Danil Nikolaev: to VM - - // TODO: 23-Mar-25, Danil Nikolaev: crash if no messages (ex. new chat) - onChatMaterialsDropdownItemClicked( - screenState.conversationId, - uiMessages.values.firstMessage().conversationMessageId - ) - }, - text = { - Text(text = stringResource(UiR.string.chat_materials_action_title)) - }, - leadingIcon = { - Icon( - painter = painterResource(UiR.drawable.ic_multimedia), - contentDescription = null - ) - } - ) DropdownMenuItem( onClick = { onRefresh() @@ -808,10 +441,13 @@ fun MessagesHistoryScreen( isPaginating = screenState.isPaginating, messageBarHeight = messageBarHeight, onRequestScrollToCmId = { cmId -> - coroutineScope.launch { - listState.animateScrollToItem( - index = uiMessages.values.indexOfMessageByCmId(cmId) - ) + val index = uiMessages.values.indexOfMessageByCmId(cmId) + if (index == null) { // сообщения нет в списке + // pizdets + } else { + coroutineScope.launch { + listState.animateScrollToItem(index = index) + } } }, onMessageClicked = { id -> @@ -847,12 +483,15 @@ fun MessagesHistoryScreen( .clip(RoundedCornerShape(36.dp)) .then( if (theme.enableBlur) { - Modifier.hazeEffect( - state = hazeState, - style = HazeMaterials.ultraThin() - ).border(1.dp, MaterialTheme.colorScheme.outlineVariant, - RoundedCornerShape(36.dp) - ) + Modifier + .hazeEffect( + state = hazeState, + style = HazeMaterials.ultraThin() + ) + .border( + 1.dp, MaterialTheme.colorScheme.outlineVariant, + RoundedCornerShape(36.dp) + ) } else Modifier ) .animateContentSize() @@ -1042,23 +681,7 @@ fun MessagesHistoryScreen( } 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 - ) - } - } + VkErrorView(baseError = baseError) } } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt index 549d7dee..c5fc000b 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt @@ -45,13 +45,10 @@ fun MessagesList( uiMessages: ImmutableList, isPaginating: Boolean, messageBarHeight: Dp, - onRequestScrollToCmId: (cmId: Int) -> Unit = {}, - onMessageClicked: (Int) -> Unit = {}, - onMessageLongClicked: (Int) -> Unit = {} + onRequestScrollToCmId: (cmId: Long) -> Unit = {}, + onMessageClicked: (Long) -> Unit = {}, + onMessageLongClicked: (Long) -> Unit = {} ) { - val messages = remember(uiMessages) { - uiMessages.toList() - } val theme = LocalThemeConfig.current val view = LocalView.current @@ -77,7 +74,7 @@ fun MessagesList( } items( - items = messages, + items = uiMessages.values, key = UiItem::id, contentType = { item -> when (item) { diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt index 04343e70..7f97b086 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt @@ -38,7 +38,7 @@ fun OutgoingMessageBubble( ) { MessageBubble( modifier = Modifier, - text = message.text.orDots(), + text = message.text, isOut = true, date = message.date, edited = message.isEdited, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt index 27cd9289..6d83607c 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/PinnedMessageContainer.kt @@ -35,7 +35,7 @@ fun PinnedMessageContainer( title: String, summary: AnnotatedString?, canChangePin: Boolean, - onPinnedMessageClicked: (Int) -> Unit = {}, + onPinnedMessageClicked: (Long) -> Unit = {}, onUnpinMessageButtonClicked: () -> Unit = {} ) { Row( 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 36de0c13..4ac0a280 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 @@ -1,19 +1,20 @@ package dev.meloda.fast.messageshistory.util +import com.conena.nanokt.collections.indexOfFirstOrNull 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 = +fun List.indexOfMessageById(messageId: Long): Int = indexOfFirst { it.id == messageId } -fun List.findMessageById(messageId: Int): UiItem.Message? = +fun List.findMessageById(messageId: Long): UiItem.Message? = firstOrNull { it.id == messageId } as UiItem.Message? -fun List.indexOfMessageByCmId(cmId: Int): Int = - indexOfFirst { it.cmId == cmId } +fun List.indexOfMessageByCmId(cmId: Long): Int? = + indexOfFirstOrNull { it.cmId == cmId } -fun List.findMessageByCmId(cmId: Int): UiItem.Message = +fun List.findMessageByCmId(cmId: Long): UiItem.Message = first { it.cmId == cmId } as UiItem.Message diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt index 238c6f7f..f795b053 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt @@ -1,10 +1,15 @@ package dev.meloda.fast.messageshistory.util import android.content.res.Resources +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.AnnotatedString.Annotation import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.StringAnnotation import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import dev.meloda.fast.common.extensions.orDots import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiText @@ -15,6 +20,7 @@ import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.messageshistory.model.SendingStatus import dev.meloda.fast.messageshistory.model.UiItem import dev.meloda.fast.model.api.PeerType +import dev.meloda.fast.model.api.domain.FormatDataType import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.ui.R @@ -22,7 +28,7 @@ import java.text.SimpleDateFormat import java.util.Locale import dev.meloda.fast.ui.R as UiR -private fun isAccount(fromId: Int) = fromId == UserConfig.userId +private fun isAccount(fromId: Long) = fromId == UserConfig.userId fun VkMessage.extractAvatar() = when { isUser() -> { @@ -101,7 +107,7 @@ fun VkMessage.asPresentation( ): UiItem = when { action != null -> UiItem.ActionMessage( id = id, - conversationMessageId = conversationMessageId, + conversationMessageId = cmId, text = extractActionText( resources = resourceProvider.resources, youPrefix = resourceProvider.getString(R.string.you_message_prefix), @@ -112,8 +118,12 @@ fun VkMessage.asPresentation( else -> UiItem.Message( id = id, - conversationMessageId = conversationMessageId, - text = text, + conversationMessageId = cmId, + text = extractTextWithVisualizedMentions( + isOut = isOut, + originalText = text, + formatData = formatData + ), isOut = isOut, fromId = fromId, date = extractDate(), @@ -542,3 +552,144 @@ fun VkMessage.extractActionText( } } } + +// TODO: 04-Apr-25, Danil Nikolaev: get rid of method duplication +fun extractTextWithVisualizedMentions( + isOut: Boolean, + originalText: String?, + formatData: VkMessage.FormatData? +): AnnotatedString? { + if (originalText == null) return null + + val annotations = + mutableListOf>() + + val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex() + + val mentions = mutableListOf() + + var currentIndex = 0 + val replacements = mutableListOf>() + + val newText = regex.replace(originalText) { matchResult -> + val idPrefix = matchResult.groups[1]?.value.orEmpty() + val startIndex = matchResult.range.first + val endIndex = matchResult.range.last + + val id = matchResult.groups[2]?.value ?: "" + + val replaced = matchResult.groups[3]?.value.orEmpty() + + val indexRange = + (startIndex + currentIndex)..startIndex + currentIndex + replaced.length + + replacements.add(indexRange to replaced) + + mentions += MentionIndex( + id = id.toLongOrNull() ?: -1, + idPrefix = idPrefix, + indexRange = indexRange + ) + + currentIndex += replaced.length - (endIndex - startIndex + 1) + + replaced + } + + mentions.forEach { mention -> + val startIndex = mention.indexRange.first + val endIndex = mention.indexRange.last + + annotations += AnnotatedString.Range( + item = SpanStyle(color = Color.Red), + start = startIndex, + end = endIndex + ) + annotations += AnnotatedString.Range( + item = StringAnnotation(mention.id.toString()), + tag = mention.idPrefix, + start = startIndex, + end = endIndex + ) + } + + if (formatData == null) return AnnotatedString(text = newText, annotations = annotations) + + var current = 0 + + val newOffsets = formatData.items.map { (offset, length) -> + val r = replacements.filter { (range, _) -> + (range - current) collidesWith (offset.. range.first + } + + current = r.sumOf { (range, _) -> range.last - range.first - 1 } + + offset + current + } + + formatData.items.forEachIndexed { index, item -> + val offset = newOffsets[index] + + val spanStyle = when (item.type) { + FormatDataType.BOLD -> { + SpanStyle(fontWeight = FontWeight.SemiBold) + } + + FormatDataType.ITALIC -> { + SpanStyle(fontStyle = FontStyle.Italic) + } + + FormatDataType.UNDERLINE -> { + SpanStyle(textDecoration = TextDecoration.Underline) + } + + FormatDataType.URL -> { + annotations += AnnotatedString.Range( + item = StringAnnotation(item.url.orEmpty()), + start = offset, + end = offset + item.length, + tag = newText.substring(offset, offset + item.length) + ) + + if (isOut) { + SpanStyle( + fontWeight = FontWeight.SemiBold, + textDecoration = TextDecoration.Underline + ) + + } else { + SpanStyle( + fontWeight = FontWeight.SemiBold, + color = Color.Red + ) + } + } + } + + annotations += AnnotatedString.Range( + item = spanStyle, + start = offset, + end = offset + item.length + ) + } + + return AnnotatedString(text = newText, annotations = annotations) +} + +data class MentionIndex( + val id: Long, + val idPrefix: String, + val indexRange: IntRange +) + +infix fun ClosedRange.collidesWith(other: ClosedRange): Boolean { + return this.start < other.endInclusive && other.start < this.endInclusive +} + +operator fun ClosedRange.minus(other: ClosedRange): ClosedRange { + return (this.start - other.start)..(this.endInclusive - other.endInclusive) +} + +operator fun ClosedRange.minus(other: Int): ClosedRange { + return (this.start - other)..(this.endInclusive - other) +} diff --git a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt index ddb31e89..0227a08f 100644 --- a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt +++ b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/navigation/ProfileRoute.kt @@ -1,14 +1,15 @@ package dev.meloda.fast.profile.navigation -import androidx.navigation.NavController +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import dev.meloda.fast.model.BaseError import dev.meloda.fast.profile.ProfileViewModel import dev.meloda.fast.profile.ProfileViewModelImpl import dev.meloda.fast.profile.presentation.ProfileRoute -import dev.meloda.fast.ui.extensions.sharedViewModel import kotlinx.serialization.Serializable +import org.koin.androidx.compose.koinViewModel @Serializable object Profile @@ -16,12 +17,13 @@ object Profile fun NavGraphBuilder.profileScreen( onError: (BaseError) -> Unit, onSettingsButtonClicked: () -> Unit, - onPhotoClicked: (url: String) -> Unit, - navController: NavController + onPhotoClicked: (url: String) -> Unit ) { composable { - val viewModel: ProfileViewModel = - it.sharedViewModel(navController = navController) + val context = LocalContext.current + val viewModel: ProfileViewModel = koinViewModel( + viewModelStoreOwner = context as AppCompatActivity + ) ProfileRoute( onError = onError, diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 1a76347c..7e4b3a98 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -9,6 +9,7 @@ android { dependencies { implementation(projects.core.data) + implementation(projects.core.domain) implementation(projects.core.model) implementation(projects.core.ui) diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt index b94909bb..216737e7 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt @@ -5,7 +5,6 @@ import android.os.Build import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.extensions.findWithIndex import dev.meloda.fast.common.extensions.setValue @@ -19,16 +18,19 @@ import dev.meloda.fast.data.db.AccountsRepository import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.datastore.UserSettings +import dev.meloda.fast.domain.AuthUseCase import dev.meloda.fast.model.database.AccountEntity import dev.meloda.fast.settings.model.SettingsItem import dev.meloda.fast.settings.model.SettingsScreenState import dev.meloda.fast.settings.model.SettingsShowOptions import dev.meloda.fast.settings.model.TextProvider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import dev.meloda.fast.ui.R as UiR interface SettingsViewModel { @@ -37,7 +39,7 @@ interface SettingsViewModel { val hapticType: StateFlow fun onLogOutAlertDismissed() - fun onLogOutAlertPositiveClick() + suspend fun onLogOutAlertPositiveClick() fun onPerformCrashAlertDismissed() fun onPerformCrashPositiveButtonClicked() @@ -50,6 +52,7 @@ interface SettingsViewModel { } class SettingsViewModelImpl( + private val authUseCase: AuthUseCase, private val accountsRepository: AccountsRepository, private val userSettings: UserSettings, private val resources: Resources, @@ -69,20 +72,37 @@ class SettingsViewModelImpl( emitShowOptions { old -> old.copy(showLogOut = false) } } - override fun onLogOutAlertPositiveClick() { - viewModelScope.launch(Dispatchers.IO) { - accountsRepository.storeAccounts( - listOf( - AccountEntity( - userId = UserConfig.userId, - accessToken = "", - fastToken = UserConfig.fastToken, - trustedHash = UserConfig.trustedHash + override suspend fun onLogOutAlertPositiveClick() { + withContext(Dispatchers.IO) { + val tasks = listOf( +// async { +// suspendCoroutine { continuation -> +// authUseCase.logout().listenValue(viewModelScope) { state -> +// state.processState( +// any = { continuation.resume(Unit) }, +// success = {}, +// error = {} +// ) +// } +// } +// }, + async { + accountsRepository.storeAccounts( + listOf( + AccountEntity( + userId = UserConfig.userId, + accessToken = "", + fastToken = UserConfig.fastToken, + trustedHash = UserConfig.trustedHash, + exchangeToken = null + ) + ) ) - ) + }, + async { UserConfig.clear() } ) - UserConfig.clear() + tasks.awaitAll() } } diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt index c177ad53..e83a7af2 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -52,6 +53,7 @@ import dev.meloda.fast.settings.presentation.item.TitleTextItem import dev.meloda.fast.ui.components.ActionInvokeDismiss import dev.meloda.fast.ui.components.MaterialDialog import dev.meloda.fast.ui.theme.LocalThemeConfig +import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import dev.meloda.fast.ui.R as UiR @@ -83,12 +85,16 @@ fun SettingsRoute( onSettingsItemValueChanged = viewModel::onSettingsItemChanged ) + val scope = rememberCoroutineScope() + HandlePopups( performCrashPositiveClick = viewModel::onPerformCrashPositiveButtonClicked, performCrashDismissed = viewModel::onPerformCrashAlertDismissed, logoutPositiveClick = { - viewModel.onLogOutAlertPositiveClick() - onLogOutButtonClicked() + scope.launch { + viewModel.onLogOutAlertPositiveClick() + onLogOutButtonClicked() + } }, logoutDismissed = viewModel::onLogOutAlertDismissed, screenState = screenState