From 66deae6fc3d16739477190a50aa2776ddaa3d8c1 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 31 May 2026 06:48:21 +0300 Subject: [PATCH] temp --- .../kotlin/dev/meloda/fast/MainViewModel.kt | 83 +++++++----------- .../meloda/fast/common/di/AndroidModule.kt | 18 ++++ .../fast/common/di/ApplicationModule.kt | 28 +++--- .../meloda/fast/presentation/MainActivity.kt | 4 +- .../fast/presentation/NetworkObserver.kt | 86 ++++++------------- .../meloda/fast/presentation/RootScreen.kt | 14 ++- .../fast/common/NetworkStateListener.kt | 23 +++++ .../meloda/fast/common/model/NetworkState.kt | 3 + .../meloda/fast/common/model/NetworkStatus.kt | 10 +++ .../meloda/fast/common/model/NetworkType.kt | 5 ++ .../fast/common/provider/ResourceProvider.kt | 2 - .../dev/meloda/fast/model/AccountDto.kt | 24 ++++++ .../fast/model/database/AccountEntity.kt | 11 ++- .../fast/ui/common/LocalNetworkState.kt | 6 ++ core/ui/src/main/res/values/strings.xml | 1 + .../meloda/fast/auth/login/LoginViewModel.kt | 6 +- .../dev/meloda/fast/convos/ConvosViewModel.kt | 21 ++++- .../dev/meloda/fast/convos/di/ConvosModule.kt | 4 +- .../fast/convos/presentation/ConvosScreen.kt | 5 ++ .../meloda/fast/settings/SettingsViewModel.kt | 6 +- 20 files changed, 210 insertions(+), 150 deletions(-) create mode 100644 app/src/main/kotlin/dev/meloda/fast/common/di/AndroidModule.kt create mode 100644 core/common/src/main/kotlin/dev/meloda/fast/common/NetworkStateListener.kt create mode 100644 core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkState.kt create mode 100644 core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkStatus.kt create mode 100644 core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkType.kt create mode 100644 core/model/src/main/kotlin/dev/meloda/fast/model/AccountDto.kt create mode 100644 core/ui/src/main/kotlin/dev/meloda/fast/ui/common/LocalNetworkState.kt diff --git a/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt b/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt index 72eab7e5..05124df9 100644 --- a/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt +++ b/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt @@ -29,61 +29,30 @@ import dev.meloda.fast.navigation.Main import dev.meloda.fast.settings.navigation.Settings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -interface MainViewModel { - - val startDestination: StateFlow - val isNeedToReplaceWithAuth: StateFlow - val currentUser: StateFlow - - val isNeedToShowNotificationsDeniedDialog: StateFlow - val isNeedToShowNotificationsRationaleDialog: StateFlow - val isNeedToCheckNotificationsPermission: StateFlow - val isNeedToRequestNotifications: StateFlow - - fun onError(error: BaseError) - - fun onNavigatedToAuth() - - fun onAppResumed(intent: Intent) - - @OptIn(ExperimentalPermissionsApi::class) - fun onPermissionCheckStatus(status: PermissionStatus) - fun onPermissionsRequested() - - fun onNotificationsDeniedDialogConfirmClicked() - fun onNotificationsDeniedDialogCancelClicked() - fun onNotificationsDeniedDialogDismissed() - fun onNotificationsRationaleDialogDismissed() - fun onNotificationsRationaleDialogCancelClicked() - - fun onUserAuthenticated() -} - -class MainViewModelImpl( +class MainViewModel( private val getCurrentAccountUseCase: GetCurrentAccountUseCase, private val loadUserByIdUseCase: LoadUserByIdUseCase, private val userSettings: UserSettings, private val longPollController: LongPollController, private val logger: FastLogger -) : MainViewModel, ViewModel() { +) : ViewModel() { - override val startDestination = MutableStateFlow(null) - override val isNeedToReplaceWithAuth = MutableStateFlow(false) - override val currentUser = MutableStateFlow(null) + val startDestination = MutableStateFlow(null) + val isNeedToReplaceWithAuth = MutableStateFlow(false) + val currentUser = MutableStateFlow(null) - override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false) - override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false) - override val isNeedToCheckNotificationsPermission = MutableStateFlow(false) - override val isNeedToRequestNotifications = MutableStateFlow(false) + val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false) + val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false) + val isNeedToCheckNotificationsPermission = MutableStateFlow(false) + val isNeedToRequestNotifications = MutableStateFlow(false) private var openNotificationsSettings = false private var openAppSettings = false - override fun onError(error: BaseError) { + fun onError(error: BaseError) { when (error) { BaseError.SessionExpired, BaseError.AccountBlocked -> { @@ -94,11 +63,11 @@ class MainViewModelImpl( } } - override fun onNavigatedToAuth() { + fun onNavigatedToAuth() { isNeedToReplaceWithAuth.update { false } } - override fun onAppResumed(intent: Intent) { + fun onAppResumed(intent: Intent) { openNotificationsSettings = intent.hasCategory(NotificationCompat.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) openAppSettings = @@ -125,7 +94,7 @@ class MainViewModelImpl( } @ExperimentalPermissionsApi - override fun onPermissionCheckStatus(status: PermissionStatus) { + fun onPermissionCheckStatus(status: PermissionStatus) { isNeedToCheckNotificationsPermission.update { false } when (status) { @@ -147,33 +116,33 @@ class MainViewModelImpl( } } - override fun onPermissionsRequested() { + fun onPermissionsRequested() { isNeedToRequestNotifications.update { false } } - override fun onNotificationsDeniedDialogConfirmClicked() { + fun onNotificationsDeniedDialogConfirmClicked() { isNeedToRequestNotifications.update { true } } - override fun onNotificationsDeniedDialogCancelClicked() { + fun onNotificationsDeniedDialogCancelClicked() { isNeedToShowNotificationsDeniedDialog.update { false } disableBackgroundLongPoll() } - override fun onNotificationsDeniedDialogDismissed() { + fun onNotificationsDeniedDialogDismissed() { isNeedToShowNotificationsDeniedDialog.update { false } } - override fun onNotificationsRationaleDialogDismissed() { + fun onNotificationsRationaleDialogDismissed() { isNeedToShowNotificationsRationaleDialog.update { false } } - override fun onNotificationsRationaleDialogCancelClicked() { + fun onNotificationsRationaleDialogCancelClicked() { isNeedToShowNotificationsRationaleDialog.update { false } disableBackgroundLongPoll() } - override fun onUserAuthenticated() { + fun onUserAuthenticated() { loadProfile() } @@ -202,11 +171,17 @@ class MainViewModelImpl( private fun loadAccounts() { viewModelScope.launch(Dispatchers.IO) { - val currentAccount = getCurrentAccountUseCase() + val currentAccount = getCurrentAccountUseCase()?.mapToDto() logger.debug( - this@MainViewModelImpl::class, - "loadAccounts(): currentAccount: $currentAccount" + this@MainViewModel::class, + "loadAccounts(): currentAccount: %s" + .format( + currentAccount?.copy( + accessToken = if (currentAccount.accessToken.isNotEmpty()) "" + else "null" + ) + ) ) listenLongPollState() diff --git a/app/src/main/kotlin/dev/meloda/fast/common/di/AndroidModule.kt b/app/src/main/kotlin/dev/meloda/fast/common/di/AndroidModule.kt new file mode 100644 index 00000000..f32e35e1 --- /dev/null +++ b/app/src/main/kotlin/dev/meloda/fast/common/di/AndroidModule.kt @@ -0,0 +1,18 @@ +package dev.meloda.fast.common.di + +import android.content.Context +import android.content.res.Resources +import android.net.ConnectivityManager +import android.os.PowerManager +import androidx.preference.PreferenceManager +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.module + +val androidModule = module { + // TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class + factoryOf(PreferenceManager::getDefaultSharedPreferences) + factory { androidContext().resources } + factory { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager } + factory { androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } +} 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 54bccc4e..1f4de979 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 @@ -1,16 +1,13 @@ package dev.meloda.fast.common.di -import android.content.Context -import android.content.res.Resources -import android.os.PowerManager -import androidx.preference.PreferenceManager import coil.ImageLoader import coil.annotation.ExperimentalCoilApi -import dev.meloda.fast.MainViewModelImpl +import dev.meloda.fast.MainViewModel import dev.meloda.fast.auth.authModule import dev.meloda.fast.chatmaterials.di.chatMaterialsModule import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.LongPollControllerImpl +import dev.meloda.fast.common.NetworkStateListener import dev.meloda.fast.common.provider.Provider import dev.meloda.fast.common.provider.ResourceProvider import dev.meloda.fast.common.provider.ResourceProviderImpl @@ -27,7 +24,7 @@ import dev.meloda.fast.profile.di.profileModule 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.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.viewModelOf import org.koin.core.qualifier.qualifier @@ -52,27 +49,24 @@ val applicationModule = module { ) includes(loggerModule) + includes(androidModule) - // TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class - singleOf(PreferenceManager::getDefaultSharedPreferences) - single { androidContext().resources } - factory { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager } + factoryOf(::ApiLanguageProvider) bind Provider::class - singleOf(::ApiLanguageProvider) bind Provider::class - - viewModelOf(::MainViewModelImpl) { - qualifier = qualifier("main") - } + viewModelOf(::MainViewModel) { qualifier = qualifier("main") } single { ImageLoader.Builder(get()) .crossfade(true) .build() - .also { it.diskCache?.directory?.toFile()?.listFiles() } + .also { + it.diskCache?.directory?.toFile()?.listFiles() + } } singleOf(::LongPollControllerImpl) bind LongPollController::class singleOf(::ResourceProviderImpl) bind ResourceProvider::class - singleOf(::NetworkObserver) + singleOf(::NetworkStateListener) + singleOf(::NetworkObserver) { qualifier = qualifier("main") } } diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt index 3e074a16..6e9d9150 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt @@ -20,7 +20,6 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.LifecycleResumeEffect import com.google.accompanist.permissions.ExperimentalPermissionsApi import dev.meloda.fast.MainViewModel -import dev.meloda.fast.MainViewModelImpl import dev.meloda.fast.common.AppConstants import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.domain.LongPollEventsHandler @@ -70,7 +69,7 @@ class MainActivity : AppCompatActivity() { setContent { val logger: FastLogger = koinInject() - val viewModel: MainViewModel = koinViewModel() + val viewModel: MainViewModel = koinViewModel() LifecycleResumeEffect(true) { viewModel.onAppResumed(intent) onPauseOrDispose {} @@ -184,7 +183,6 @@ class MainActivity : AppCompatActivity() { super.onDestroy() stopServices() get().onDestroy() - get().onDestroy() } companion object { diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/NetworkObserver.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/NetworkObserver.kt index 75c7f9c9..3fe775b2 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/NetworkObserver.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/NetworkObserver.kt @@ -1,30 +1,24 @@ package dev.meloda.fast.presentation -import android.content.Context import android.net.ConnectivityManager import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import android.os.Build +import dev.meloda.fast.common.NetworkStateListener +import dev.meloda.fast.common.model.NetworkState +import dev.meloda.fast.common.model.NetworkStatus +import dev.meloda.fast.common.model.NetworkType import dev.meloda.fast.logger.FastLogger -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import java.util.concurrent.ConcurrentHashMap -class NetworkObserver( - context: Context, - private val logger: FastLogger +internal class NetworkObserver( + private val connectivityManager: ConnectivityManager, + private val logger: FastLogger, + private val networkStateListener: NetworkStateListener ) { - private val connectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - private val networkStatus = MutableStateFlow(NetworkStatus.UNAVAILABLE) - val networkStatusFlow = networkStatus.asStateFlow() - - private val networkState = MutableStateFlow(NetworkState.DISCONNECTED) - val networkStateFlow = networkState.asStateFlow() - private val networks = ConcurrentHashMap() private var clearCallbacks: (() -> Unit)? = null @@ -40,12 +34,7 @@ class NetworkObserver( NetworkState.DISCONNECTED } - networkState.value = state - networkStatus.value = when (state) { - NetworkState.CONNECTED -> NetworkStatus.AVAILABLE - NetworkState.DISCONNECTED -> NetworkStatus.UNAVAILABLE - } - + networkStateListener.updateNetworkState(state) log("STATE: $state") } @@ -58,7 +47,8 @@ class NetworkObserver( network = network, capabilities = connectivityManager.getNetworkCapabilities(network), properties = connectivityManager.getLinkProperties(network), - status = NetworkStatus.AVAILABLE + status = NetworkStatus.AVAILABLE, + assumeInternet = true ) syncNetworkState() @@ -156,6 +146,10 @@ class NetworkObserver( refreshActiveNetwork() } + private fun log(text: String) { + logger.debug(this::class, text) + } + private fun refreshActiveNetwork() { val network = connectivityManager.activeNetwork if (network == null) { @@ -177,17 +171,14 @@ class NetworkObserver( syncNetworkState() } - private fun log(text: String) { - logger.debug(this::class, text) - } - private fun mapNetworkModel( network: Network, capabilities: NetworkCapabilities? = null, properties: LinkProperties? = null, status: NetworkStatus? = null, maxMsToLive: Long? = null, - from: NetworkModel? = null + from: NetworkModel? = null, + assumeInternet: Boolean = false ): NetworkModel { val caps = capabilities ?: from?.networkCapabilities @@ -199,12 +190,15 @@ class NetworkObserver( else -> from?.type ?: NetworkType.UNKNOWN } - val hasInternet = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - ?: from?.hasInternet - ?: false + val hasInternet = if (caps != null) { + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } else { + from?.hasInternet ?: assumeInternet + } val signalStrength = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { caps?.signalStrength } else { null @@ -224,27 +218,8 @@ class NetworkObserver( ?: connectivityManager.getLinkProperties(network) ) } - - fun onDestroy() { - clearCallbacks?.let { unregisterCallback -> - runCatching { unregisterCallback() } - .onFailure { throwable -> - logger.error( - this::class.java, - "Failed to unregister network callback", - throwable - ) - } - } - clearCallbacks = null - networks.clear() - syncNetworkState() - } } -enum class NetworkType { - CELLULAR, WIFI, UNKNOWN -} data class NetworkModel( val id: Int, @@ -261,14 +236,3 @@ data class NetworkModel( fun isInternetAvailable(): Boolean = hasInternet && isStatusOk() } - -enum class NetworkStatus { - AVAILABLE, UNAVAILABLE, LOST, BLOCKED, UNBLOCKED; - - fun isOk(): Boolean = when (this) { - AVAILABLE, UNBLOCKED -> true - UNAVAILABLE, LOST, BLOCKED -> false - } -} - -enum class NetworkState { CONNECTED, DISCONNECTED } 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 939ff487..993e83c4 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -38,13 +38,13 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import dev.meloda.fast.MainViewModel -import dev.meloda.fast.MainViewModelImpl import dev.meloda.fast.auth.authNavGraph import dev.meloda.fast.auth.captcha.presentation.CaptchaScreen import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials import dev.meloda.fast.common.LongPollController +import dev.meloda.fast.common.NetworkStateListener import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.convos.navigation.createChatScreen import dev.meloda.fast.convos.navigation.navigateToCreateChat @@ -64,6 +64,7 @@ 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.common.LocalLogger +import dev.meloda.fast.ui.common.LocalNetworkState import dev.meloda.fast.ui.common.LocalSizeConfig import dev.meloda.fast.ui.model.DeviceSize import dev.meloda.fast.ui.model.SizeConfig @@ -93,7 +94,7 @@ fun RootScreen( val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle() val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle() - val viewModel: MainViewModel = koinViewModel() + val viewModel: MainViewModel = koinViewModel() val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle() val permissionState = @@ -221,10 +222,17 @@ fun RootScreen( } } + val networkStateListener: NetworkStateListener = koinInject() + val networkState by networkStateListener.networkStateFlow.collectAsStateWithLifecycle() + LaunchedEffect(networkState) { + logger.debug("RootScreen", "NetworkState: $networkState") + } + CompositionLocalProvider( LocalThemeConfig provides themeConfig, LocalSizeConfig provides sizeConfig, - LocalUser provides currentUser + LocalUser provides currentUser, + LocalNetworkState provides networkState ) { AppTheme( useDarkTheme = themeConfig.darkMode, diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/NetworkStateListener.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/NetworkStateListener.kt new file mode 100644 index 00000000..1a2f75e9 --- /dev/null +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/NetworkStateListener.kt @@ -0,0 +1,23 @@ +package dev.meloda.fast.common + +import dev.meloda.fast.common.model.NetworkState +import dev.meloda.fast.common.model.NetworkStatus +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class NetworkStateListener { + + private val networkStatus = MutableStateFlow(NetworkStatus.UNAVAILABLE) + val networkStatusFlow = networkStatus.asStateFlow() + + private val networkState = MutableStateFlow(NetworkState.DISCONNECTED) + val networkStateFlow = networkState.asStateFlow() + + fun updateNetworkState(state: NetworkState) { + networkState.value = state + networkStatus.value = when (state) { + NetworkState.CONNECTED -> NetworkStatus.AVAILABLE + NetworkState.DISCONNECTED -> NetworkStatus.UNAVAILABLE + } + } +} diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkState.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkState.kt new file mode 100644 index 00000000..56942ea3 --- /dev/null +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkState.kt @@ -0,0 +1,3 @@ +package dev.meloda.fast.common.model + +enum class NetworkState { CONNECTED, DISCONNECTED } diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkStatus.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkStatus.kt new file mode 100644 index 00000000..06fff57c --- /dev/null +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkStatus.kt @@ -0,0 +1,10 @@ +package dev.meloda.fast.common.model + +enum class NetworkStatus { + AVAILABLE, UNAVAILABLE, LOST, BLOCKED, UNBLOCKED; + + fun isOk(): Boolean = when (this) { + AVAILABLE, UNBLOCKED -> true + UNAVAILABLE, LOST, BLOCKED -> false + } +} diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkType.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkType.kt new file mode 100644 index 00000000..4227a4cf --- /dev/null +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/model/NetworkType.kt @@ -0,0 +1,5 @@ +package dev.meloda.fast.common.model + +enum class NetworkType { + CELLULAR, WIFI, UNKNOWN +} diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/provider/ResourceProvider.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/provider/ResourceProvider.kt index 1224c085..ed0503bb 100644 --- a/core/common/src/main/kotlin/dev/meloda/fast/common/provider/ResourceProvider.kt +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/provider/ResourceProvider.kt @@ -3,9 +3,7 @@ package dev.meloda.fast.common.provider import android.content.res.Resources interface ResourceProvider { - val resources: Resources - fun getString(resId: Int): String } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/AccountDto.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/AccountDto.kt new file mode 100644 index 00000000..07b9b244 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/AccountDto.kt @@ -0,0 +1,24 @@ +package dev.meloda.fast.model + +import dev.meloda.fast.model.database.AccountEntity + +data class AccountDto( + val userId: Long, + val accessToken: String, + val fastToken: String?, + val trustedHash: String?, + val exchangeToken: String? +) { + + fun mapToEntity(): AccountEntity = AccountEntity( + userId = userId, + accessToken = accessToken, + fastToken = fastToken, + trustedHash = trustedHash, + exchangeToken = exchangeToken + ) + + override fun toString(): String { + return super.toString() + } +} 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 38c4a519..dc2d6da6 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 @@ -2,6 +2,7 @@ package dev.meloda.fast.model.database import androidx.room.Entity import androidx.room.PrimaryKey +import dev.meloda.fast.model.AccountDto @Entity(tableName = "accounts") data class AccountEntity( @@ -11,4 +12,12 @@ data class AccountEntity( val fastToken: String?, val trustedHash: String?, val exchangeToken: String? -) +) { + fun mapToDto(): AccountDto = AccountDto( + userId = userId, + accessToken = accessToken, + fastToken = fastToken, + trustedHash = trustedHash, + exchangeToken = exchangeToken + ) +} diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/common/LocalNetworkState.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/common/LocalNetworkState.kt new file mode 100644 index 00000000..44e29594 --- /dev/null +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/common/LocalNetworkState.kt @@ -0,0 +1,6 @@ +package dev.meloda.fast.ui.common + +import androidx.compose.runtime.compositionLocalOf +import dev.meloda.fast.common.model.NetworkState + +val LocalNetworkState = compositionLocalOf { NetworkState.DISCONNECTED } diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 34ffb6d3..4573be92 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -189,6 +189,7 @@ Application language Refresh Loading… + No network… Conversations Archive Friends 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 13de9e93..f33eaef0 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 @@ -28,7 +28,7 @@ import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.OAuthUseCase import dev.meloda.fast.logger.FastLogger -import dev.meloda.fast.model.database.AccountEntity +import dev.meloda.fast.model.AccountDto import dev.meloda.fast.network.OAuthErrorDomain import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers @@ -236,7 +236,7 @@ class LoginViewModel( // TODO: 30-Mar-25, Danil Nikolaev: get fast's app token - val currentAccount = AccountEntity( + val currentAccount = AccountDto( userId = userId, accessToken = accessToken, fastToken = null, @@ -251,7 +251,7 @@ class LoginViewModel( UserConfig.exchangeToken = account.exchangeToken } - accountsRepository.storeAccounts(listOf(currentAccount)) + accountsRepository.storeAccounts(listOf(currentAccount.mapToEntity())) startLongPoll() diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt index 68c148b8..7bff2e5c 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt @@ -9,12 +9,14 @@ import androidx.lifecycle.viewModelScope import coil.ImageLoader import coil.request.ImageRequest import com.conena.nanokt.collections.indexOfFirstOrNull +import dev.meloda.fast.common.NetworkStateListener 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.common.model.NetworkState import dev.meloda.fast.convos.model.ConvoDialog import dev.meloda.fast.convos.model.ConvoIntent import dev.meloda.fast.convos.model.ConvoNavigationIntent @@ -31,6 +33,7 @@ import dev.meloda.fast.domain.LongPollEventsHandler import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.domain.util.asPresentation import dev.meloda.fast.domain.util.extractAvatar +import dev.meloda.fast.logger.FastLogger import dev.meloda.fast.model.ConvosFilter import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.LongPollParsedEvent @@ -54,7 +57,9 @@ class ConvosViewModel( private val userSettings: UserSettings, private val imageLoader: ImageLoader, private val applicationContext: Context, - private val loadConvosByIdUseCase: LoadConvosByIdUseCase + private val loadConvosByIdUseCase: LoadConvosByIdUseCase, + private val networkStateListener: NetworkStateListener, + private val logger: FastLogger ) : ViewModel() { private val screenState = MutableStateFlow(ConvosScreenState.EMPTY) @@ -87,6 +92,16 @@ class ConvosViewModel( userSettings.useContactNames.listenValue(viewModelScope) { syncUiConvos() } + + networkStateListener.networkStateFlow.listenValue { state -> + logger.debug(this@ConvosViewModel::class, "network state changed: $state") + + if (state == NetworkState.CONNECTED) { + if (screenState.value.error != null) { + onRefresh() + } + } + } } fun handleIntent(intent: ConvoIntent) { @@ -193,12 +208,12 @@ class ConvosViewModel( loadConvos() } - private fun onErrorConsumed() { + private fun clearError() { screenState.updateValue { copy(error = null) } } private fun onRefresh() { - onErrorConsumed() + clearError() loadConvos(offset = 0) } diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt index 32d8ebaa..6e1766f5 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt @@ -32,6 +32,8 @@ private fun Scope.createConvosViewModel(filter: ConvosFilter): ConvosViewModel { userSettings = get(), imageLoader = get(), applicationContext = get(), - loadConvosByIdUseCase = get() + loadConvosByIdUseCase = get(), + networkStateListener = get(), + logger = get() ) } diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt index 4d2a54b4..22064b80 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt @@ -56,11 +56,13 @@ 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.common.model.NetworkState import dev.meloda.fast.convos.model.ConvoIntent import dev.meloda.fast.convos.model.ConvosScreenState import dev.meloda.fast.convos.navigation.ConvoGraph import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.common.LocalNetworkState import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.SegmentedButtonItem @@ -156,6 +158,8 @@ fun ConvosScreen( animationSpec = tween(durationMillis = 50) ) + val networkState = LocalNetworkState.current + Scaffold( modifier = Modifier.fillMaxSize(), contentWindowInsets = WindowInsets.statusBars, @@ -166,6 +170,7 @@ fun ConvosScreen( Text( text = stringResource( id = when { + networkState == NetworkState.DISCONNECTED -> R.string.title_no_network screenState.isLoading -> R.string.title_loading isArchive -> R.string.title_archive else -> R.string.title_convos 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 1f13eb44..8b685344 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 @@ -26,6 +26,7 @@ import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.GetCurrentAccountUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.logger.FastLogger +import dev.meloda.fast.model.AccountDto import dev.meloda.fast.model.database.AccountEntity import dev.meloda.fast.settings.model.HapticType import dev.meloda.fast.settings.model.SettingsDialog @@ -149,13 +150,14 @@ class SettingsViewModel( UserConfig.currentUserId = user.id val account = getCurrentAccountUseCase() + ?.mapToDto() ?.copy( userId = user.id, accessToken = accessToken, fastToken = null, exchangeToken = exchangeToken, trustedHash = trustedHash - ) ?: AccountEntity( + ) ?: AccountDto( userId = user.id, accessToken = accessToken, fastToken = null, @@ -163,7 +165,7 @@ class SettingsViewModel( exchangeToken = exchangeToken ) - accountsRepository.storeAccounts(listOf(account)) + accountsRepository.storeAccounts(listOf(account.mapToEntity())) screenEffect.tryEmit( SettingsEffect.Navigate(SettingsNavigationIntent.Restart)