From 7e5843759d93ca894deb9e390f6dc9f7f27cb8fb Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sat, 11 Jan 2025 06:38:08 +0300 Subject: [PATCH] release/0.1.6 (#103) * Bump com.google.guava:guava from 33.3.1-jre to 33.4.0-jre (#97) * Bump coroutines from 1.9.0 to 1.10.1 (#100) * some improvements + loading conversation on new message if it is not already in the list * Bump koin from 4.0.0 to 4.0.1 (#101) * minor update --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../fast/common/di/ApplicationModule.kt | 4 +- .../fast/common/extensions/Extensions.kt | 2 - .../main/kotlin/dev/meloda/fast/data/State.kt | 21 +- .../meloda/fast/domain/OAuthUseCaseImpl.kt | 152 +++++----- .../dev/meloda/fast/network/ValidationType.kt | 4 +- .../meloda/fast/auth/login/LoginViewModel.kt | 7 + .../fast/auth/login/model/LoginError.kt | 1 + .../auth/login/presentation/LoginScreen.kt | 11 +- .../conversations/ConversationsViewModel.kt | 274 ++++++++++++------ .../presentation/ConversationsScreen.kt | 16 - .../util/ConversationDomainMapper.kt | 2 +- gradle/libs.versions.toml | 10 +- 12 files changed, 310 insertions(+), 194 deletions(-) diff --git a/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt b/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt index efb95bb4..d17330bd 100644 --- a/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt +++ b/app/src/main/kotlin/dev/meloda/fast/common/di/ApplicationModule.kt @@ -26,8 +26,8 @@ import dev.meloda.fast.provider.ApiLanguageProvider import dev.meloda.fast.service.longpolling.di.longPollModule import dev.meloda.fast.settings.di.settingsModule import org.koin.android.ext.koin.androidContext -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.core.qualifier.qualifier import org.koin.dsl.bind import org.koin.dsl.module @@ -61,7 +61,7 @@ val applicationModule = module { qualifier = qualifier("main") } - single { + single { ImageLoader.Builder(get()) .crossfade(true) .build() 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 b85befec..eee07f7d 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 @@ -1,7 +1,5 @@ package dev.meloda.fast.common.extensions -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay 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 b87a0619..ca3d40a9 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 @@ -20,18 +20,20 @@ sealed class State { data object ConnectionError : Error() - data object Unknown : Error() + data object UnknownError : Error() data object InternalError : Error() data class OAuthError(val error: OAuthErrorDomain) : Error() + + data class TestError(val message: String) : Error() } fun isLoading(): Boolean = this is Loading companion object { - val UNKNOWN_ERROR = Error.Unknown + val UNKNOWN_ERROR = Error.UnknownError } } @@ -73,11 +75,12 @@ fun ApiResult.mapToState() = when (this) { is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() } -fun ApiResult.mapToState(successMapper: (T) -> N) = when (this) { - is ApiResult.Success -> State.Success(successMapper(this.value)) +fun ApiResult.mapToState(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() -} + 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() + } 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 d03f02f2..082f2c2d 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 @@ -33,92 +33,100 @@ class OAuthUseCaseImpl( forceSms = forceSms ) - val error = response.error?.let(VkOAuthError::parse) - val errorType = response.errorType?.let(VkOAuthErrorType::parse) + 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 + val newState = when (error) { + null -> { + State.Success( + AuthInfo( + userId = response.userId, + accessToken = response.accessToken, + validationHash = response.validationHash ) ) } - } - VkOAuthError.NEED_CAPTCHA -> { - State.Error.OAuthError( - OAuthErrorDomain.CaptchaRequiredError( - captchaSid = response.captchaSid.orEmpty(), - captchaImageUrl = response.captchaImage.orEmpty() + 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_CLIENT -> { + State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError) + } - VkOAuthError.INVALID_REQUEST -> { - when (errorType) { - null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError) + VkOAuthError.INVALID_REQUEST -> { + when (errorType) { + null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError) - VkOAuthErrorType.WRONG_OTP -> { - State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode) + 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) + } } + } - 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) } } - VkOAuthError.UNKNOWN -> { - State.Error.OAuthError(OAuthErrorDomain.UnknownError) + emit(newState) + }.fold( + onSuccess = { + }, + onFailure = { + emit(State.Error.TestError(it.stackTraceToString())) } - } - - emit(newState) + ) } } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/ValidationType.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/ValidationType.kt index 50a6c4a1..e29218b8 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/ValidationType.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/ValidationType.kt @@ -5,6 +5,8 @@ enum class ValidationType(val value: String) { SMS("2fa_sms"); companion object { - fun parse(value: String): ValidationType = entries.first { it.value == value } + fun parse(value: String): ValidationType = + entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Unknown validation type $value") } } 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 9390645f..976ea9e0 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 @@ -345,6 +345,13 @@ class LoginViewModelImpl( true } + is State.Error.TestError -> { + val message = stateError.message + val error = LoginError.SimpleError(message = message) + loginError.update { error } + true + } + else -> false } } 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 index d6b5fd78..c3a6c84b 100644 --- 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 @@ -9,4 +9,5 @@ sealed class 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/presentation/LoginScreen.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LoginScreen.kt index b004f221..b85683a7 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 @@ -50,8 +50,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -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.LoginError import dev.meloda.fast.auth.login.model.LoginScreenState @@ -441,5 +439,14 @@ fun HandleError( confirmText = stringResource(id = UiR.string.ok) ) } + + is LoginError.SimpleError -> { + MaterialDialog( + onDismissRequest = onDismiss, + title = "Error", + text = error.message, + confirmText = stringResource(id = UiR.string.ok) + ) + } } } 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 cb575634..35a4fad3 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 @@ -1,8 +1,11 @@ package dev.meloda.fast.conversations +import android.content.Context import android.content.res.Resources 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.extensions.createTimerFlow import dev.meloda.fast.common.extensions.findWithIndex @@ -18,6 +21,7 @@ import dev.meloda.fast.data.State import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.ConversationsUseCase +import dev.meloda.fast.domain.LoadConversationsByIdUseCase import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.model.BaseError @@ -43,7 +47,6 @@ interface ConversationsViewModel { val screenState: StateFlow val baseError: StateFlow - val imagesToPreload: StateFlow> val currentOffset: StateFlow val canPaginate: StateFlow val scrollToTop: StateFlow @@ -78,16 +81,23 @@ class ConversationsViewModelImpl( private val conversationsUseCase: ConversationsUseCase, private val messagesUseCase: MessagesUseCase, private val resources: Resources, - private val userSettings: UserSettings + private val userSettings: UserSettings, + private val imageLoader: ImageLoader, + private val applicationContext: Context, + private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase ) : ConversationsViewModel, ViewModel() { override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY) override val baseError = MutableStateFlow(null) - override val imagesToPreload = MutableStateFlow>(emptyList()) override val currentOffset = MutableStateFlow(0) override val canPaginate = MutableStateFlow(false) override val scrollToTop = MutableStateFlow(false) + // TODO: 22-Dec-24, Danil Nikolaev: rewrite + private val useContactNames = { + userSettings.useContactNames.value + } + override fun onPaginationConditionsMet() { currentOffset.update { screenState.value.conversations.size } loadConversations() @@ -281,9 +291,17 @@ class ConversationsViewModelImpl( val paginationExhausted = !itemsCountSufficient && screenState.value.conversations.isNotEmpty() - imagesToPreload.setValue { + val imagesToPreload = response.mapNotNull { it.extractAvatar().extractUrl() } + + imagesToPreload.forEach { url -> + imageLoader.enqueue( + ImageRequest.Builder(applicationContext) + .data(url) + .build() + ) } + conversationsUseCase.storeConversations(response) val loadedConversations = response.map { @@ -337,7 +355,12 @@ class ConversationsViewModelImpl( conversations.update { newConversations } screenState.setValue { old -> old.copy( - conversations = newConversations.map { it.asPresentation(resources) } + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } ) } } @@ -373,13 +396,40 @@ class ConversationsViewModelImpl( private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { val message = event.message + val newConversations = conversations.value.toMutableList() val conversationIndex = newConversations.indexOfFirstOrNull { it.id == message.peerId } - if (conversationIndex == null) { // диалога нет в списке - // pizdets - // TODO: 04/07/2024, Danil Nikolaev: load conversation and store info + if (conversationIndex == null) { + loadConversationsByIdUseCase(peerIds = listOf(message.peerId)) + .listenValue(viewModelScope) { state -> + state.processState( + error = { error -> + + }, + success = { response -> + val conversation = (response.firstOrNull() ?: return@listenValue) + .copy(lastMessage = message) + + // TODO: 22-Dec-24, Danil Nikolaev: handle interactions and pinned state + + newConversations.add(pinnedConversationsCount.value, conversation) + conversations.update { newConversations } + + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } + ) + } + } + ) + } } else { val conversation = newConversations[conversationIndex] var newConversation = conversation.copy( @@ -420,7 +470,12 @@ class ConversationsViewModelImpl( screenState.setValue { old -> old.copy( - conversations = newConversations.map { it.asPresentation(resources) } + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } ) } } @@ -444,7 +499,12 @@ class ConversationsViewModelImpl( screenState.setValue { old -> old.copy( - conversations = newConversations.map { it.asPresentation(resources) } + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } ) } } @@ -454,20 +514,29 @@ class ConversationsViewModelImpl( val newConversations = conversations.value.toMutableList() val conversationIndex = - newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return + newConversations.indexOfFirstOrNull { it.id == event.peerId } - newConversations[conversationIndex] = - newConversations[conversationIndex].copy( - inRead = event.messageId, - unreadCount = event.unreadCount - ) + if (conversationIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations[conversationIndex] = + newConversations[conversationIndex].copy( + inRead = event.messageId, + unreadCount = event.unreadCount + ) - conversations.update { newConversations } + conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { it.asPresentation(resources) } - ) + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } + ) + } } } @@ -475,19 +544,28 @@ class ConversationsViewModelImpl( val newConversations = conversations.value.toMutableList() val conversationIndex = - newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return + newConversations.indexOfFirstOrNull { it.id == event.peerId } - newConversations[conversationIndex] = - newConversations[conversationIndex].copy( - outRead = event.messageId, - unreadCount = event.unreadCount - ) + if (conversationIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations[conversationIndex] = + newConversations[conversationIndex].copy( + outRead = event.messageId, + unreadCount = event.unreadCount + ) - conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { it.asPresentation(resources) } - ) + conversations.update { newConversations } + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } + ) + } } } @@ -496,34 +574,43 @@ class ConversationsViewModelImpl( val newConversations = conversations.value.toMutableList() val conversationIndex = - newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return + newConversations.indexOfFirstOrNull { it.id == event.peerId } - val pin = event.majorId > 0 - - val conversation = newConversations[conversationIndex].copy(majorId = event.majorId) - - newConversations.removeAt(conversationIndex) - - if (pin) { - newConversations.add(0, conversation) + if (conversationIndex == null) { // диалога нет в списке + // pizdets } else { - pinnedCount -= 1 + val pin = event.majorId > 0 - newConversations.add(conversation) + val conversation = newConversations[conversationIndex].copy(majorId = event.majorId) - val pinnedSubList = newConversations.filter(VkConversation::isPinned) - val unpinnedSubList = newConversations - .filterNot(VkConversation::isPinned) - .sortedByDescending { it.lastMessage?.date } + newConversations.removeAt(conversationIndex) - newConversations.clear() - newConversations += pinnedSubList + unpinnedSubList - } + if (pin) { + newConversations.add(0, conversation) + } else { + pinnedCount -= 1 - conversations.update { newConversations } + newConversations.add(conversation) - screenState.setValue { old -> - old.copy(conversations = newConversations.map { it.asPresentation(resources) }) + val pinnedSubList = newConversations.filter(VkConversation::isPinned) + val unpinnedSubList = newConversations + .filterNot(VkConversation::isPinned) + .sortedByDescending { it.lastMessage?.date } + + newConversations.clear() + newConversations += pinnedSubList + unpinnedSubList + } + + conversations.update { newConversations } + + screenState.setValue { old -> + old.copy(conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + }) + } } } @@ -543,44 +630,53 @@ class ConversationsViewModelImpl( val newConversations = conversations.value.toMutableList() val conversationAndIndex = - newConversations.findWithIndex { it.id == peerId } ?: return + newConversations.findWithIndex { it.id == peerId } - newConversations[conversationAndIndex.first] = - conversationAndIndex.second.copy( - interactionType = interactionType.value, - interactionIds = userIds - ) + if (conversationAndIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations[conversationAndIndex.first] = + conversationAndIndex.second.copy( + interactionType = interactionType.value, + interactionIds = userIds + ) - conversations.update { newConversations } + conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { it.asPresentation(resources) } - ) - } - - interactionsTimers[peerId]?.let { interactionJob -> - if (interactionJob.interactionType == interactionType) { - interactionJob.timerJob.cancel(NewInteractionException) + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } + ) } - } - var timeoutAction: (() -> Unit)? = null + interactionsTimers[peerId]?.let { interactionJob -> + if (interactionJob.interactionType == interactionType) { + interactionJob.timerJob.cancel(NewInteractionException) + } + } - val timerJob = createTimerFlow( - time = 5, - onTimeoutAction = { timeoutAction?.invoke() } - ).launchIn(viewModelScope) + var timeoutAction: (() -> Unit)? = null - val newInteractionJob = InteractionJob( - interactionType = interactionType, - timerJob = timerJob - ) + val timerJob = createTimerFlow( + time = 5, + onTimeoutAction = { timeoutAction?.invoke() } + ).launchIn(viewModelScope) - interactionsTimers[peerId] = newInteractionJob + val newInteractionJob = InteractionJob( + interactionType = interactionType, + timerJob = timerJob + ) - timeoutAction = { - stopInteraction(peerId, newInteractionJob) + interactionsTimers[peerId] = newInteractionJob + + timeoutAction = { + stopInteraction(peerId, newInteractionJob) + } } } @@ -600,7 +696,12 @@ class ConversationsViewModelImpl( conversations.update { newConversations } screenState.setValue { old -> old.copy( - conversations = newConversations.map { it.asPresentation(resources) } + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } ) } @@ -629,7 +730,12 @@ class ConversationsViewModelImpl( conversations.update { newConversations } screenState.setValue { old -> old.copy( - conversations = newConversations.map { it.asPresentation(resources) } + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames() + ) + } ) } } 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 dbf270da..cf19746b 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 @@ -54,7 +54,6 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -65,8 +64,6 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.imageLoader -import coil.request.ImageRequest import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi @@ -96,24 +93,11 @@ fun ConversationsRoute( onConversationPhotoClicked: (url: String) -> Unit, viewModel: ConversationsViewModel ) { - val context = LocalContext.current - val screenState by viewModel.screenState.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle() - val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle() - LaunchedEffect(imagesToPreload) { - imagesToPreload.forEach { url -> - context.imageLoader.enqueue( - ImageRequest.Builder(context) - .data(url) - .build() - ) - } - } - ConversationsScreen( screenState = screenState, baseError = baseError, 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 d9590c3c..cd267345 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 @@ -33,7 +33,7 @@ import dev.meloda.fast.ui.R as UiR fun VkConversation.asPresentation( resources: Resources, - useContactName: Boolean = false + useContactName: Boolean ): UiConversation = UiConversation( id = id, lastMessageId = lastMessageId, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 379994a3..16d75de2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ minSdk = "23" targetSdk = "35" compileSdk = "35" -versionCode = "8" -versionName = "0.1.5" +versionCode = "9" +versionName = "0.1.6" agp = "8.7.3" converterMoshi = "2.11.0" @@ -13,14 +13,14 @@ kotlin = "2.1.0" ksp = "2.1.0-1.0.29" compose-bom = "2024.12.01" -koin = "4.0.0" +koin = "4.0.1" accompanist = "0.37.0" coil = "2.7.0" -coroutines = "1.9.0" +coroutines = "1.10.1" junit = "4.13.2" chucker = "4.1.0" -guava = "33.3.1-jre" +guava = "33.4.0-jre" lifecycle = "2.8.7" core-ktx = "1.15.0" material = "1.12.0"