profile avatar in bottom bar;
ability to open app settings from system's about app screen
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user