1 Commits

Author SHA1 Message Date
melod1n 66deae6fc3 temp 2026-05-31 06:48:21 +03:00
20 changed files with 210 additions and 150 deletions
@@ -29,61 +29,30 @@ import dev.meloda.fast.navigation.Main
import dev.meloda.fast.settings.navigation.Settings import dev.meloda.fast.settings.navigation.Settings
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
interface MainViewModel { class MainViewModel(
val startDestination: StateFlow<Any?>
val isNeedToReplaceWithAuth: StateFlow<Boolean>
val currentUser: StateFlow<VkUser?>
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean>
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(
private val getCurrentAccountUseCase: GetCurrentAccountUseCase, private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val loadUserByIdUseCase: LoadUserByIdUseCase, private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val userSettings: UserSettings, private val userSettings: UserSettings,
private val longPollController: LongPollController, private val longPollController: LongPollController,
private val logger: FastLogger private val logger: FastLogger
) : MainViewModel, ViewModel() { ) : ViewModel() {
override val startDestination = MutableStateFlow<Any?>(null) val startDestination = MutableStateFlow<Any?>(null)
override val isNeedToReplaceWithAuth = MutableStateFlow(false) val isNeedToReplaceWithAuth = MutableStateFlow(false)
override val currentUser = MutableStateFlow<VkUser?>(null) val currentUser = MutableStateFlow<VkUser?>(null)
override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false) val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false) val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false) val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
override val isNeedToRequestNotifications = MutableStateFlow(false) val isNeedToRequestNotifications = MutableStateFlow(false)
private var openNotificationsSettings = false private var openNotificationsSettings = false
private var openAppSettings = false private var openAppSettings = false
override fun onError(error: BaseError) { fun onError(error: BaseError) {
when (error) { when (error) {
BaseError.SessionExpired, BaseError.SessionExpired,
BaseError.AccountBlocked -> { BaseError.AccountBlocked -> {
@@ -94,11 +63,11 @@ class MainViewModelImpl(
} }
} }
override fun onNavigatedToAuth() { fun onNavigatedToAuth() {
isNeedToReplaceWithAuth.update { false } isNeedToReplaceWithAuth.update { false }
} }
override fun onAppResumed(intent: Intent) { fun onAppResumed(intent: Intent) {
openNotificationsSettings = openNotificationsSettings =
intent.hasCategory(NotificationCompat.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) intent.hasCategory(NotificationCompat.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
openAppSettings = openAppSettings =
@@ -125,7 +94,7 @@ class MainViewModelImpl(
} }
@ExperimentalPermissionsApi @ExperimentalPermissionsApi
override fun onPermissionCheckStatus(status: PermissionStatus) { fun onPermissionCheckStatus(status: PermissionStatus) {
isNeedToCheckNotificationsPermission.update { false } isNeedToCheckNotificationsPermission.update { false }
when (status) { when (status) {
@@ -147,33 +116,33 @@ class MainViewModelImpl(
} }
} }
override fun onPermissionsRequested() { fun onPermissionsRequested() {
isNeedToRequestNotifications.update { false } isNeedToRequestNotifications.update { false }
} }
override fun onNotificationsDeniedDialogConfirmClicked() { fun onNotificationsDeniedDialogConfirmClicked() {
isNeedToRequestNotifications.update { true } isNeedToRequestNotifications.update { true }
} }
override fun onNotificationsDeniedDialogCancelClicked() { fun onNotificationsDeniedDialogCancelClicked() {
isNeedToShowNotificationsDeniedDialog.update { false } isNeedToShowNotificationsDeniedDialog.update { false }
disableBackgroundLongPoll() disableBackgroundLongPoll()
} }
override fun onNotificationsDeniedDialogDismissed() { fun onNotificationsDeniedDialogDismissed() {
isNeedToShowNotificationsDeniedDialog.update { false } isNeedToShowNotificationsDeniedDialog.update { false }
} }
override fun onNotificationsRationaleDialogDismissed() { fun onNotificationsRationaleDialogDismissed() {
isNeedToShowNotificationsRationaleDialog.update { false } isNeedToShowNotificationsRationaleDialog.update { false }
} }
override fun onNotificationsRationaleDialogCancelClicked() { fun onNotificationsRationaleDialogCancelClicked() {
isNeedToShowNotificationsRationaleDialog.update { false } isNeedToShowNotificationsRationaleDialog.update { false }
disableBackgroundLongPoll() disableBackgroundLongPoll()
} }
override fun onUserAuthenticated() { fun onUserAuthenticated() {
loadProfile() loadProfile()
} }
@@ -202,11 +171,17 @@ class MainViewModelImpl(
private fun loadAccounts() { private fun loadAccounts() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val currentAccount = getCurrentAccountUseCase() val currentAccount = getCurrentAccountUseCase()?.mapToDto()
logger.debug( logger.debug(
this@MainViewModelImpl::class, this@MainViewModel::class,
"loadAccounts(): currentAccount: $currentAccount" "loadAccounts(): currentAccount: %s"
.format(
currentAccount?.copy(
accessToken = if (currentAccount.accessToken.isNotEmpty()) "<redacted>"
else "null"
)
)
) )
listenLongPollState() listenLongPollState()
@@ -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<Resources> { androidContext().resources }
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
factory<ConnectivityManager> { androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
}
@@ -1,16 +1,13 @@
package dev.meloda.fast.common.di 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.ImageLoader
import coil.annotation.ExperimentalCoilApi import coil.annotation.ExperimentalCoilApi
import dev.meloda.fast.MainViewModelImpl import dev.meloda.fast.MainViewModel
import dev.meloda.fast.auth.authModule import dev.meloda.fast.auth.authModule
import dev.meloda.fast.chatmaterials.di.chatMaterialsModule import dev.meloda.fast.chatmaterials.di.chatMaterialsModule
import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.LongPollControllerImpl 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.Provider
import dev.meloda.fast.common.provider.ResourceProvider import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.common.provider.ResourceProviderImpl 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.provider.ApiLanguageProvider
import dev.meloda.fast.service.longpolling.di.longPollModule import dev.meloda.fast.service.longpolling.di.longPollModule
import dev.meloda.fast.settings.di.settingsModule 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.singleOf
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.qualifier import org.koin.core.qualifier.qualifier
@@ -52,27 +49,24 @@ val applicationModule = module {
) )
includes(loggerModule) includes(loggerModule)
includes(androidModule)
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class factoryOf(::ApiLanguageProvider) bind Provider::class
singleOf(PreferenceManager::getDefaultSharedPreferences)
single<Resources> { androidContext().resources }
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
singleOf(::ApiLanguageProvider) bind Provider::class viewModelOf(::MainViewModel) { qualifier = qualifier("main") }
viewModelOf(::MainViewModelImpl) {
qualifier = qualifier("main")
}
single<ImageLoader> { single<ImageLoader> {
ImageLoader.Builder(get()) ImageLoader.Builder(get())
.crossfade(true) .crossfade(true)
.build() .build()
.also { it.diskCache?.directory?.toFile()?.listFiles() } .also {
it.diskCache?.directory?.toFile()?.listFiles()
}
} }
singleOf(::LongPollControllerImpl) bind LongPollController::class singleOf(::LongPollControllerImpl) bind LongPollController::class
singleOf(::ResourceProviderImpl) bind ResourceProvider::class singleOf(::ResourceProviderImpl) bind ResourceProvider::class
singleOf(::NetworkObserver) singleOf(::NetworkStateListener)
singleOf(::NetworkObserver) { qualifier = qualifier("main") }
} }
@@ -20,7 +20,6 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import dev.meloda.fast.MainViewModel import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.common.AppConstants import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.LongPollEventsHandler import dev.meloda.fast.domain.LongPollEventsHandler
@@ -70,7 +69,7 @@ class MainActivity : AppCompatActivity() {
setContent { setContent {
val logger: FastLogger = koinInject() val logger: FastLogger = koinInject()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>() val viewModel: MainViewModel = koinViewModel()
LifecycleResumeEffect(true) { LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent) viewModel.onAppResumed(intent)
onPauseOrDispose {} onPauseOrDispose {}
@@ -184,7 +183,6 @@ class MainActivity : AppCompatActivity() {
super.onDestroy() super.onDestroy()
stopServices() stopServices()
get<LongPollEventsHandler>().onDestroy() get<LongPollEventsHandler>().onDestroy()
get<NetworkObserver>().onDestroy()
} }
companion object { companion object {
@@ -1,30 +1,24 @@
package dev.meloda.fast.presentation package dev.meloda.fast.presentation
import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.LinkProperties import android.net.LinkProperties
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest 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 dev.meloda.fast.logger.FastLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class NetworkObserver( internal class NetworkObserver(
context: Context, private val connectivityManager: ConnectivityManager,
private val logger: FastLogger 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<Network, NetworkModel>() private val networks = ConcurrentHashMap<Network, NetworkModel>()
private var clearCallbacks: (() -> Unit)? = null private var clearCallbacks: (() -> Unit)? = null
@@ -40,12 +34,7 @@ class NetworkObserver(
NetworkState.DISCONNECTED NetworkState.DISCONNECTED
} }
networkState.value = state networkStateListener.updateNetworkState(state)
networkStatus.value = when (state) {
NetworkState.CONNECTED -> NetworkStatus.AVAILABLE
NetworkState.DISCONNECTED -> NetworkStatus.UNAVAILABLE
}
log("STATE: $state") log("STATE: $state")
} }
@@ -58,7 +47,8 @@ class NetworkObserver(
network = network, network = network,
capabilities = connectivityManager.getNetworkCapabilities(network), capabilities = connectivityManager.getNetworkCapabilities(network),
properties = connectivityManager.getLinkProperties(network), properties = connectivityManager.getLinkProperties(network),
status = NetworkStatus.AVAILABLE status = NetworkStatus.AVAILABLE,
assumeInternet = true
) )
syncNetworkState() syncNetworkState()
@@ -156,6 +146,10 @@ class NetworkObserver(
refreshActiveNetwork() refreshActiveNetwork()
} }
private fun log(text: String) {
logger.debug(this::class, text)
}
private fun refreshActiveNetwork() { private fun refreshActiveNetwork() {
val network = connectivityManager.activeNetwork val network = connectivityManager.activeNetwork
if (network == null) { if (network == null) {
@@ -177,17 +171,14 @@ class NetworkObserver(
syncNetworkState() syncNetworkState()
} }
private fun log(text: String) {
logger.debug(this::class, text)
}
private fun mapNetworkModel( private fun mapNetworkModel(
network: Network, network: Network,
capabilities: NetworkCapabilities? = null, capabilities: NetworkCapabilities? = null,
properties: LinkProperties? = null, properties: LinkProperties? = null,
status: NetworkStatus? = null, status: NetworkStatus? = null,
maxMsToLive: Long? = null, maxMsToLive: Long? = null,
from: NetworkModel? = null from: NetworkModel? = null,
assumeInternet: Boolean = false
): NetworkModel { ): NetworkModel {
val caps = capabilities val caps = capabilities
?: from?.networkCapabilities ?: from?.networkCapabilities
@@ -199,12 +190,15 @@ class NetworkObserver(
else -> from?.type ?: NetworkType.UNKNOWN else -> from?.type ?: NetworkType.UNKNOWN
} }
val hasInternet = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) val hasInternet = if (caps != null) {
?: from?.hasInternet caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
?: false caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else {
from?.hasInternet ?: assumeInternet
}
val signalStrength = 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 caps?.signalStrength
} else { } else {
null null
@@ -224,27 +218,8 @@ class NetworkObserver(
?: connectivityManager.getLinkProperties(network) ?: 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( data class NetworkModel(
val id: Int, val id: Int,
@@ -261,14 +236,3 @@ data class NetworkModel(
fun isInternetAvailable(): Boolean = hasInternet && isStatusOk() 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 }
@@ -38,13 +38,13 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import dev.meloda.fast.MainViewModel import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.auth.authNavGraph import dev.meloda.fast.auth.authNavGraph
import dev.meloda.fast.auth.captcha.presentation.CaptchaScreen import dev.meloda.fast.auth.captcha.presentation.CaptchaScreen
import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.auth.navigateToAuth
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.NetworkStateListener
import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.convos.navigation.createChatScreen import dev.meloda.fast.convos.navigation.createChatScreen
import dev.meloda.fast.convos.navigation.navigateToCreateChat 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.settings.navigation.settingsScreen
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.LocalLogger 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.common.LocalSizeConfig
import dev.meloda.fast.ui.model.DeviceSize import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.model.SizeConfig import dev.meloda.fast.ui.model.SizeConfig
@@ -93,7 +94,7 @@ fun RootScreen(
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle() val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle() val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>() val viewModel: MainViewModel = koinViewModel()
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle() val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
val permissionState = 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( CompositionLocalProvider(
LocalThemeConfig provides themeConfig, LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig, LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser LocalUser provides currentUser,
LocalNetworkState provides networkState
) { ) {
AppTheme( AppTheme(
useDarkTheme = themeConfig.darkMode, useDarkTheme = themeConfig.darkMode,
@@ -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
}
}
}
@@ -0,0 +1,3 @@
package dev.meloda.fast.common.model
enum class NetworkState { CONNECTED, DISCONNECTED }
@@ -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
}
}
@@ -0,0 +1,5 @@
package dev.meloda.fast.common.model
enum class NetworkType {
CELLULAR, WIFI, UNKNOWN
}
@@ -3,9 +3,7 @@ package dev.meloda.fast.common.provider
import android.content.res.Resources import android.content.res.Resources
interface ResourceProvider { interface ResourceProvider {
val resources: Resources val resources: Resources
fun getString(resId: Int): String fun getString(resId: Int): String
} }
@@ -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()
}
}
@@ -2,6 +2,7 @@ package dev.meloda.fast.model.database
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import dev.meloda.fast.model.AccountDto
@Entity(tableName = "accounts") @Entity(tableName = "accounts")
data class AccountEntity( data class AccountEntity(
@@ -11,4 +12,12 @@ data class AccountEntity(
val fastToken: String?, val fastToken: String?,
val trustedHash: String?, val trustedHash: String?,
val exchangeToken: String? val exchangeToken: String?
) {
fun mapToDto(): AccountDto = AccountDto(
userId = userId,
accessToken = accessToken,
fastToken = fastToken,
trustedHash = trustedHash,
exchangeToken = exchangeToken
) )
}
@@ -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 }
+1
View File
@@ -189,6 +189,7 @@
<string name="title_application_language">Application language</string> <string name="title_application_language">Application language</string>
<string name="action_refresh">Refresh</string> <string name="action_refresh">Refresh</string>
<string name="title_loading">Loading&#8230;</string> <string name="title_loading">Loading&#8230;</string>
<string name="title_no_network">No network&#8230;</string>
<string name="title_convos">Conversations</string> <string name="title_convos">Conversations</string>
<string name="title_archive">Archive</string> <string name="title_archive">Archive</string>
<string name="title_friends">Friends</string> <string name="title_friends">Friends</string>
@@ -28,7 +28,7 @@ import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.OAuthUseCase import dev.meloda.fast.domain.OAuthUseCase
import dev.meloda.fast.logger.FastLogger 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 dev.meloda.fast.network.OAuthErrorDomain
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -236,7 +236,7 @@ class LoginViewModel(
// TODO: 30-Mar-25, Danil Nikolaev: get fast's app token // TODO: 30-Mar-25, Danil Nikolaev: get fast's app token
val currentAccount = AccountEntity( val currentAccount = AccountDto(
userId = userId, userId = userId,
accessToken = accessToken, accessToken = accessToken,
fastToken = null, fastToken = null,
@@ -251,7 +251,7 @@ class LoginViewModel(
UserConfig.exchangeToken = account.exchangeToken UserConfig.exchangeToken = account.exchangeToken
} }
accountsRepository.storeAccounts(listOf(currentAccount)) accountsRepository.storeAccounts(listOf(currentAccount.mapToEntity()))
startLongPoll() startLongPoll()
@@ -9,12 +9,14 @@ import androidx.lifecycle.viewModelScope
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.conena.nanokt.collections.indexOfFirstOrNull import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.NetworkStateListener
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.createTimerFlow import dev.meloda.fast.common.extensions.createTimerFlow
import dev.meloda.fast.common.extensions.findWithIndex import dev.meloda.fast.common.extensions.findWithIndex
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.extensions.updateValue 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.ConvoDialog
import dev.meloda.fast.convos.model.ConvoIntent import dev.meloda.fast.convos.model.ConvoIntent
import dev.meloda.fast.convos.model.ConvoNavigationIntent 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.MessagesUseCase
import dev.meloda.fast.domain.util.asPresentation import dev.meloda.fast.domain.util.asPresentation
import dev.meloda.fast.domain.util.extractAvatar import dev.meloda.fast.domain.util.extractAvatar
import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.ConvosFilter import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.LongPollParsedEvent
@@ -54,7 +57,9 @@ class ConvosViewModel(
private val userSettings: UserSettings, private val userSettings: UserSettings,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
private val applicationContext: Context, private val applicationContext: Context,
private val loadConvosByIdUseCase: LoadConvosByIdUseCase private val loadConvosByIdUseCase: LoadConvosByIdUseCase,
private val networkStateListener: NetworkStateListener,
private val logger: FastLogger
) : ViewModel() { ) : ViewModel() {
private val screenState = MutableStateFlow(ConvosScreenState.EMPTY) private val screenState = MutableStateFlow(ConvosScreenState.EMPTY)
@@ -87,6 +92,16 @@ class ConvosViewModel(
userSettings.useContactNames.listenValue(viewModelScope) { userSettings.useContactNames.listenValue(viewModelScope) {
syncUiConvos() 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) { fun handleIntent(intent: ConvoIntent) {
@@ -193,12 +208,12 @@ class ConvosViewModel(
loadConvos() loadConvos()
} }
private fun onErrorConsumed() { private fun clearError() {
screenState.updateValue { copy(error = null) } screenState.updateValue { copy(error = null) }
} }
private fun onRefresh() { private fun onRefresh() {
onErrorConsumed() clearError()
loadConvos(offset = 0) loadConvos(offset = 0)
} }
@@ -32,6 +32,8 @@ private fun Scope.createConvosViewModel(filter: ConvosFilter): ConvosViewModel {
userSettings = get(), userSettings = get(),
imageLoader = get(), imageLoader = get(),
applicationContext = get(), applicationContext = get(),
loadConvosByIdUseCase = get() loadConvosByIdUseCase = get(),
networkStateListener = get(),
logger = get()
) )
} }
@@ -56,11 +56,13 @@ import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials 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.ConvoIntent
import dev.meloda.fast.convos.model.ConvosScreenState import dev.meloda.fast.convos.model.ConvosScreenState
import dev.meloda.fast.convos.navigation.ConvoGraph import dev.meloda.fast.convos.navigation.ConvoGraph
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.ui.R 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.FullScreenContainedLoader
import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.components.SegmentedButtonItem import dev.meloda.fast.ui.components.SegmentedButtonItem
@@ -156,6 +158,8 @@ fun ConvosScreen(
animationSpec = tween(durationMillis = 50) animationSpec = tween(durationMillis = 50)
) )
val networkState = LocalNetworkState.current
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars, contentWindowInsets = WindowInsets.statusBars,
@@ -166,6 +170,7 @@ fun ConvosScreen(
Text( Text(
text = stringResource( text = stringResource(
id = when { id = when {
networkState == NetworkState.DISCONNECTED -> R.string.title_no_network
screenState.isLoading -> R.string.title_loading screenState.isLoading -> R.string.title_loading
isArchive -> R.string.title_archive isArchive -> R.string.title_archive
else -> R.string.title_convos else -> R.string.title_convos
@@ -26,6 +26,7 @@ import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.GetCurrentAccountUseCase import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.logger.FastLogger import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.AccountDto
import dev.meloda.fast.model.database.AccountEntity import dev.meloda.fast.model.database.AccountEntity
import dev.meloda.fast.settings.model.HapticType import dev.meloda.fast.settings.model.HapticType
import dev.meloda.fast.settings.model.SettingsDialog import dev.meloda.fast.settings.model.SettingsDialog
@@ -149,13 +150,14 @@ class SettingsViewModel(
UserConfig.currentUserId = user.id UserConfig.currentUserId = user.id
val account = getCurrentAccountUseCase() val account = getCurrentAccountUseCase()
?.mapToDto()
?.copy( ?.copy(
userId = user.id, userId = user.id,
accessToken = accessToken, accessToken = accessToken,
fastToken = null, fastToken = null,
exchangeToken = exchangeToken, exchangeToken = exchangeToken,
trustedHash = trustedHash trustedHash = trustedHash
) ?: AccountEntity( ) ?: AccountDto(
userId = user.id, userId = user.id,
accessToken = accessToken, accessToken = accessToken,
fastToken = null, fastToken = null,
@@ -163,7 +165,7 @@ class SettingsViewModel(
exchangeToken = exchangeToken exchangeToken = exchangeToken
) )
accountsRepository.storeAccounts(listOf(account)) accountsRepository.storeAccounts(listOf(account.mapToEntity()))
screenEffect.tryEmit( screenEffect.tryEmit(
SettingsEffect.Navigate(SettingsNavigationIntent.Restart) SettingsEffect.Navigate(SettingsNavigationIntent.Restart)