forked from melod1n/fast-messenger
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>
This commit is contained in:
@@ -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> {
|
||||
ImageLoader.Builder(get())
|
||||
.crossfade(true)
|
||||
.build()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,18 +20,20 @@ sealed class State<out T> {
|
||||
|
||||
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 <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
|
||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||
}
|
||||
|
||||
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = when (this) {
|
||||
is ApiResult.Success -> State.Success(successMapper(this.value))
|
||||
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+190
-84
@@ -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<ConversationsScreenState>
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val imagesToPreload: StateFlow<List<String>>
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
val scrollToTop: StateFlow<Boolean>
|
||||
@@ -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<BaseError?>(null)
|
||||
override val imagesToPreload = MutableStateFlow<List<String>>(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()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-16
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user