Add theme option to disable animations and fix account avatar loading in bottom bar

This commit is contained in:
2025-03-28 19:59:38 +03:00
parent 9aa85d40c6
commit da9644cde1
14 changed files with 104 additions and 84 deletions
@@ -24,6 +24,7 @@ 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.model.api.domain.VkUser
import dev.meloda.fast.navigation.Main
import dev.meloda.fast.settings.navigation.Settings
import kotlinx.coroutines.Dispatchers
@@ -36,14 +37,13 @@ interface MainViewModel {
val startDestination: StateFlow<Any?>
val isNeedToReplaceWithAuth: StateFlow<Boolean>
val currentUser: StateFlow<VkUser?>
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
val isNeedToRequestNotifications: StateFlow<Boolean>
val profileImageUrl: StateFlow<String?>
fun onError(error: BaseError)
fun onNavigatedToAuth()
@@ -59,6 +59,8 @@ interface MainViewModel {
fun onNotificationsDeniedDialogDismissed()
fun onNotificationsRationaleDialogDismissed()
fun onNotificationsRationaleDialogCancelClicked()
fun onUserAuthenticated()
}
class MainViewModelImpl(
@@ -70,14 +72,13 @@ class MainViewModelImpl(
override val startDestination = MutableStateFlow<Any?>(null)
override val isNeedToReplaceWithAuth = MutableStateFlow(false)
override val currentUser = MutableStateFlow<VkUser?>(null)
override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
override val isNeedToRequestNotifications = MutableStateFlow(false)
override val profileImageUrl = MutableStateFlow<String?>(null)
private var openNotificationsSettings = false
private var openAppSettings = false
@@ -170,17 +171,20 @@ class MainViewModelImpl(
disableBackgroundLongPoll()
}
override fun onUserAuthenticated() {
loadProfile()
}
private fun loadProfile() {
loadUserByIdUseCase(userId = null)
.listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
profileImageUrl.emit(null)
currentUser.emit(null)
},
success = { response ->
val user = response ?: return@listenValue
profileImageUrl.emit(user.photo100)
currentUser.emit(user)
}
)
}
@@ -2,7 +2,6 @@ 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
@@ -25,8 +24,7 @@ fun NavGraphBuilder.mainScreen(
onConversationClicked: (conversationId: Int) -> Unit,
onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit,
onCreateChatClicked: () -> Unit,
viewModel: MainViewModel
onCreateChatClicked: () -> Unit
) {
val navigationItems = ImmutableList.of(
BottomNavigationItem(
@@ -57,8 +55,7 @@ fun NavGraphBuilder.mainScreen(
onConversationItemClicked = onConversationClicked,
onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked,
onCreateChatClicked = onCreateChatClicked,
viewModel = viewModel
onCreateChatClicked = onCreateChatClicked
)
}
}
@@ -38,6 +38,7 @@ import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.service.OnlineService
import dev.meloda.fast.service.longpolling.LongPollingService
import dev.meloda.fast.ui.model.DeviceSize
@@ -46,6 +47,7 @@ import dev.meloda.fast.ui.model.ThemeConfig
import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.theme.LocalSizeConfig
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.KoinContext
@@ -98,6 +100,8 @@ class MainActivity : AppCompatActivity() {
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent)
onPauseOrDispose {}
@@ -202,6 +206,7 @@ class MainActivity : AppCompatActivity() {
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
@@ -214,7 +219,7 @@ class MainActivity : AppCompatActivity() {
setDarkMode,
useSystemFont
) {
mutableStateOf(
derivedStateOf {
ThemeConfig(
darkMode = setDarkMode,
dynamicColors = dynamicColors,
@@ -222,14 +227,16 @@ class MainActivity : AppCompatActivity() {
amoledDark = amoledDark,
enableBlur = enableBlur,
enableMultiline = enableMultiline,
useSystemFont = useSystemFont
useSystemFont = useSystemFont,
enableAnimations = enableAnimations
)
)
}
}
CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig
LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser
) {
AppTheme(
useDarkTheme = themeConfig.darkMode,
@@ -16,6 +16,7 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -27,7 +28,6 @@ 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
@@ -36,7 +36,6 @@ import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
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
@@ -45,6 +44,7 @@ import dev.meloda.fast.navigation.MainGraph
import dev.meloda.fast.profile.navigation.profileScreen
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.ImmutableList
@OptIn(ExperimentalHazeMaterialsApi::class)
@@ -56,19 +56,21 @@ fun MainScreen(
onConversationItemClicked: (conversationId: Int) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userId: Int) -> Unit = {},
onCreateChatClicked: () -> Unit = {},
viewModel: MainViewModel
onCreateChatClicked: () -> Unit = {}
) {
val currentTheme = LocalThemeConfig.current
val hazeState = remember { HazeState() }
val navController = rememberNavController()
val profileImageUrl by viewModel.profileImageUrl.collectAsStateWithLifecycle()
var selectedItemIndex by rememberSaveable {
mutableIntStateOf(1)
}
val user = LocalUser.current
val profileImageUrl by remember(user) {
derivedStateOf { user?.photo100 }
}
Scaffold(
bottomBar = {
NavigationBar(
@@ -122,9 +124,7 @@ fun MainScreen(
.size(24.dp)
.clip(CircleShape)
.alpha(if (isLoading) 0f else 1f),
onSuccess = {
isLoading = false
}
onSuccess = { isLoading = false }
)
} else {
Icon(
@@ -118,7 +118,10 @@ fun RootScreen(
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
authNavGraph(
onNavigateToMain = navController::navigateToMain,
onNavigateToMain = {
viewModel.onUserAuthenticated()
navController.navigateToMain()
},
navController = navController
)
mainScreen(
@@ -127,8 +130,7 @@ fun RootScreen(
onConversationClicked = navController::navigateToMessagesHistory,
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
onMessageClicked = navController::navigateToMessagesHistory,
onCreateChatClicked = navController::navigateToCreateChat,
viewModel = viewModel
onCreateChatClicked = navController::navigateToCreateChat
)
messagesHistoryScreen(
@@ -24,6 +24,7 @@ interface UserSettings {
val showEmojiButton: StateFlow<Boolean>
val showTimeInActionMessages: StateFlow<Boolean>
val useSystemFont: StateFlow<Boolean>
val enableAnimations: StateFlow<Boolean>
val showDebugCategory: StateFlow<Boolean>
fun onUseContactNamesChanged(use: Boolean)
@@ -68,6 +69,7 @@ class UserSettingsImpl : UserSettings {
override val showTimeInActionMessages =
MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages)
override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont)
override val enableAnimations = MutableStateFlow(AppSettings.Experimental.moreAnimations)
override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory)
override fun onUseContactNamesChanged(use: Boolean) {
@@ -7,5 +7,6 @@ data class ThemeConfig(
val amoledDark: Boolean,
val enableBlur: Boolean,
val enableMultiline: Boolean,
val useSystemFont: Boolean
val useSystemFont: Boolean,
val enableAnimations: Boolean
)
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import dev.chrisbanes.haze.HazeState
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.model.SizeConfig
@@ -113,7 +114,8 @@ val LocalThemeConfig = compositionLocalOf {
amoledDark = false,
enableBlur = false,
enableMultiline = false,
useSystemFont = false
useSystemFont = false,
enableAnimations = false
)
}
@@ -124,13 +126,9 @@ val LocalSizeConfig = compositionLocalOf {
)
}
val LocalHazeState = compositionLocalOf {
HazeState()
}
val LocalBottomPadding = compositionLocalOf {
0.dp
}
val LocalHazeState = compositionLocalOf { HazeState() }
val LocalBottomPadding = compositionLocalOf { 0.dp }
val LocalUser = compositionLocalOf<VkUser?> { null }
@Composable
fun AppTheme(
@@ -26,7 +26,7 @@ import dev.meloda.fast.conversations.model.ConversationsScreenState
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.ui.model.api.ConversationOption
import dev.meloda.fast.ui.model.api.UiConversation
import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.theme.LocalThemeConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -41,6 +41,7 @@ fun ConversationsList(
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
padding: PaddingValues
) {
val theme = LocalThemeConfig.current
val coroutineScope = rememberCoroutineScope()
LazyColumn(
@@ -68,7 +69,12 @@ fun ConversationsList(
maxLines = maxLines,
isUserAccount = isUserAccount,
conversation = conversation,
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
modifier =
if (theme.enableAnimations) Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
)
else Modifier
)
Spacer(modifier = Modifier.height(8.dp))
@@ -78,7 +84,14 @@ fun ConversationsList(
Column(
modifier = Modifier
.fillMaxWidth()
.animateItem(fadeInSpec = null, fadeOutSpec = null),
.then(
if (theme.enableAnimations)
Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
)
else Modifier
),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (screenState.isPaginating) {
@@ -5,10 +5,6 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@@ -283,23 +279,17 @@ fun ConversationsScreen(
)
Column {
// AnimatedVisibility(
// visible = listState.isScrollingUp(),
// enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
// exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
// ) {
FloatingActionButton(
onClick = onCreateChatButtonClicked,
modifier = Modifier.offset {
IntOffset(0, offsetY)
}
) {
Icon(
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
contentDescription = "Add chat button"
)
FloatingActionButton(
onClick = onCreateChatButtonClicked,
modifier = Modifier.offset {
IntOffset(0, offsetY)
}
// }
) {
Icon(
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
contentDescription = "Add chat button"
)
}
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
}
@@ -27,14 +27,21 @@ import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.imageLoader
import dev.meloda.fast.messageshistory.model.UiItem
import dev.meloda.fast.ui.theme.LocalThemeConfig
@Composable
fun IncomingMessageBubble(
modifier: Modifier = Modifier,
message: UiItem.Message,
animate: Boolean,
) {
Row(modifier = modifier.fillMaxWidth().then(if (animate) Modifier.animateContentSize() else Modifier),) {
Row(
modifier = modifier
.fillMaxWidth()
.then(
if (LocalThemeConfig.current.enableAnimations) Modifier.animateContentSize()
else Modifier
),
) {
Row(
modifier = Modifier
.fillMaxWidth(0.85f)
@@ -81,13 +88,12 @@ fun IncomingMessageBubble(
isOut = false,
date = message.date,
edited = message.isEdited,
animate = animate,
isRead = message.isRead,
sendingStatus = message.sendingStatus,
pinned = message.isPinned
)
}
}
Spacer(modifier=Modifier.fillMaxWidth(0.25f))
Spacer(modifier = Modifier.fillMaxWidth(0.25f))
}
}
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import dev.meloda.fast.messageshistory.model.SendingStatus
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.R as UiR
@Composable
@@ -39,11 +40,11 @@ fun MessageBubble(
isOut: Boolean,
date: String?,
edited: Boolean,
animate: Boolean,
isRead: Boolean,
sendingStatus: SendingStatus,
pinned: Boolean
) {
val theme = LocalThemeConfig.current
val backgroundColor = if (!isOut) {
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
} else {
@@ -65,7 +66,7 @@ fun MessageBubble(
horizontal = 8.dp,
vertical = 6.dp
)
.then(if (animate) Modifier.animateContentSize() else Modifier),
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
) {
val minDateContainerWidth = remember(edited, isOut) {
val mainPart = if (edited) 50.dp else 30.dp
@@ -89,7 +90,7 @@ fun MessageBubble(
.padding(end = 4.dp)
.padding(end = dateContainerWidth)
.padding(end = 4.dp)
.then(if (animate) Modifier.animateContentSize() else Modifier),
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
color = textColor
)
}
@@ -98,7 +99,7 @@ fun MessageBubble(
modifier = Modifier
.align(Alignment.BottomEnd)
.defaultMinSize(minWidth = dateContainerWidth)
.then(if (animate) Modifier.animateContentSize() else Modifier),
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
) {
if (pinned) {
Icon(
@@ -49,20 +49,17 @@ fun MessagesList(
onMessageClicked: (Int) -> Unit = {},
onMessageLongClicked: (Int) -> Unit = {}
) {
val enableAnimations = remember {
AppSettings.Experimental.moreAnimations
}
val messages = remember(immutableMessages) {
immutableMessages.toList()
}
val currentTheme = LocalThemeConfig.current
val theme = LocalThemeConfig.current
val view = LocalView.current
LazyColumn(
modifier = modifier
.fillMaxWidth()
.then(
if (currentTheme.enableBlur) {
if (theme.enableBlur) {
Modifier.hazeSource(state = hazeState)
} else Modifier
),
@@ -93,7 +90,7 @@ fun MessagesList(
is UiItem.ActionMessage -> {
ActionMessageItem(
modifier = Modifier.then(
if (enableAnimations) Modifier.animateItem(
if (theme.enableAnimations) Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
) else Modifier
@@ -119,7 +116,7 @@ fun MessagesList(
Surface(
modifier = Modifier
.then(
if (enableAnimations) Modifier.animateItem(
if (theme.enableAnimations) Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
) else Modifier
@@ -141,14 +138,13 @@ fun MessagesList(
Modifier
.padding(vertical = 4.dp)
.then(
if (enableAnimations) Modifier.animateItem(
if (theme.enableAnimations) Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
)
else Modifier
),
message = item,
animate = enableAnimations
message = item
)
} else {
IncomingMessageBubble(
@@ -156,14 +152,13 @@ fun MessagesList(
Modifier
.padding(vertical = 4.dp)
.then(
if (enableAnimations) Modifier.animateItem(
if (theme.enableAnimations) Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
)
else Modifier
),
message = item,
animate = enableAnimations
message = item
)
}
}
@@ -12,15 +12,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.meloda.fast.common.extensions.orDots
import dev.meloda.fast.messageshistory.model.UiItem
import dev.meloda.fast.ui.theme.LocalThemeConfig
@Composable
fun OutgoingMessageBubble(
modifier: Modifier = Modifier,
message: UiItem.Message,
animate: Boolean
) {
Row(
modifier = modifier.fillMaxWidth().then(if (animate) Modifier.animateContentSize() else Modifier),
modifier = modifier
.fillMaxWidth()
.then(
if (LocalThemeConfig.current.enableAnimations) Modifier.animateContentSize()
else Modifier
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
@@ -37,7 +42,6 @@ fun OutgoingMessageBubble(
isOut = true,
date = message.date,
edited = message.isEdited,
animate = animate,
isRead = message.isRead,
sendingStatus = message.sendingStatus,
pinned = message.isPinned