a lot of improvements for long polling service and notifications
This commit is contained in:
@@ -182,5 +182,4 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.kotlin.serialization)
|
implementation(libs.kotlin.serialization)
|
||||||
|
|
||||||
implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
package com.meloda.app.fast
|
package com.meloda.app.fast
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.PermissionStatus
|
||||||
|
import com.meloda.app.fast.auth.AuthGraph
|
||||||
import com.meloda.app.fast.common.UserConfig
|
import com.meloda.app.fast.common.UserConfig
|
||||||
|
import com.meloda.app.fast.common.extensions.ifEmpty
|
||||||
|
import com.meloda.app.fast.common.extensions.listenValue
|
||||||
import com.meloda.app.fast.common.extensions.setValue
|
import com.meloda.app.fast.common.extensions.setValue
|
||||||
import com.meloda.app.fast.common.extensions.updateValue
|
import com.meloda.app.fast.data.db.GetCurrentAccountUseCase
|
||||||
import com.meloda.app.fast.data.db.AccountsRepository
|
|
||||||
import com.meloda.app.fast.datastore.SettingsController
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
import com.meloda.app.fast.datastore.SettingsKeys
|
|
||||||
import com.meloda.app.fast.datastore.UserSettings
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
|
import com.meloda.app.fast.datastore.model.LongPollState
|
||||||
import com.meloda.app.fast.model.BaseError
|
import com.meloda.app.fast.model.BaseError
|
||||||
import com.meloda.app.fast.model.LongPollState
|
import com.meloda.app.fast.navigation.Main
|
||||||
import com.meloda.app.fast.model.MainScreenState
|
|
||||||
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.StateFlow
|
||||||
@@ -21,30 +27,33 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
interface MainViewModel {
|
interface MainViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<MainScreenState>
|
val startDestination: StateFlow<Any?>
|
||||||
val isNeedToOpenAuth: StateFlow<Boolean>
|
val isNeedToReplaceWithAuth: StateFlow<Boolean>
|
||||||
|
|
||||||
val longPollState: StateFlow<LongPollState>
|
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
|
||||||
val startOnlineService: StateFlow<Boolean>
|
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
|
||||||
|
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
|
||||||
fun useDynamicColorsChanged(use: Boolean)
|
val isNeedToRequestNotifications: StateFlow<Boolean>
|
||||||
|
|
||||||
fun useDarkThemeChanged(use: Boolean)
|
|
||||||
|
|
||||||
fun onRequestNotificationsPermissionClicked(fromRationale: Boolean)
|
|
||||||
fun onNotificationsAlertNegativeClicked()
|
|
||||||
|
|
||||||
fun onNotificationsRequested()
|
|
||||||
|
|
||||||
fun onAppPermissionsOpened()
|
|
||||||
|
|
||||||
fun onError(error: BaseError)
|
fun onError(error: BaseError)
|
||||||
|
|
||||||
fun onNavigatedToAuth()
|
fun onNavigatedToAuth()
|
||||||
|
|
||||||
|
fun onAppResumed()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
fun onPermissionCheckStatus(status: PermissionStatus)
|
||||||
|
fun onPermissionsRequested()
|
||||||
|
|
||||||
|
fun onNotificationsDeniedDialogConfirmClicked()
|
||||||
|
fun onNotificationsDeniedDialogCancelClicked()
|
||||||
|
fun onNotificationsDeniedDialogDismissed()
|
||||||
|
fun onNotificationsRationaleDialogDismissed()
|
||||||
|
fun onNotificationsRationaleDialogCancelClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainViewModelImpl(
|
class MainViewModelImpl(
|
||||||
private val accountsRepository: AccountsRepository,
|
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
|
||||||
private val userSettings: UserSettings
|
private val userSettings: UserSettings
|
||||||
) : MainViewModel, ViewModel() {
|
) : MainViewModel, ViewModel() {
|
||||||
|
|
||||||
@@ -52,99 +61,137 @@ class MainViewModelImpl(
|
|||||||
loadAccounts()
|
loadAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
|
override val startDestination = MutableStateFlow<Any?>(null)
|
||||||
override val isNeedToOpenAuth = MutableStateFlow(false)
|
override val isNeedToReplaceWithAuth = MutableStateFlow(false)
|
||||||
|
|
||||||
override val longPollState = MutableStateFlow(
|
override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
|
||||||
if (SettingsController.getBoolean(
|
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
|
||||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
override val isNeedToRequestNotifications = MutableStateFlow(false)
|
||||||
)
|
|
||||||
) {
|
|
||||||
LongPollState.ForegroundService
|
|
||||||
} else {
|
|
||||||
LongPollState.DefaultService
|
|
||||||
}
|
|
||||||
)
|
|
||||||
override val startOnlineService = MutableStateFlow(
|
|
||||||
SettingsController.getBoolean(
|
|
||||||
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS,
|
|
||||||
SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun useDynamicColorsChanged(use: Boolean) {
|
|
||||||
screenState.updateValue(screenState.value.copy(useDynamicColors = use))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun useDarkThemeChanged(use: Boolean) {
|
|
||||||
screenState.updateValue(screenState.value.copy(useDarkTheme = use))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) {
|
|
||||||
screenState.setValue { old ->
|
|
||||||
if (fromRationale) {
|
|
||||||
old.copy(isNeedToOpenAppPermissions = true)
|
|
||||||
} else {
|
|
||||||
old.copy(isNeedToRequestNotifications = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNotificationsAlertNegativeClicked() {
|
|
||||||
SettingsController.edit {
|
|
||||||
putBoolean(
|
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
userSettings.setLongPollBackground(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNotificationsRequested() {
|
|
||||||
screenState.setValue { old -> old.copy(isNeedToRequestNotifications = false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAppPermissionsOpened() {
|
|
||||||
screenState.setValue { old -> old.copy(isNeedToOpenAppPermissions = false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(error: BaseError) {
|
override fun onError(error: BaseError) {
|
||||||
when (error) {
|
when (error) {
|
||||||
BaseError.SessionExpired -> {
|
BaseError.SessionExpired -> {
|
||||||
isNeedToOpenAuth.update { true }
|
isNeedToReplaceWithAuth.update { true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToAuth() {
|
override fun onNavigatedToAuth() {
|
||||||
isNeedToOpenAuth.update { false }
|
isNeedToReplaceWithAuth.update { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAppResumed() {
|
||||||
|
if (isNeedToShowNotificationsRationaleDialog.value) {
|
||||||
|
isNeedToCheckNotificationsPermission.update { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
userSettings.onLanguageChanged(
|
||||||
|
AppCompatDelegate.getApplicationLocales()
|
||||||
|
.toLanguageTags()
|
||||||
|
.ifEmpty { null }
|
||||||
|
?: LocaleListCompat.getDefault()
|
||||||
|
.toLanguageTags()
|
||||||
|
.split(",")
|
||||||
|
.firstOrNull()
|
||||||
|
.orEmpty()
|
||||||
|
.take(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
userSettings.updateUsingDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalPermissionsApi
|
||||||
|
override fun onPermissionCheckStatus(status: PermissionStatus) {
|
||||||
|
isNeedToCheckNotificationsPermission.update { false }
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
is PermissionStatus.Denied -> {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||||
|
|
||||||
|
if (status.shouldShowRationale) {
|
||||||
|
isNeedToShowNotificationsRationaleDialog.update { true }
|
||||||
|
} else {
|
||||||
|
isNeedToShowNotificationsDeniedDialog.update { true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionStatus.Granted -> {
|
||||||
|
if (isNeedToShowNotificationsRationaleDialog.value) {
|
||||||
|
isNeedToShowNotificationsRationaleDialog.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPermissionsRequested() {
|
||||||
|
isNeedToRequestNotifications.update { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationsDeniedDialogConfirmClicked() {
|
||||||
|
isNeedToRequestNotifications.update { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationsDeniedDialogCancelClicked() {
|
||||||
|
isNeedToShowNotificationsDeniedDialog.update { false }
|
||||||
|
disableBackgroundLongPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationsDeniedDialogDismissed() {
|
||||||
|
isNeedToShowNotificationsDeniedDialog.update { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationsRationaleDialogDismissed() {
|
||||||
|
isNeedToShowNotificationsRationaleDialog.update { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationsRationaleDialogCancelClicked() {
|
||||||
|
isNeedToShowNotificationsRationaleDialog.update { false }
|
||||||
|
disableBackgroundLongPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listenLongPollState() {
|
||||||
|
userSettings.longPollStateToApply.listenValue { newState ->
|
||||||
|
if (newState == LongPollState.Background) {
|
||||||
|
isNeedToCheckNotificationsPermission.update { true }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAccounts() {
|
private fun loadAccounts() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val accounts = accountsRepository.getAccounts()
|
val currentAccount = getCurrentAccountUseCase()
|
||||||
|
|
||||||
Log.d("MainViewModel", "initUserConfig: accounts: $accounts")
|
Log.d("MainViewModel", "currentAccount: $currentAccount")
|
||||||
|
|
||||||
if (accounts.isNotEmpty()) {
|
listenLongPollState()
|
||||||
val currentAccount = accounts.find { it.userId == UserConfig.currentUserId }
|
|
||||||
if (currentAccount != null) {
|
if (currentAccount != null) {
|
||||||
UserConfig.apply {
|
UserConfig.apply {
|
||||||
this.userId = currentAccount.userId
|
this.userId = currentAccount.userId
|
||||||
this.accessToken = currentAccount.accessToken
|
this.accessToken = currentAccount.accessToken
|
||||||
this.fastToken = currentAccount.fastToken
|
this.fastToken = currentAccount.fastToken
|
||||||
this.trustedHash = currentAccount.trustedHash
|
this.trustedHash = currentAccount.trustedHash
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userSettings.setLongPollStateToApply(
|
||||||
|
if (SettingsController.isLongPollInBackgroundEnabled) {
|
||||||
|
LongPollState.Background
|
||||||
|
} else {
|
||||||
|
LongPollState.InApp
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.setValue { old ->
|
startDestination.setValue {
|
||||||
old.copy(
|
if (currentAccount == null) AuthGraph
|
||||||
accounts = accounts,
|
else Main
|
||||||
accountsLoaded = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun disableBackgroundLongPoll() {
|
||||||
|
SettingsController.isLongPollInBackgroundEnabled = false
|
||||||
|
userSettings.setLongPollStateToApply(LongPollState.InApp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.meloda.app.fast.model
|
|
||||||
|
|
||||||
sealed class LongPollState {
|
|
||||||
data object ForegroundService : LongPollState()
|
|
||||||
data object DefaultService : LongPollState()
|
|
||||||
data object Stop : LongPollState()
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.meloda.app.fast.model
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import com.meloda.app.fast.model.database.AccountEntity
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class MainScreenState(
|
|
||||||
val accounts: List<AccountEntity>,
|
|
||||||
val accountsLoaded: Boolean,
|
|
||||||
val useDarkTheme: Boolean,
|
|
||||||
val useDynamicColors: Boolean,
|
|
||||||
val isNeedToRequestNotifications: Boolean,
|
|
||||||
val isNeedToOpenAppPermissions: Boolean
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val EMPTY: MainScreenState = MainScreenState(
|
|
||||||
accounts = emptyList(),
|
|
||||||
accountsLoaded = false,
|
|
||||||
useDarkTheme = false,
|
|
||||||
useDynamicColors = false,
|
|
||||||
isNeedToRequestNotifications = false,
|
|
||||||
isNeedToOpenAppPermissions = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.meloda.app.fast.model
|
|
||||||
|
|
||||||
sealed class ServicesState {
|
|
||||||
data object Started : ServicesState()
|
|
||||||
data object Stopped : ServicesState()
|
|
||||||
data object Unknown : ServicesState()
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,13 @@ import android.app.NotificationManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.util.Log
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@@ -26,34 +22,33 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.window.core.layout.WindowWidthSizeClass
|
import com.conena.nanokt.android.content.pxToDp
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.meloda.app.fast.MainViewModel
|
import com.meloda.app.fast.MainViewModel
|
||||||
import com.meloda.app.fast.common.UiText
|
import com.meloda.app.fast.MainViewModelImpl
|
||||||
import com.meloda.app.fast.common.extensions.ifEmpty
|
import com.meloda.app.fast.common.AppConstants
|
||||||
import com.meloda.app.fast.common.extensions.isSdkAtLeast
|
import com.meloda.app.fast.common.extensions.isSdkAtLeast
|
||||||
import com.meloda.app.fast.datastore.SettingsController
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
import com.meloda.app.fast.datastore.SettingsKeys
|
|
||||||
import com.meloda.app.fast.datastore.UserSettings
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
|
import com.meloda.app.fast.datastore.model.LongPollState
|
||||||
import com.meloda.app.fast.datastore.model.ThemeConfig
|
import com.meloda.app.fast.datastore.model.ThemeConfig
|
||||||
import com.meloda.app.fast.designsystem.AppTheme
|
import com.meloda.app.fast.designsystem.AppTheme
|
||||||
import com.meloda.app.fast.designsystem.CheckPermission
|
|
||||||
import com.meloda.app.fast.designsystem.LocalTheme
|
import com.meloda.app.fast.designsystem.LocalTheme
|
||||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
|
||||||
import com.meloda.app.fast.designsystem.RequestPermission
|
|
||||||
import com.meloda.app.fast.model.MainScreenState
|
|
||||||
import com.meloda.app.fast.service.OnlineService
|
import com.meloda.app.fast.service.OnlineService
|
||||||
import com.meloda.app.fast.service.longpolling.LongPollingService
|
import com.meloda.app.fast.service.longpolling.LongPollingService
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.KoinContext
|
import org.koin.compose.KoinContext
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -76,30 +71,64 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
KoinContext {
|
KoinContext {
|
||||||
|
val context = LocalContext.current
|
||||||
val userSettings: UserSettings = koinInject()
|
val userSettings: UserSettings = koinInject()
|
||||||
|
|
||||||
LifecycleResumeEffect(true) {
|
val longPollCurrentState by userSettings.longPollCurrentState.collectAsStateWithLifecycle()
|
||||||
userSettings.onLanguageChanged(
|
val longPollStateToApply by userSettings.longPollStateToApply.collectAsStateWithLifecycle()
|
||||||
AppCompatDelegate.getApplicationLocales()
|
|
||||||
.toLanguageTags()
|
|
||||||
.ifEmpty { null }
|
|
||||||
?: LocaleListCompat.getDefault()
|
|
||||||
.toLanguageTags()
|
|
||||||
.split(",")
|
|
||||||
.firstOrNull()
|
|
||||||
.orEmpty()
|
|
||||||
.take(5)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||||
|
|
||||||
|
LifecycleResumeEffect(true) {
|
||||||
|
viewModel.onAppResumed()
|
||||||
onPauseOrDispose {}
|
onPauseOrDispose {}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(true) {
|
val permissionState =
|
||||||
userSettings.updateUsingDarkTheme()
|
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
|
||||||
|
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
|
||||||
|
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(isNeedToCheckPermission) {
|
||||||
|
if (isNeedToCheckPermission) {
|
||||||
|
viewModel.onPermissionCheckStatus(permissionState.status)
|
||||||
|
|
||||||
|
if (permissionState.status.isGranted) {
|
||||||
|
if (longPollCurrentState == LongPollState.InApp) {
|
||||||
|
toggleLongPollService(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLongPollService(
|
||||||
|
enable = true,
|
||||||
|
inBackground = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLongPollInBackground by userSettings.longPollBackground.collectAsStateWithLifecycle()
|
LaunchedEffect(isNeedToRequestPermission) {
|
||||||
toggleLongPollService(true, isLongPollInBackground)
|
if (isNeedToRequestPermission) {
|
||||||
|
viewModel.onPermissionsRequested()
|
||||||
|
permissionState.launchPermissionRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(longPollStateToApply) {
|
||||||
|
if (longPollStateToApply != LongPollState.Background) {
|
||||||
|
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
|
||||||
|
&& longPollCurrentState != longPollStateToApply
|
||||||
|
) {
|
||||||
|
toggleLongPollService(false)
|
||||||
|
Log.d("LongPoll", "recreate()")
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLongPollService(
|
||||||
|
enable = longPollStateToApply.isLaunched(),
|
||||||
|
inBackground = longPollStateToApply == LongPollState.Background
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isOnline by userSettings.online.collectAsStateWithLifecycle()
|
val isOnline by userSettings.online.collectAsStateWithLifecycle()
|
||||||
LifecycleResumeEffect(isOnline) {
|
LifecycleResumeEffect(isOnline) {
|
||||||
@@ -110,11 +139,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val windowAdaptiveInfo = currentWindowAdaptiveInfo()
|
val isDeviceCompact by remember(true) {
|
||||||
|
|
||||||
val isDeviceCompact by remember(windowAdaptiveInfo) {
|
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
|
context.resources.displayMetrics.widthPixels.pxToDp() <= 360
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +165,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
selectedColorScheme = currentTheme.selectedColorScheme,
|
selectedColorScheme = currentTheme.selectedColorScheme,
|
||||||
useAmoledBackground = currentTheme.usingAmoledBackground,
|
useAmoledBackground = currentTheme.usingAmoledBackground,
|
||||||
) {
|
) {
|
||||||
RootScreen()
|
RootScreen(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,40 +174,37 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun createNotificationChannels() {
|
private fun createNotificationChannels() {
|
||||||
isSdkAtLeast(Build.VERSION_CODES.O) {
|
isSdkAtLeast(Build.VERSION_CODES.O) {
|
||||||
val dialogsName = "Dialogs"
|
val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
|
||||||
val dialogsDescriptionText = "Channel for dialogs notifications"
|
val noCategoryDescriptionText = getString(UiR.string.notification_channel_no_category_description)
|
||||||
val dialogsImportance = NotificationManager.IMPORTANCE_HIGH
|
val noCategoryImportance = NotificationManager.IMPORTANCE_HIGH
|
||||||
val dialogsChannel =
|
val noCategoryChannel =
|
||||||
NotificationChannel("simple_notifications", dialogsName, dialogsImportance).apply {
|
NotificationChannel(AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED, noCategoryName, noCategoryImportance).apply {
|
||||||
description = dialogsDescriptionText
|
description = noCategoryDescriptionText
|
||||||
}
|
}
|
||||||
|
|
||||||
val longPollName = "Long Polling"
|
val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
|
||||||
val longPollDescriptionText = "Channel for long polling service"
|
val longPollDescriptionText = getString(UiR.string.notification_channel_long_polling_service_description)
|
||||||
val longPollImportance = NotificationManager.IMPORTANCE_NONE
|
val longPollImportance = NotificationManager.IMPORTANCE_NONE
|
||||||
val longPollChannel =
|
val longPollChannel =
|
||||||
NotificationChannel("long_polling", longPollName, longPollImportance).apply {
|
NotificationChannel(AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING, longPollName, longPollImportance).apply {
|
||||||
description = longPollDescriptionText
|
description = longPollDescriptionText
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationManager: NotificationManager =
|
val notificationManager: NotificationManager =
|
||||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
notificationManager.createNotificationChannels(listOf(dialogsChannel, longPollChannel))
|
notificationManager.createNotificationChannels(listOf(noCategoryChannel, longPollChannel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleLongPollService(
|
private fun toggleLongPollService(
|
||||||
enable: Boolean,
|
enable: Boolean,
|
||||||
asForeground: Boolean = SettingsController.getBoolean(
|
inBackground: Boolean = SettingsController.isLongPollInBackgroundEnabled
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
|
||||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
val longPollIntent = Intent(this, LongPollingService::class.java)
|
val longPollIntent = Intent(this, LongPollingService::class.java)
|
||||||
|
|
||||||
if (asForeground) {
|
if (inBackground) {
|
||||||
ContextCompat.startForegroundService(this, longPollIntent)
|
ContextCompat.startForegroundService(this, longPollIntent)
|
||||||
} else {
|
} else {
|
||||||
startService(longPollIntent)
|
startService(longPollIntent)
|
||||||
@@ -201,104 +225,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private fun stopServices() {
|
private fun stopServices() {
|
||||||
toggleOnlineService(enable = false)
|
toggleOnlineService(enable = false)
|
||||||
|
|
||||||
val asForeground = SettingsController.getBoolean(
|
val asForeground = SettingsController.isLongPollInBackgroundEnabled
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
|
||||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!asForeground) {
|
if (!asForeground) {
|
||||||
toggleLongPollService(enable = false)
|
toggleLongPollService(enable = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setNewLanguage(newLanguage: String) {
|
|
||||||
val appLocales = AppCompatDelegate.getApplicationLocales()
|
|
||||||
if (newLanguage.isEmpty()) {
|
|
||||||
if (!appLocales.isEmpty) {
|
|
||||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!appLocales.toLanguageTags().startsWith(newLanguage)) {
|
|
||||||
val newLocale = LocaleListCompat.forLanguageTags(newLanguage)
|
|
||||||
AppCompatDelegate.setApplicationLocales(newLocale)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
stopServices()
|
stopServices()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
|
||||||
fun NotificationsPermissionChecker(
|
|
||||||
screenState: MainScreenState,
|
|
||||||
viewModel: MainViewModel
|
|
||||||
) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
|
||||||
|
|
||||||
val permission =
|
|
||||||
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
|
|
||||||
if (screenState.isNeedToOpenAppPermissions) {
|
|
||||||
viewModel.onAppPermissionsOpened()
|
|
||||||
|
|
||||||
LocalContext.current.apply {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
|
||||||
Uri.fromParts("package", this.packageName, null)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screenState.isNeedToRequestNotifications) {
|
|
||||||
RequestPermission(permission = permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isNeedToCheckNotificationsPermission by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
||||||
SettingsController.getBoolean(
|
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
|
||||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNeedToCheckNotificationsPermission) {
|
|
||||||
CheckPermission(
|
|
||||||
showRationale = {
|
|
||||||
MaterialDialog(
|
|
||||||
title = UiText.Resource(UiR.string.warning),
|
|
||||||
text = UiText.Simple("The application will not be able to work properly without permission to send notifications."),
|
|
||||||
confirmText = UiText.Simple("Grant"),
|
|
||||||
confirmAction = {
|
|
||||||
viewModel.onRequestNotificationsPermissionClicked(true)
|
|
||||||
},
|
|
||||||
cancelText = UiText.Resource(UiR.string.cancel),
|
|
||||||
cancelAction = viewModel::onNotificationsAlertNegativeClicked,
|
|
||||||
onDismissAction = viewModel::onNotificationsAlertNegativeClicked,
|
|
||||||
buttonsInvokeDismiss = false
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDenied = {
|
|
||||||
MaterialDialog(
|
|
||||||
title = UiText.Resource(UiR.string.warning),
|
|
||||||
text = UiText.Simple("The application needs permission to send notifications to update messages and other information."),
|
|
||||||
confirmText = UiText.Simple("Grant"),
|
|
||||||
confirmAction = {
|
|
||||||
viewModel.onRequestNotificationsPermissionClicked(false)
|
|
||||||
},
|
|
||||||
cancelText = UiText.Resource(UiR.string.cancel),
|
|
||||||
onDismissAction = {},
|
|
||||||
buttonsInvokeDismiss = false
|
|
||||||
)
|
|
||||||
},
|
|
||||||
permission = permission
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
package com.meloda.app.fast.presentation
|
package com.meloda.app.fast.presentation
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@@ -14,12 +22,11 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.meloda.app.fast.MainViewModel
|
import com.meloda.app.fast.MainViewModel
|
||||||
import com.meloda.app.fast.MainViewModelImpl
|
import com.meloda.app.fast.MainViewModelImpl
|
||||||
import com.meloda.app.fast.auth.AuthGraph
|
|
||||||
import com.meloda.app.fast.auth.authNavGraph
|
import com.meloda.app.fast.auth.authNavGraph
|
||||||
import com.meloda.app.fast.auth.navigateToAuth
|
import com.meloda.app.fast.auth.navigateToAuth
|
||||||
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsScreen
|
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsScreen
|
||||||
import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials
|
import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials
|
||||||
import com.meloda.app.fast.common.UserConfig
|
import com.meloda.app.fast.designsystem.R
|
||||||
import com.meloda.app.fast.languagepicker.navigation.languagePickerScreen
|
import com.meloda.app.fast.languagepicker.navigation.languagePickerScreen
|
||||||
import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker
|
import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker
|
||||||
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryScreen
|
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryScreen
|
||||||
@@ -31,10 +38,15 @@ import com.meloda.app.fast.settings.navigation.settingsScreen
|
|||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RootScreen(navController: NavHostController = rememberNavController()) {
|
fun RootScreen(
|
||||||
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
navController: NavHostController = rememberNavController(),
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||||
val isNeedToOpenAuth by viewModel.isNeedToOpenAuth.collectAsStateWithLifecycle()
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
|
||||||
|
val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle()
|
||||||
|
val isNeedToShowDeniedDialog by viewModel.isNeedToShowNotificationsDeniedDialog.collectAsStateWithLifecycle()
|
||||||
|
val isNeedToShowRationaleDialog by viewModel.isNeedToShowNotificationsRationaleDialog.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(isNeedToOpenAuth) {
|
LaunchedEffect(isNeedToOpenAuth) {
|
||||||
if (isNeedToOpenAuth) {
|
if (isNeedToOpenAuth) {
|
||||||
@@ -43,14 +55,63 @@ fun RootScreen(navController: NavHostController = rememberNavController()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenState.accountsLoaded) {
|
if (isNeedToShowDeniedDialog) {
|
||||||
val isNeedToShowConversations = remember {
|
AlertDialog(
|
||||||
screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn()
|
onDismissRequest = viewModel::onNotificationsDeniedDialogDismissed,
|
||||||
}
|
title = { Text(text = stringResource(id = R.string.warning)) },
|
||||||
|
text = { Text(text = stringResource(id = R.string.background_long_poll_denied_text)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = viewModel::onNotificationsDeniedDialogConfirmClicked) {
|
||||||
|
Text(text = stringResource(id = R.string.action_request))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = viewModel::onNotificationsDeniedDialogCancelClicked) {
|
||||||
|
Text(text = stringResource(id = R.string.action_disable))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = false,
|
||||||
|
dismissOnClickOutside = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNeedToShowRationaleDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = viewModel::onNotificationsRationaleDialogDismissed,
|
||||||
|
title = { Text(text = stringResource(id = R.string.warning)) },
|
||||||
|
text = { Text(text = stringResource(id = R.string.background_long_poll_rationale_text)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", context.packageName, null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.title_settings))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = viewModel::onNotificationsRationaleDialogCancelClicked) {
|
||||||
|
Text(text = stringResource(id = R.string.action_disable))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = false,
|
||||||
|
dismissOnClickOutside = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDestination != null) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = if (isNeedToShowConversations) Main else AuthGraph,
|
startDestination = requireNotNull(startDestination),
|
||||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||||
) {
|
) {
|
||||||
@@ -81,11 +142,6 @@ fun RootScreen(navController: NavHostController = rememberNavController()) {
|
|||||||
languagePickerScreen(onBack = navController::navigateUp)
|
languagePickerScreen(onBack = navController::navigateUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationsPermissionChecker(
|
|
||||||
screenState = screenState,
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavController.navigateToMain() {
|
fun NavController.navigateToMain() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.util.Log
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import com.conena.nanokt.android.app.stopForegroundCompat
|
import com.conena.nanokt.android.app.stopForegroundCompat
|
||||||
|
import com.meloda.app.fast.common.AppConstants
|
||||||
import com.meloda.app.fast.common.UserConfig
|
import com.meloda.app.fast.common.UserConfig
|
||||||
import com.meloda.app.fast.common.VkConstants
|
import com.meloda.app.fast.common.VkConstants
|
||||||
import com.meloda.app.fast.common.extensions.listenValue
|
import com.meloda.app.fast.common.extensions.listenValue
|
||||||
@@ -20,6 +21,9 @@ import com.meloda.app.fast.data.LongPollUseCase
|
|||||||
import com.meloda.app.fast.data.processState
|
import com.meloda.app.fast.data.processState
|
||||||
import com.meloda.app.fast.datastore.SettingsController
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
import com.meloda.app.fast.datastore.SettingsKeys
|
import com.meloda.app.fast.datastore.SettingsKeys
|
||||||
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
|
import com.meloda.app.fast.datastore.model.LongPollState
|
||||||
|
import com.meloda.app.fast.designsystem.R
|
||||||
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
||||||
import com.meloda.app.fast.model.api.data.VkLongPollData
|
import com.meloda.app.fast.model.api.data.VkLongPollData
|
||||||
import com.meloda.app.fast.util.NotificationsUtils
|
import com.meloda.app.fast.util.NotificationsUtils
|
||||||
@@ -36,6 +40,8 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
|
|
||||||
class LongPollingService : Service() {
|
class LongPollingService : Service() {
|
||||||
|
|
||||||
|
private val userSettings: UserSettings by inject()
|
||||||
|
|
||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
|
|
||||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
@@ -44,6 +50,9 @@ class LongPollingService : Service() {
|
|||||||
if (throwable !is NoAccessTokenException) {
|
if (throwable !is NoAccessTokenException) {
|
||||||
throwable.printStackTrace()
|
throwable.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userSettings.updateLongPollCurrentState(LongPollState.Exception)
|
||||||
|
userSettings.setLongPollStateToApply(LongPollState.Exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val coroutineContext: CoroutineContext
|
private val coroutineContext: CoroutineContext
|
||||||
@@ -70,14 +79,14 @@ class LongPollingService : Service() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (startId > 1) return START_STICKY
|
if (startId > 1) return START_STICKY
|
||||||
|
|
||||||
val asForeground = preferences.getBoolean(
|
val inBackground = preferences.getBoolean(
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
STATE_TAG,
|
STATE_TAG,
|
||||||
"onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId;\ninstance: $this"
|
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (currentJob != null) {
|
if (currentJob != null) {
|
||||||
@@ -105,14 +114,19 @@ class LongPollingService : Service() {
|
|||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
if (asForeground) {
|
userSettings.updateLongPollCurrentState(
|
||||||
|
if (inBackground) LongPollState.Background
|
||||||
|
else LongPollState.InApp
|
||||||
|
)
|
||||||
|
|
||||||
|
if (inBackground) {
|
||||||
val notification =
|
val notification =
|
||||||
NotificationsUtils.createNotification(
|
NotificationsUtils.createNotification(
|
||||||
context = this,
|
context = this,
|
||||||
title = "LongPoll",
|
title = getString(R.string.long_polling_service_notification_title),
|
||||||
contentText = "нажмите, чтобы убрать уведомление",
|
contentText = getString(R.string.long_polling_service_notification_content),
|
||||||
notRemovable = false,
|
notRemovable = false,
|
||||||
channelId = "long_polling",
|
channelId = AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
|
||||||
priority = NotificationsUtils.NotificationPriority.Low,
|
priority = NotificationsUtils.NotificationPriority.Low,
|
||||||
category = NotificationCompat.CATEGORY_SERVICE,
|
category = NotificationCompat.CATEGORY_SERVICE,
|
||||||
customNotificationId = NOTIFICATION_ID,
|
customNotificationId = NOTIFICATION_ID,
|
||||||
@@ -240,6 +254,7 @@ class LongPollingService : Service() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.d(STATE_TAG, "onDestroy")
|
Log.d(STATE_TAG, "onDestroy")
|
||||||
|
userSettings.updateLongPollCurrentState(LongPollState.Stopped)
|
||||||
try {
|
try {
|
||||||
SettingsController.edit {
|
SettingsController.edit {
|
||||||
putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true)
|
putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import com.meloda.app.fast.common.AppConstants
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
object NotificationsUtils {
|
object NotificationsUtils {
|
||||||
@@ -20,7 +21,7 @@ object NotificationsUtils {
|
|||||||
timeStampWhen: Long? = null,
|
timeStampWhen: Long? = null,
|
||||||
notify: Boolean = false,
|
notify: Boolean = false,
|
||||||
notRemovable: Boolean = false,
|
notRemovable: Boolean = false,
|
||||||
channelId: String = "simple_notifications",
|
channelId: String = AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
|
||||||
priority: NotificationPriority = NotificationPriority.Default,
|
priority: NotificationPriority = NotificationPriority.Default,
|
||||||
contentIntent: PendingIntent? = null,
|
contentIntent: PendingIntent? = null,
|
||||||
category: String? = null,
|
category: String? = null,
|
||||||
|
|||||||
@@ -7,4 +7,7 @@ object AppConstants {
|
|||||||
const val API_VERSION = "5.173"
|
const val API_VERSION = "5.173"
|
||||||
const val URL_OAUTH = "https://oauth.vk.com"
|
const val URL_OAUTH = "https://oauth.vk.com"
|
||||||
const val URL_API = "https://api.vk.com/method"
|
const val URL_API = "https://api.vk.com/method"
|
||||||
|
|
||||||
|
const val NOTIFICATION_CHANNEL_UNCATEGORIZED = "uncategorized"
|
||||||
|
const val NOTIFICATION_CHANNEL_LONG_POLLING = "long_polling"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -141,6 +142,7 @@ fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ChecksSdkIntAtLeast(parameter = 0, lambda = 1)
|
||||||
fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean {
|
fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean {
|
||||||
return if (Build.VERSION.SDK_INT >= sdkInt) {
|
return if (Build.VERSION.SDK_INT >= sdkInt) {
|
||||||
action?.invoke()
|
action?.invoke()
|
||||||
|
|||||||
@@ -6,5 +6,7 @@ interface AccountsRepository {
|
|||||||
|
|
||||||
suspend fun getAccounts(): List<AccountEntity>
|
suspend fun getAccounts(): List<AccountEntity>
|
||||||
|
|
||||||
|
suspend fun getAccountById(userId: Int): AccountEntity?
|
||||||
|
|
||||||
suspend fun storeAccounts(accounts: List<AccountEntity>)
|
suspend fun storeAccounts(accounts: List<AccountEntity>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ class AccountsRepositoryImpl(
|
|||||||
|
|
||||||
override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll()
|
override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll()
|
||||||
|
|
||||||
|
override suspend fun getAccountById(userId: Int): AccountEntity? =
|
||||||
|
accountDao.getById(userId)
|
||||||
|
|
||||||
override suspend fun storeAccounts(
|
override suspend fun storeAccounts(
|
||||||
accounts: List<AccountEntity>
|
accounts: List<AccountEntity>
|
||||||
) = accountDao.insertAll(accounts)
|
) = accountDao.insertAll(accounts)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.meloda.app.fast.data.db
|
||||||
|
|
||||||
|
import com.meloda.app.fast.common.UserConfig
|
||||||
|
import com.meloda.app.fast.model.database.AccountEntity
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class GetCurrentAccountUseCase(private val accountsRepository: AccountsRepository) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(): AccountEntity? = withContext(Dispatchers.IO) {
|
||||||
|
accountsRepository.getAccountById(UserConfig.currentUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import com.meloda.app.fast.data.api.users.UsersUseCaseImpl
|
|||||||
import com.meloda.app.fast.data.api.videos.VideosRepository
|
import com.meloda.app.fast.data.api.videos.VideosRepository
|
||||||
import com.meloda.app.fast.data.db.AccountsRepository
|
import com.meloda.app.fast.data.db.AccountsRepository
|
||||||
import com.meloda.app.fast.data.db.AccountsRepositoryImpl
|
import com.meloda.app.fast.data.db.AccountsRepositoryImpl
|
||||||
|
import com.meloda.app.fast.data.db.GetCurrentAccountUseCase
|
||||||
import com.meloda.app.fast.database.di.databaseModule
|
import com.meloda.app.fast.database.di.databaseModule
|
||||||
import com.meloda.app.fast.datastore.di.dataStoreModule
|
import com.meloda.app.fast.datastore.di.dataStoreModule
|
||||||
import com.meloda.app.fast.network.di.networkModule
|
import com.meloda.app.fast.network.di.networkModule
|
||||||
@@ -67,6 +68,7 @@ val dataModule = module {
|
|||||||
singleOf(::VideosRepository)
|
singleOf(::VideosRepository)
|
||||||
|
|
||||||
singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class
|
singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class
|
||||||
|
singleOf(::GetCurrentAccountUseCase)
|
||||||
|
|
||||||
singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class
|
singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ abstract class AccountDao : EntityDao<AccountEntity> {
|
|||||||
@Query("SELECT * FROM accounts")
|
@Query("SELECT * FROM accounts")
|
||||||
abstract suspend fun getAll(): List<AccountEntity>
|
abstract suspend fun getAll(): List<AccountEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM accounts WHERE userId = :userId")
|
||||||
|
abstract suspend fun getById(userId: Int): AccountEntity?
|
||||||
|
|
||||||
@Query("DELETE FROM accounts WHERE userId = :userId")
|
@Query("DELETE FROM accounts WHERE userId = :userId")
|
||||||
abstract suspend fun deleteById(userId: Int)
|
abstract suspend fun deleteById(userId: Int)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,4 +50,15 @@ object SettingsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isLongPollInBackgroundEnabled: Boolean =
|
||||||
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
|
get() = getBoolean(
|
||||||
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||||
|
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||||
|
)
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.meloda.app.fast.datastore
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.util.Log
|
||||||
|
import com.meloda.app.fast.datastore.model.LongPollState
|
||||||
import com.meloda.app.fast.datastore.model.ThemeConfig
|
import com.meloda.app.fast.datastore.model.ThemeConfig
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -9,7 +11,8 @@ import kotlinx.coroutines.flow.update
|
|||||||
|
|
||||||
interface UserSettings {
|
interface UserSettings {
|
||||||
val theme: StateFlow<ThemeConfig>
|
val theme: StateFlow<ThemeConfig>
|
||||||
val longPollBackground: StateFlow<Boolean>
|
val longPollStateToApply: StateFlow<LongPollState>
|
||||||
|
val longPollCurrentState: StateFlow<LongPollState>
|
||||||
val online: StateFlow<Boolean>
|
val online: StateFlow<Boolean>
|
||||||
val debugSettingsEnabled: StateFlow<Boolean>
|
val debugSettingsEnabled: StateFlow<Boolean>
|
||||||
val useContactNames: StateFlow<Boolean>
|
val useContactNames: StateFlow<Boolean>
|
||||||
@@ -21,7 +24,8 @@ interface UserSettings {
|
|||||||
fun useDynamicColorsChanged(use: Boolean)
|
fun useDynamicColorsChanged(use: Boolean)
|
||||||
fun useBlurChanged(use: Boolean)
|
fun useBlurChanged(use: Boolean)
|
||||||
fun useMultiline(use: Boolean)
|
fun useMultiline(use: Boolean)
|
||||||
fun setLongPollBackground(background: Boolean)
|
fun setLongPollStateToApply(newState: LongPollState)
|
||||||
|
fun updateLongPollCurrentState(currentState: LongPollState)
|
||||||
fun setOnline(use: Boolean)
|
fun setOnline(use: Boolean)
|
||||||
fun enableDebugSettings(enable: Boolean)
|
fun enableDebugSettings(enable: Boolean)
|
||||||
fun onUseContactNamesChanged(use: Boolean)
|
fun onUseContactNamesChanged(use: Boolean)
|
||||||
@@ -45,12 +49,9 @@ class UserSettingsImpl(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override val longPollBackground = MutableStateFlow(
|
override val longPollStateToApply = MutableStateFlow<LongPollState>(LongPollState.Stopped)
|
||||||
SettingsController.getBoolean(
|
override val longPollCurrentState = MutableStateFlow<LongPollState>(LongPollState.Stopped)
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
|
||||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
|
||||||
)
|
|
||||||
)
|
|
||||||
override val online = MutableStateFlow(
|
override val online = MutableStateFlow(
|
||||||
SettingsController.getBoolean(
|
SettingsController.getBoolean(
|
||||||
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS,
|
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS,
|
||||||
@@ -107,8 +108,14 @@ class UserSettingsImpl(
|
|||||||
theme.value = theme.value.copy(multiline = use)
|
theme.value = theme.value.copy(multiline = use)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setLongPollBackground(background: Boolean) {
|
override fun setLongPollStateToApply(newState: LongPollState) {
|
||||||
longPollBackground.value = background
|
longPollStateToApply.update { newState }
|
||||||
|
Log.d("UserSettings", "setLongPollState: $newState")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateLongPollCurrentState(currentState: LongPollState) {
|
||||||
|
longPollCurrentState.update { currentState }
|
||||||
|
Log.d("UserSettings", "updateLongPollCurrentState: $currentState")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOnline(use: Boolean) {
|
override fun setOnline(use: Boolean) {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.meloda.app.fast.datastore.model
|
||||||
|
|
||||||
|
sealed class LongPollState {
|
||||||
|
data object Stopped : LongPollState()
|
||||||
|
|
||||||
|
// TODO: 15/07/2024, Danil Nikolaev: support for android 15
|
||||||
|
// data object Terminated : LongPollState()
|
||||||
|
data object InApp : LongPollState()
|
||||||
|
data object Background : LongPollState()
|
||||||
|
data object Exception : LongPollState()
|
||||||
|
|
||||||
|
|
||||||
|
fun isLaunched(): Boolean = this in listOf(InApp, Background)
|
||||||
|
}
|
||||||
@@ -4,15 +4,11 @@ import android.content.res.Configuration
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import com.google.accompanist.permissions.PermissionState
|
|
||||||
import com.google.accompanist.permissions.PermissionStatus
|
|
||||||
import com.meloda.app.fast.common.UiText
|
import com.meloda.app.fast.common.UiText
|
||||||
import com.meloda.app.fast.common.util.AndroidUtils
|
import com.meloda.app.fast.common.util.AndroidUtils
|
||||||
import com.meloda.app.fast.datastore.SettingsController
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
@@ -81,32 +77,3 @@ fun Modifier.handleEnterKey(
|
|||||||
action.invoke()
|
action.invoke()
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
|
||||||
fun CheckPermission(
|
|
||||||
showRationale: @Composable () -> Unit,
|
|
||||||
onDenied: @Composable () -> Unit,
|
|
||||||
permission: PermissionState,
|
|
||||||
) {
|
|
||||||
when (val status = permission.status) {
|
|
||||||
is PermissionStatus.Denied -> {
|
|
||||||
if (status.shouldShowRationale) {
|
|
||||||
showRationale()
|
|
||||||
} else {
|
|
||||||
onDenied()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is PermissionStatus.Granted -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
|
||||||
fun RequestPermission(
|
|
||||||
permission: PermissionState
|
|
||||||
) {
|
|
||||||
LaunchedEffect(Unit) { permission.launchPermissionRequest() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class ImmutableList<T>(val values: List<T>) : Iterable<T> {
|
|||||||
return values.map(transform).toImmutableList()
|
return values.map(transform).toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <R> mapNotNull(transform: (T) -> R?): ImmutableList<R> {
|
||||||
|
return values.mapNotNull(transform).toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
inline fun <R> mapIndexed(transform: (index: Int, T) -> R): ImmutableList<R> {
|
inline fun <R> mapIndexed(transform: (index: Int, T) -> R): ImmutableList<R> {
|
||||||
return values.mapIndexed(transform).toImmutableList()
|
return values.mapIndexed(transform).toImmutableList()
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-25
@@ -1,11 +1,5 @@
|
|||||||
package com.meloda.app.fast.designsystem
|
package com.meloda.app.fast.designsystem
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.scaleIn
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
@@ -32,15 +26,19 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.meloda.app.fast.common.UiText
|
import com.meloda.app.fast.common.UiText
|
||||||
|
import com.meloda.app.fast.common.parseString
|
||||||
import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList
|
import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList
|
||||||
|
|
||||||
// TODO: 08.04.2023, Danil Nikolaev: review
|
// TODO: 08.04.2023, Danil Nikolaev: refactor this
|
||||||
|
@Deprecated("need refactoring")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MaterialDialog(
|
fun MaterialDialog(
|
||||||
@@ -58,17 +56,22 @@ fun MaterialDialog(
|
|||||||
items: ImmutableList<UiText> = ImmutableList.empty(),
|
items: ImmutableList<UiText> = ImmutableList.empty(),
|
||||||
onItemClick: ((index: Int) -> Unit)? = null,
|
onItemClick: ((index: Int) -> Unit)? = null,
|
||||||
buttonsInvokeDismiss: Boolean = true,
|
buttonsInvokeDismiss: Boolean = true,
|
||||||
|
properties: DialogProperties = DialogProperties(),
|
||||||
customContent: (@Composable ColumnScope.() -> Unit)? = null,
|
customContent: (@Composable ColumnScope.() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var isVisible by remember {
|
var isVisible by rememberSaveable {
|
||||||
mutableStateOf(true)
|
mutableStateOf(true)
|
||||||
}
|
}
|
||||||
val onDismissRequest = {
|
val onDismissRequest = remember {
|
||||||
onDismissAction.invoke()
|
{
|
||||||
isVisible = false
|
onDismissAction.invoke()
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val stringTitles = items.map { it.getString().orEmpty() }
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val stringTitles = items.mapNotNull { it.parseString(context.resources) }
|
||||||
|
|
||||||
var alertItems by remember {
|
var alertItems by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
@@ -81,11 +84,10 @@ fun MaterialDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AlertAnimation(visible = isVisible) {
|
||||||
if (isVisible) {
|
|
||||||
// AlertAnimation(visible = isVisible) {
|
|
||||||
BasicAlertDialog(
|
BasicAlertDialog(
|
||||||
onDismissRequest = onDismissRequest
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = properties
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } }
|
val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } }
|
||||||
@@ -251,15 +253,16 @@ fun MaterialDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AlertAnimation(
|
fun AlertAnimation(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
content: @Composable AnimatedVisibilityScope.() -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
if (visible) content()
|
||||||
visible = visible,
|
// AnimatedVisibility(
|
||||||
enter = fadeIn(animationSpec = tween(400)) +
|
// visible = visible,
|
||||||
scaleIn(animationSpec = tween(400)),
|
// enter = fadeIn(animationSpec = tween(400)) +
|
||||||
exit = fadeOut(animationSpec = tween(150)),
|
// scaleIn(animationSpec = tween(400)),
|
||||||
content = content
|
// exit = fadeOut(animationSpec = tween(150)),
|
||||||
)
|
// content = content
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -188,4 +188,14 @@
|
|||||||
<string name="settings_activity_send_online_title">Быть «в сети»</string>
|
<string name="settings_activity_send_online_title">Быть «в сети»</string>
|
||||||
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string>
|
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string>
|
||||||
<string name="settings_debug_title">Отладка</string>
|
<string name="settings_debug_title">Отладка</string>
|
||||||
|
<string name="action_disable">Отключить</string>
|
||||||
|
<string name="background_long_poll_rationale_text">Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям</string>
|
||||||
|
<string name="background_long_poll_denied_text">Приложению нужен доступ к уведомлениям для обновления сообщений в фоне</string>
|
||||||
|
<string name="action_request">Запросить</string>
|
||||||
|
<string name="long_polling_service_notification_title">Обновление сообщений в фоне</string>
|
||||||
|
<string name="long_polling_service_notification_content">Нажмите, чтобы скрыть</string>
|
||||||
|
<string name="notification_channel_no_category_name">Без категории</string>
|
||||||
|
<string name="notification_channel_no_category_description">Уведомления без категории</string>
|
||||||
|
<string name="notification_channel_long_polling_service_name">Сервис обновления сообщений</string>
|
||||||
|
<string name="notification_channel_long_polling_service_description">Уведомления сервиса обновлений сообщений</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -246,5 +246,17 @@
|
|||||||
<string name="settings_activity_send_online_title">Send online status</string>
|
<string name="settings_activity_send_online_title">Send online status</string>
|
||||||
<string name="settings_activity_send_online_summary">Online status will be sent every five minutes</string>
|
<string name="settings_activity_send_online_summary">Online status will be sent every five minutes</string>
|
||||||
<string name="settings_debug_title">Debug</string>
|
<string name="settings_debug_title">Debug</string>
|
||||||
|
<string name="background_long_poll_rationale_text">The app won\'t be able to update messages in the background without access to notifications</string>
|
||||||
|
<string name="action_disable">Disable</string>
|
||||||
|
<string name="background_long_poll_denied_text">The app needs access to notifications to update messages in the background</string>
|
||||||
|
<string name="action_request">Request</string>
|
||||||
|
|
||||||
</resources>
|
<string name="long_polling_service_notification_title">Updating messages in background</string>
|
||||||
|
<string name="long_polling_service_notification_content">Tap to hide</string>
|
||||||
|
|
||||||
|
<string name="notification_channel_no_category_name">Uncategorized</string>
|
||||||
|
<string name="notification_channel_no_category_description">Uncategorized notifications</string>
|
||||||
|
|
||||||
|
<string name="notification_channel_long_polling_service_name">Message update service</string>
|
||||||
|
<string name="notification_channel_long_polling_service_description">Message update service notifications</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ import com.meloda.app.fast.data.State
|
|||||||
import com.meloda.app.fast.data.api.users.UsersUseCase
|
import com.meloda.app.fast.data.api.users.UsersUseCase
|
||||||
import com.meloda.app.fast.data.db.AccountsRepository
|
import com.meloda.app.fast.data.db.AccountsRepository
|
||||||
import com.meloda.app.fast.data.processState
|
import com.meloda.app.fast.data.processState
|
||||||
|
import com.meloda.app.fast.datastore.SettingsController
|
||||||
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
|
import com.meloda.app.fast.datastore.model.LongPollState
|
||||||
import com.meloda.app.fast.model.database.AccountEntity
|
import com.meloda.app.fast.model.database.AccountEntity
|
||||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
import com.meloda.app.fast.network.OAuthErrorDomain
|
||||||
import com.meloda.fast.auth.login.model.CaptchaArguments
|
import com.meloda.fast.auth.login.model.CaptchaArguments
|
||||||
import com.meloda.fast.auth.login.model.LoginError
|
import com.meloda.fast.auth.login.model.LoginError
|
||||||
import com.meloda.fast.auth.login.model.LoginScreenState
|
import com.meloda.fast.auth.login.model.LoginScreenState
|
||||||
import com.meloda.fast.auth.login.model.LoginValidationArguments
|
|
||||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
|
import com.meloda.fast.auth.login.model.LoginValidationArguments
|
||||||
import com.meloda.fast.auth.login.model.LoginValidationResult
|
import com.meloda.fast.auth.login.model.LoginValidationResult
|
||||||
import com.meloda.fast.auth.login.validation.LoginValidator
|
import com.meloda.fast.auth.login.validation.LoginValidator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -67,7 +70,8 @@ class LoginViewModelImpl(
|
|||||||
private val oAuthUseCase: OAuthUseCase,
|
private val oAuthUseCase: OAuthUseCase,
|
||||||
private val usersUseCase: UsersUseCase,
|
private val usersUseCase: UsersUseCase,
|
||||||
private val accountsRepository: AccountsRepository,
|
private val accountsRepository: AccountsRepository,
|
||||||
private val loginValidator: LoginValidator
|
private val loginValidator: LoginValidator,
|
||||||
|
private val userSettings: UserSettings
|
||||||
) : ViewModel(), LoginViewModel {
|
) : ViewModel(), LoginViewModel {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||||
@@ -155,6 +159,8 @@ class LoginViewModelImpl(
|
|||||||
UserConfig.trustedHash = account.trustedHash
|
UserConfig.trustedHash = account.trustedHash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startLongPoll()
|
||||||
|
|
||||||
usersUseCase.get(
|
usersUseCase.get(
|
||||||
userIds = null,
|
userIds = null,
|
||||||
fields = VkConstants.USER_FIELDS,
|
fields = VkConstants.USER_FIELDS,
|
||||||
@@ -238,6 +244,8 @@ class LoginViewModelImpl(
|
|||||||
UserConfig.trustedHash = account.trustedHash
|
UserConfig.trustedHash = account.trustedHash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startLongPoll()
|
||||||
|
|
||||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||||
|
|
||||||
captchaArguments.update { null }
|
captchaArguments.update { null }
|
||||||
@@ -338,4 +346,14 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startLongPoll() {
|
||||||
|
userSettings.setLongPollStateToApply(
|
||||||
|
if (SettingsController.isLongPollInBackgroundEnabled) {
|
||||||
|
LongPollState.Background
|
||||||
|
} else {
|
||||||
|
LongPollState.InApp
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-13
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.defaultMinSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
@@ -156,17 +155,13 @@ fun ConversationsScreen(
|
|||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val currentTheme = LocalTheme.current
|
val currentTheme = LocalTheme.current
|
||||||
|
|
||||||
val maxLines by remember {
|
val maxLines by remember(currentTheme) {
|
||||||
derivedStateOf {
|
mutableIntStateOf(if (currentTheme.multiline) 2 else 1)
|
||||||
if (currentTheme.multiline) 2 else 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
val isListScrollingUp = listState.isScrollingUp()
|
val paginationConditionMet by remember(canPaginate, listState) {
|
||||||
|
|
||||||
val paginationConditionMet by remember {
|
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
canPaginate &&
|
canPaginate &&
|
||||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||||
@@ -292,7 +287,7 @@ fun ConversationsScreen(
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isListScrollingUp,
|
visible = listState.isScrollingUp(),
|
||||||
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||||
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||||
) {
|
) {
|
||||||
@@ -351,10 +346,7 @@ fun ConversationsScreen(
|
|||||||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
ConversationsListComposable(
|
ConversationsListComposable(
|
||||||
onConversationsClick = { id ->
|
onConversationsClick = onConversationItemClicked,
|
||||||
onConversationItemClicked(id)
|
|
||||||
|
|
||||||
},
|
|
||||||
onConversationsLongClick = onConversationItemLongClicked,
|
onConversationsLongClick = onConversationItemLongClicked,
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
state = listState,
|
state = listState,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.meloda.app.fast.datastore.SettingsController
|
|||||||
import com.meloda.app.fast.datastore.SettingsKeys
|
import com.meloda.app.fast.datastore.SettingsKeys
|
||||||
import com.meloda.app.fast.datastore.UserSettings
|
import com.meloda.app.fast.datastore.UserSettings
|
||||||
import com.meloda.app.fast.datastore.isDebugSettingsShown
|
import com.meloda.app.fast.datastore.isDebugSettingsShown
|
||||||
|
import com.meloda.app.fast.datastore.model.LongPollState
|
||||||
import com.meloda.app.fast.model.database.AccountEntity
|
import com.meloda.app.fast.model.database.AccountEntity
|
||||||
import com.meloda.app.fast.settings.model.SettingsItem
|
import com.meloda.app.fast.settings.model.SettingsItem
|
||||||
import com.meloda.app.fast.settings.model.SettingsScreenState
|
import com.meloda.app.fast.settings.model.SettingsScreenState
|
||||||
@@ -161,7 +162,14 @@ class SettingsViewModelImpl(
|
|||||||
when (key) {
|
when (key) {
|
||||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
|
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
|
||||||
val isEnabled = (newValue as? Boolean) == true
|
val isEnabled = (newValue as? Boolean) == true
|
||||||
userSettings.setLongPollBackground(isEnabled)
|
userSettings.setLongPollStateToApply(
|
||||||
|
userSettings.longPollStateToApply.value.let { state ->
|
||||||
|
if (state.isLaunched()) {
|
||||||
|
if (isEnabled) LongPollState.Background
|
||||||
|
else LongPollState.InApp
|
||||||
|
} else state
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
// TODO: 26/11/2023, Danil Nikolaev: implement
|
// TODO: 26/11/2023, Danil Nikolaev: implement
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.5.0"
|
agp = "8.5.1"
|
||||||
converterMoshi = "2.11.0"
|
converterMoshi = "2.11.0"
|
||||||
eithernet = "1.9.0"
|
eithernet = "1.9.0"
|
||||||
haze = "0.7.3"
|
haze = "0.7.3"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
ksp = "2.0.0-1.0.22"
|
ksp = "2.0.0-1.0.22"
|
||||||
vkompose = "0.5.4-k2"
|
|
||||||
|
|
||||||
|
|
||||||
compose-bom = "2024.06.00"
|
compose-bom = "2024.06.00"
|
||||||
koin = "3.5.6"
|
koin = "3.5.6"
|
||||||
|
|
||||||
accompanist = "0.34.0"
|
accompanist = "0.35.1-alpha"
|
||||||
coil = "2.6.0"
|
coil = "2.6.0"
|
||||||
coroutines = "1.9.0-RC"
|
coroutines = "1.9.0-RC"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user