profile avatar in bottom bar;

ability to open app settings from system's about app screen
This commit is contained in:
2024-12-12 10:44:34 +03:00
parent cd42a7992a
commit 3b66eb8da0
8 changed files with 124 additions and 60 deletions
@@ -1,11 +1,14 @@
package dev.meloda.fast
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.conena.nanokt.android.os.isMinSdk
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import dev.meloda.fast.auth.AuthGraph
@@ -15,11 +18,14 @@ import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.navigation.Main
import dev.meloda.fast.settings.navigation.Settings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -36,11 +42,13 @@ interface MainViewModel {
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean>
val profileImageUrl: StateFlow<String?>
fun onError(error: BaseError)
fun onNavigatedToAuth()
fun onAppResumed()
fun onAppResumed(intent: Intent)
@OptIn(ExperimentalPermissionsApi::class)
fun onPermissionCheckStatus(status: PermissionStatus)
@@ -55,14 +63,11 @@ interface MainViewModel {
class MainViewModelImpl(
private val getCurrentAccountUseCase: GetCurrentAccountUseCase,
private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val userSettings: UserSettings,
private val longPollController: LongPollController
) : MainViewModel, ViewModel() {
init {
loadAccounts()
}
override val startDestination = MutableStateFlow<Any?>(null)
override val isNeedToReplaceWithAuth = MutableStateFlow(false)
@@ -71,6 +76,11 @@ class MainViewModelImpl(
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
override val isNeedToRequestNotifications = MutableStateFlow(false)
override val profileImageUrl = MutableStateFlow<String?>(null)
private var openNotificationsSettings = false
private var openAppSettings = false
override fun onError(error: BaseError) {
when (error) {
BaseError.SessionExpired -> {
@@ -83,7 +93,12 @@ class MainViewModelImpl(
isNeedToReplaceWithAuth.update { false }
}
override fun onAppResumed() {
override fun onAppResumed(intent: Intent) {
openNotificationsSettings =
intent.hasCategory(NotificationCompat.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
openAppSettings =
isMinSdk(Build.VERSION_CODES.N) && intent.action == Intent.ACTION_APPLICATION_PREFERENCES
if (isNeedToShowNotificationsRationaleDialog.value) {
isNeedToShowNotificationsRationaleDialog.update { false }
isNeedToCheckNotificationsPermission.update { true }
@@ -100,6 +115,8 @@ class MainViewModelImpl(
.take(5)
userSettings.onAppLanguageChanged(newLanguage)
loadAccounts()
}
@ExperimentalPermissionsApi
@@ -151,6 +168,22 @@ class MainViewModelImpl(
disableBackgroundLongPoll()
}
private fun loadProfile() {
loadUserByIdUseCase(userId = null)
.listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
profileImageUrl.emit(null)
},
success = { response ->
val user = response ?: return@listenValue
profileImageUrl.emit(user.photo100)
}
)
}
}
private fun listenLongPollState() {
longPollController.stateToApply.listenValue(viewModelScope) { newState ->
if (newState == LongPollState.Background) {
@@ -184,9 +217,17 @@ class MainViewModelImpl(
)
}
if (currentAccount != null) {
loadProfile()
}
startDestination.setValue {
if (currentAccount == null) AuthGraph
else Main
when {
openAppSettings -> Settings
openNotificationsSettings -> Settings
currentAccount == null -> AuthGraph
else -> Main
}
}
}
}
@@ -2,6 +2,7 @@ package dev.meloda.fast.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import dev.meloda.fast.MainViewModel
import dev.meloda.fast.conversations.navigation.Conversations
import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.model.BaseError
@@ -22,7 +23,8 @@ fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit,
onConversationClicked: (conversationId: Int) -> Unit,
onPhotoClicked: (url: String) -> Unit
onPhotoClicked: (url: String) -> Unit,
viewModel: MainViewModel
) {
val navigationItems = ImmutableList.of(
BottomNavigationItem(
@@ -51,7 +53,8 @@ fun NavGraphBuilder.mainScreen(
onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked,
onConversationItemClicked = onConversationClicked,
onPhotoClicked = onPhotoClicked
onPhotoClicked = onPhotoClicked,
viewModel = viewModel
)
}
}
@@ -99,7 +99,7 @@ class MainActivity : AppCompatActivity() {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
LifecycleResumeEffect(true) {
viewModel.onAppResumed()
viewModel.onAppResumed(intent)
onPauseOrDispose {}
}
@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
@@ -16,19 +18,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import coil.compose.SubcomposeAsyncImage
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.MainViewModel
import dev.meloda.fast.conversations.navigation.conversationsScreen
import dev.meloda.fast.friends.navigation.friendsScreen
import dev.meloda.fast.model.BaseError
@@ -47,12 +55,15 @@ fun MainScreen(
onError: (BaseError) -> Unit = {},
onSettingsButtonClicked: () -> Unit = {},
onConversationItemClicked: (conversationId: Int) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}
onPhotoClicked: (url: String) -> Unit = {},
viewModel: MainViewModel
) {
val currentTheme = LocalThemeConfig.current
val hazeState = remember { HazeState() }
val navController = rememberNavController()
val profileImageUrl by viewModel.profileImageUrl.collectAsStateWithLifecycle()
var selectedItemIndex by rememberSaveable {
mutableIntStateOf(1)
}
@@ -90,13 +101,39 @@ fun MainScreen(
}
},
icon = {
Icon(
painter = painterResource(
id = if (selectedItemIndex == index) item.selectedIconResId
else item.unselectedIconResId
),
contentDescription = null
)
if (index == navigationItems.size - 1) {
var isLoading by remember {
mutableStateOf(true)
}
if (isLoading) {
Icon(
painter = painterResource(
id = if (selectedItemIndex == index) item.selectedIconResId
else item.unselectedIconResId
),
contentDescription = null
)
}
SubcomposeAsyncImage(
model = profileImageUrl,
contentDescription = "Profile Image",
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
.alpha(if (isLoading) 0f else 1f),
onSuccess = {
isLoading = false
}
)
} else {
Icon(
painter = painterResource(
id = if (selectedItemIndex == index) item.selectedIconResId
else item.unselectedIconResId
),
contentDescription = null
)
}
},
alwaysShowLabel = false
)
@@ -123,7 +123,8 @@ fun RootScreen(
onError = viewModel::onError,
onSettingsButtonClicked = navController::navigateToSettings,
onConversationClicked = navController::navigateToMessagesHistory,
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
viewModel = viewModel
)
messagesHistoryScreen(