a lot of improvements for long polling service and notifications

This commit is contained in:
2024-07-15 05:01:54 +03:00
parent 9481541806
commit 654f47bb94
28 changed files with 473 additions and 388 deletions
-1
View File
@@ -182,5 +182,4 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlin.serialization)
implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04")
}
@@ -1,18 +1,24 @@
package com.meloda.app.fast
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModel
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.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.updateValue
import com.meloda.app.fast.data.db.AccountsRepository
import com.meloda.app.fast.data.db.GetCurrentAccountUseCase
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.model.LongPollState
import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.model.LongPollState
import com.meloda.app.fast.model.MainScreenState
import com.meloda.app.fast.navigation.Main
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -21,30 +27,33 @@ import kotlinx.coroutines.launch
interface MainViewModel {
val screenState: StateFlow<MainScreenState>
val isNeedToOpenAuth: StateFlow<Boolean>
val startDestination: StateFlow<Any?>
val isNeedToReplaceWithAuth: StateFlow<Boolean>
val longPollState: StateFlow<LongPollState>
val startOnlineService: StateFlow<Boolean>
fun useDynamicColorsChanged(use: Boolean)
fun useDarkThemeChanged(use: Boolean)
fun onRequestNotificationsPermissionClicked(fromRationale: Boolean)
fun onNotificationsAlertNegativeClicked()
fun onNotificationsRequested()
fun onAppPermissionsOpened()
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean>
fun onError(error: BaseError)
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(
private val accountsRepository: AccountsRepository,
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val userSettings: UserSettings
) : MainViewModel, ViewModel() {
@@ -52,83 +61,111 @@ class MainViewModelImpl(
loadAccounts()
}
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
override val isNeedToOpenAuth = MutableStateFlow(false)
override val startDestination = MutableStateFlow<Any?>(null)
override val isNeedToReplaceWithAuth = MutableStateFlow(false)
override val longPollState = MutableStateFlow(
if (SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
) {
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 val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
override val isNeedToRequestNotifications = MutableStateFlow(false)
override fun onError(error: BaseError) {
when (error) {
BaseError.SessionExpired -> {
isNeedToOpenAuth.update { true }
isNeedToReplaceWithAuth.update { true }
}
}
}
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() {
viewModelScope.launch(Dispatchers.IO) {
val accounts = accountsRepository.getAccounts()
val currentAccount = getCurrentAccountUseCase()
Log.d("MainViewModel", "initUserConfig: accounts: $accounts")
Log.d("MainViewModel", "currentAccount: $currentAccount")
listenLongPollState()
if (accounts.isNotEmpty()) {
val currentAccount = accounts.find { it.userId == UserConfig.currentUserId }
if (currentAccount != null) {
UserConfig.apply {
this.userId = currentAccount.userId
@@ -136,15 +173,25 @@ class MainViewModelImpl(
this.fastToken = currentAccount.fastToken
this.trustedHash = currentAccount.trustedHash
}
userSettings.setLongPollStateToApply(
if (SettingsController.isLongPollInBackgroundEnabled) {
LongPollState.Background
} else {
LongPollState.InApp
}
)
}
startDestination.setValue {
if (currentAccount == null) AuthGraph
else Main
}
}
}
screenState.setValue { old ->
old.copy(
accounts = accounts,
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.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.LaunchedEffect
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.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.compose.LifecycleResumeEffect
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.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.meloda.app.fast.MainViewModel
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.extensions.ifEmpty
import com.meloda.app.fast.MainViewModelImpl
import com.meloda.app.fast.common.AppConstants
import com.meloda.app.fast.common.extensions.isSdkAtLeast
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.model.LongPollState
import com.meloda.app.fast.datastore.model.ThemeConfig
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.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.longpolling.LongPollingService
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.KoinContext
import org.koin.compose.koinInject
import com.meloda.app.fast.designsystem.R as UiR
class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -76,30 +71,64 @@ class MainActivity : AppCompatActivity() {
setContent {
KoinContext {
val context = LocalContext.current
val userSettings: UserSettings = koinInject()
LifecycleResumeEffect(true) {
userSettings.onLanguageChanged(
AppCompatDelegate.getApplicationLocales()
.toLanguageTags()
.ifEmpty { null }
?: LocaleListCompat.getDefault()
.toLanguageTags()
.split(",")
.firstOrNull()
.orEmpty()
.take(5)
)
val longPollCurrentState by userSettings.longPollCurrentState.collectAsStateWithLifecycle()
val longPollStateToApply by userSettings.longPollStateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
LifecycleResumeEffect(true) {
viewModel.onAppResumed()
onPauseOrDispose {}
}
LaunchedEffect(true) {
userSettings.updateUsingDarkTheme()
val permissionState =
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)
}
val isLongPollInBackground by userSettings.longPollBackground.collectAsStateWithLifecycle()
toggleLongPollService(true, isLongPollInBackground)
toggleLongPollService(
enable = true,
inBackground = true
)
}
}
}
LaunchedEffect(isNeedToRequestPermission) {
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()
LifecycleResumeEffect(isOnline) {
@@ -110,11 +139,9 @@ class MainActivity : AppCompatActivity() {
}
}
val windowAdaptiveInfo = currentWindowAdaptiveInfo()
val isDeviceCompact by remember(windowAdaptiveInfo) {
val isDeviceCompact by remember(true) {
derivedStateOf {
windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
context.resources.displayMetrics.widthPixels.pxToDp() <= 360
}
}
@@ -138,7 +165,7 @@ class MainActivity : AppCompatActivity() {
selectedColorScheme = currentTheme.selectedColorScheme,
useAmoledBackground = currentTheme.usingAmoledBackground,
) {
RootScreen()
RootScreen(viewModel = viewModel)
}
}
}
@@ -147,40 +174,37 @@ class MainActivity : AppCompatActivity() {
private fun createNotificationChannels() {
isSdkAtLeast(Build.VERSION_CODES.O) {
val dialogsName = "Dialogs"
val dialogsDescriptionText = "Channel for dialogs notifications"
val dialogsImportance = NotificationManager.IMPORTANCE_HIGH
val dialogsChannel =
NotificationChannel("simple_notifications", dialogsName, dialogsImportance).apply {
description = dialogsDescriptionText
val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
val noCategoryDescriptionText = getString(UiR.string.notification_channel_no_category_description)
val noCategoryImportance = NotificationManager.IMPORTANCE_HIGH
val noCategoryChannel =
NotificationChannel(AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED, noCategoryName, noCategoryImportance).apply {
description = noCategoryDescriptionText
}
val longPollName = "Long Polling"
val longPollDescriptionText = "Channel for long polling service"
val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
val longPollDescriptionText = getString(UiR.string.notification_channel_long_polling_service_description)
val longPollImportance = NotificationManager.IMPORTANCE_NONE
val longPollChannel =
NotificationChannel("long_polling", longPollName, longPollImportance).apply {
NotificationChannel(AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING, longPollName, longPollImportance).apply {
description = longPollDescriptionText
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannels(listOf(dialogsChannel, longPollChannel))
notificationManager.createNotificationChannels(listOf(noCategoryChannel, longPollChannel))
}
}
private fun toggleLongPollService(
enable: Boolean,
asForeground: Boolean = SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
inBackground: Boolean = SettingsController.isLongPollInBackgroundEnabled
) {
if (enable) {
val longPollIntent = Intent(this, LongPollingService::class.java)
if (asForeground) {
if (inBackground) {
ContextCompat.startForegroundService(this, longPollIntent)
} else {
startService(longPollIntent)
@@ -201,104 +225,15 @@ class MainActivity : AppCompatActivity() {
private fun stopServices() {
toggleOnlineService(enable = false)
val asForeground = SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
val asForeground = SettingsController.isLongPollInBackgroundEnabled
if (!asForeground) {
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() {
super.onDestroy()
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
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
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.LaunchedEffect
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.navigation.NavController
import androidx.navigation.NavHostController
@@ -14,12 +22,11 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.meloda.app.fast.MainViewModel
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.navigateToAuth
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsScreen
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.navigateToLanguagePicker
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
@Composable
fun RootScreen(navController: NavHostController = rememberNavController()) {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val isNeedToOpenAuth by viewModel.isNeedToOpenAuth.collectAsStateWithLifecycle()
fun RootScreen(
navController: NavHostController = rememberNavController(),
viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
) {
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) {
if (isNeedToOpenAuth) {
@@ -43,14 +55,63 @@ fun RootScreen(navController: NavHostController = rememberNavController()) {
}
}
if (screenState.accountsLoaded) {
val isNeedToShowConversations = remember {
screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn()
if (isNeedToShowDeniedDialog) {
AlertDialog(
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(
navController = navController,
startDestination = if (isNeedToShowConversations) Main else AuthGraph,
startDestination = requireNotNull(startDestination),
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
@@ -81,11 +142,6 @@ fun RootScreen(navController: NavHostController = rememberNavController()) {
languagePickerScreen(onBack = navController::navigateUp)
}
}
NotificationsPermissionChecker(
screenState = screenState,
viewModel = viewModel
)
}
fun NavController.navigateToMain() {
@@ -12,6 +12,7 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
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.VkConstants
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.datastore.SettingsController
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.VkLongPollData
import com.meloda.app.fast.util.NotificationsUtils
@@ -36,6 +40,8 @@ import kotlin.coroutines.suspendCoroutine
class LongPollingService : Service() {
private val userSettings: UserSettings by inject()
private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
@@ -44,6 +50,9 @@ class LongPollingService : Service() {
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
userSettings.updateLongPollCurrentState(LongPollState.Exception)
userSettings.setLongPollStateToApply(LongPollState.Exception)
}
private val coroutineContext: CoroutineContext
@@ -70,14 +79,14 @@ class LongPollingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY
val asForeground = preferences.getBoolean(
val inBackground = preferences.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
Log.d(
STATE_TAG,
"onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId;\ninstance: $this"
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this"
)
if (currentJob != null) {
@@ -105,14 +114,19 @@ class LongPollingService : Service() {
PendingIntent.FLAG_IMMUTABLE
)
if (asForeground) {
userSettings.updateLongPollCurrentState(
if (inBackground) LongPollState.Background
else LongPollState.InApp
)
if (inBackground) {
val notification =
NotificationsUtils.createNotification(
context = this,
title = "LongPoll",
contentText = "нажмите, чтобы убрать уведомление",
title = getString(R.string.long_polling_service_notification_title),
contentText = getString(R.string.long_polling_service_notification_content),
notRemovable = false,
channelId = "long_polling",
channelId = AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
priority = NotificationsUtils.NotificationPriority.Low,
category = NotificationCompat.CATEGORY_SERVICE,
customNotificationId = NOTIFICATION_ID,
@@ -240,6 +254,7 @@ class LongPollingService : Service() {
override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy")
userSettings.updateLongPollCurrentState(LongPollState.Stopped)
try {
SettingsController.edit {
putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true)
@@ -5,6 +5,7 @@ import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.meloda.app.fast.common.AppConstants
import com.meloda.app.fast.designsystem.R as UiR
object NotificationsUtils {
@@ -20,7 +21,7 @@ object NotificationsUtils {
timeStampWhen: Long? = null,
notify: Boolean = false,
notRemovable: Boolean = false,
channelId: String = "simple_notifications",
channelId: String = AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
priority: NotificationPriority = NotificationPriority.Default,
contentIntent: PendingIntent? = null,
category: String? = null,
@@ -7,4 +7,7 @@ object AppConstants {
const val API_VERSION = "5.173"
const val URL_OAUTH = "https://oauth.vk.com"
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.Intent
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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 {
return if (Build.VERSION.SDK_INT >= sdkInt) {
action?.invoke()
@@ -6,5 +6,7 @@ interface AccountsRepository {
suspend fun getAccounts(): List<AccountEntity>
suspend fun getAccountById(userId: Int): AccountEntity?
suspend fun storeAccounts(accounts: List<AccountEntity>)
}
@@ -9,6 +9,9 @@ class AccountsRepositoryImpl(
override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll()
override suspend fun getAccountById(userId: Int): AccountEntity? =
accountDao.getById(userId)
override suspend fun storeAccounts(
accounts: List<AccountEntity>
) = 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.db.AccountsRepository
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.datastore.di.dataStoreModule
import com.meloda.app.fast.network.di.networkModule
@@ -67,6 +68,7 @@ val dataModule = module {
singleOf(::VideosRepository)
singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class
singleOf(::GetCurrentAccountUseCase)
singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class
}
@@ -10,6 +10,9 @@ abstract class AccountDao : EntityDao<AccountEntity> {
@Query("SELECT * FROM accounts")
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")
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.os.PowerManager
import android.util.Log
import com.meloda.app.fast.datastore.model.LongPollState
import com.meloda.app.fast.datastore.model.ThemeConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -9,7 +11,8 @@ import kotlinx.coroutines.flow.update
interface UserSettings {
val theme: StateFlow<ThemeConfig>
val longPollBackground: StateFlow<Boolean>
val longPollStateToApply: StateFlow<LongPollState>
val longPollCurrentState: StateFlow<LongPollState>
val online: StateFlow<Boolean>
val debugSettingsEnabled: StateFlow<Boolean>
val useContactNames: StateFlow<Boolean>
@@ -21,7 +24,8 @@ interface UserSettings {
fun useDynamicColorsChanged(use: Boolean)
fun useBlurChanged(use: Boolean)
fun useMultiline(use: Boolean)
fun setLongPollBackground(background: Boolean)
fun setLongPollStateToApply(newState: LongPollState)
fun updateLongPollCurrentState(currentState: LongPollState)
fun setOnline(use: Boolean)
fun enableDebugSettings(enable: Boolean)
fun onUseContactNamesChanged(use: Boolean)
@@ -45,12 +49,9 @@ class UserSettingsImpl(
)
)
override val longPollBackground = MutableStateFlow(
SettingsController.getBoolean(
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
)
)
override val longPollStateToApply = MutableStateFlow<LongPollState>(LongPollState.Stopped)
override val longPollCurrentState = MutableStateFlow<LongPollState>(LongPollState.Stopped)
override val online = MutableStateFlow(
SettingsController.getBoolean(
SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS,
@@ -107,8 +108,14 @@ class UserSettingsImpl(
theme.value = theme.value.copy(multiline = use)
}
override fun setLongPollBackground(background: Boolean) {
longPollBackground.value = background
override fun setLongPollStateToApply(newState: LongPollState) {
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) {
@@ -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 androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
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.util.AndroidUtils
import com.meloda.app.fast.datastore.SettingsController
@@ -81,32 +77,3 @@ fun Modifier.handleEnterKey(
action.invoke()
} 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()
}
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> {
return values.mapIndexed(transform).toImmutableList()
}
@@ -1,11 +1,5 @@
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.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -32,15 +26,19 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.window.DialogProperties
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.parseString
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)
@Composable
fun MaterialDialog(
@@ -58,17 +56,22 @@ fun MaterialDialog(
items: ImmutableList<UiText> = ImmutableList.empty(),
onItemClick: ((index: Int) -> Unit)? = null,
buttonsInvokeDismiss: Boolean = true,
properties: DialogProperties = DialogProperties(),
customContent: (@Composable ColumnScope.() -> Unit)? = null,
) {
var isVisible by remember {
var isVisible by rememberSaveable {
mutableStateOf(true)
}
val onDismissRequest = {
val onDismissRequest = remember {
{
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 {
mutableStateOf(
@@ -81,11 +84,10 @@ fun MaterialDialog(
)
}
if (isVisible) {
// AlertAnimation(visible = isVisible) {
AlertAnimation(visible = isVisible) {
BasicAlertDialog(
onDismissRequest = onDismissRequest
onDismissRequest = onDismissRequest,
properties = properties
) {
val scrollState = rememberScrollState()
val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } }
@@ -251,15 +253,16 @@ fun MaterialDialog(
@Composable
fun AlertAnimation(
visible: Boolean,
content: @Composable AnimatedVisibilityScope.() -> Unit
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(400)) +
scaleIn(animationSpec = tween(400)),
exit = fadeOut(animationSpec = tween(150)),
content = content
)
if (visible) content()
// AnimatedVisibility(
// visible = visible,
// enter = fadeIn(animationSpec = tween(400)) +
// scaleIn(animationSpec = tween(400)),
// exit = fadeOut(animationSpec = tween(150)),
// content = content
// )
}
@Composable
@@ -188,4 +188,14 @@
<string name="settings_activity_send_online_title">Быть «в сети»</string>
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</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>
@@ -246,5 +246,17 @@
<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_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>
<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.db.AccountsRepository
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.network.OAuthErrorDomain
import com.meloda.fast.auth.login.model.CaptchaArguments
import com.meloda.fast.auth.login.model.LoginError
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.LoginValidationArguments
import com.meloda.fast.auth.login.model.LoginValidationResult
import com.meloda.fast.auth.login.validation.LoginValidator
import kotlinx.coroutines.Dispatchers
@@ -67,7 +70,8 @@ class LoginViewModelImpl(
private val oAuthUseCase: OAuthUseCase,
private val usersUseCase: UsersUseCase,
private val accountsRepository: AccountsRepository,
private val loginValidator: LoginValidator
private val loginValidator: LoginValidator,
private val userSettings: UserSettings
) : ViewModel(), LoginViewModel {
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
@@ -155,6 +159,8 @@ class LoginViewModelImpl(
UserConfig.trustedHash = account.trustedHash
}
startLongPoll()
usersUseCase.get(
userIds = null,
fields = VkConstants.USER_FIELDS,
@@ -238,6 +244,8 @@ class LoginViewModelImpl(
UserConfig.trustedHash = account.trustedHash
}
startLongPoll()
accountsRepository.storeAccounts(listOf(currentAccount))
captchaArguments.update { null }
@@ -338,4 +346,14 @@ class LoginViewModelImpl(
}
}
}
private fun startLongPoll() {
userSettings.setLongPollStateToApply(
if (SettingsController.isLongPollInBackgroundEnabled) {
LongPollState.Background
} else {
LongPollState.InApp
}
)
}
}
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyListState
@@ -156,17 +155,13 @@ fun ConversationsScreen(
val view = LocalView.current
val currentTheme = LocalTheme.current
val maxLines by remember {
derivedStateOf {
if (currentTheme.multiline) 2 else 1
}
val maxLines by remember(currentTheme) {
mutableIntStateOf(if (currentTheme.multiline) 2 else 1)
}
val listState = rememberLazyListState()
val isListScrollingUp = listState.isScrollingUp()
val paginationConditionMet by remember {
val paginationConditionMet by remember(canPaginate, listState) {
derivedStateOf {
canPaginate &&
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
@@ -292,7 +287,7 @@ fun ConversationsScreen(
Column {
AnimatedVisibility(
visible = isListScrollingUp,
visible = listState.isScrollingUp(),
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
) {
@@ -351,10 +346,7 @@ fun ConversationsScreen(
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
ConversationsListComposable(
onConversationsClick = { id ->
onConversationItemClicked(id)
},
onConversationsClick = onConversationItemClicked,
onConversationsLongClick = onConversationItemLongClicked,
screenState = screenState,
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.UserSettings
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.settings.model.SettingsItem
import com.meloda.app.fast.settings.model.SettingsScreenState
@@ -161,7 +162,14 @@ class SettingsViewModelImpl(
when (key) {
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
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) {
// TODO: 26/11/2023, Danil Nikolaev: implement
+2 -4
View File
@@ -1,17 +1,15 @@
[versions]
agp = "8.5.0"
agp = "8.5.1"
converterMoshi = "2.11.0"
eithernet = "1.9.0"
haze = "0.7.3"
kotlin = "2.0.0"
ksp = "2.0.0-1.0.22"
vkompose = "0.5.4-k2"
compose-bom = "2024.06.00"
koin = "3.5.6"
accompanist = "0.34.0"
accompanist = "0.35.1-alpha"
coil = "2.6.0"
coroutines = "1.9.0-RC"
junit = "4.13.2"