From 497d8a5a0961a0622f57881181bfcdb849298e00 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Wed, 22 Jan 2025 21:16:53 +0100 Subject: [PATCH] DROID-3225 Vault | Enhancement | Replace settings icon by user icon (#2022) --- .../anytype/di/feature/vault/VaultDI.kt | 2 + .../anytype/ui/vault/VaultFragment.kt | 3 +- .../anytypeio/anytype/ui/vault/VaultScreen.kt | 81 ++++++++++++++++--- .../features/profile/ParticipantScreen.kt | 2 +- .../ui_settings/account/ProfileScreen.kt | 11 +-- .../account/ProfileSettingsViewModel.kt | 9 +-- .../presentation/profile/ProfileIconView.kt | 9 ++- .../presentation/vault/VaultViewModel.kt | 26 +++++- 8 files changed, 113 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt index 544693e81d..46cb95e58c 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt @@ -16,6 +16,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.other.DefaultSpaceInviteResolver import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate @@ -79,4 +80,5 @@ interface VaultComponentDependencies : ComponentDependencies { fun appActionManager(): AppActionManager fun logger(): Logger fun awaitAccount(): AwaitAccountStartManager + fun profileContainer(): ProfileSubscriptionManager } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt index 1ca33220e6..6d00e0617f 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt @@ -57,7 +57,8 @@ class VaultFragment : BaseComposeFragment() { onSpaceClicked = vm::onSpaceClicked, onCreateSpaceClicked = vm::onCreateSpaceClicked, onSettingsClicked = vm::onSettingsClicked, - onOrderChanged = vm::onOrderChanged + onOrderChanged = vm::onOrderChanged, + profile = vm.profileView.collectAsStateWithLifecycle().value ) } LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt index 0dcfed3a2c..4398c369f0 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt @@ -1,6 +1,7 @@ package com.anytypeio.anytype.ui.vault import android.content.res.Configuration +import android.graphics.drawable.Drawable import android.os.Build.VERSION.SDK_INT import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -21,7 +22,9 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -34,13 +37,16 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt +import coil.compose.rememberAsyncImagePainter import com.anytypeio.anytype.BuildConfig.USE_EDGE_TO_EDGE import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id @@ -60,15 +66,22 @@ import com.anytypeio.anytype.core_ui.views.Relations3 import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK import com.anytypeio.anytype.presentation.editor.cover.CoverGradient +import com.anytypeio.anytype.presentation.profile.AccountProfile +import com.anytypeio.anytype.presentation.profile.ProfileIconView import com.anytypeio.anytype.presentation.spaces.SelectSpaceViewModel import com.anytypeio.anytype.presentation.spaces.SpaceIconView import com.anytypeio.anytype.presentation.vault.VaultViewModel.VaultSpaceView import com.anytypeio.anytype.presentation.wallpaper.WallpaperColor +import com.anytypeio.anytype.ui.sharing.SharingData import com.anytypeio.anytype.ui.widgets.types.gradient +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.bumptech.glide.request.RequestListener @Composable fun VaultScreen( + profile: AccountProfile, spaces: List, onSpaceClicked: (VaultSpaceView) -> Unit, onCreateSpaceClicked: () -> Unit, @@ -109,6 +122,7 @@ fun VaultScreen( ) { VaultScreenToolbar( + profile = profile, onPlusClicked = onCreateSpaceClicked, onSettingsClicked = onSettingsClicked, spaceCountLimitReached = spaces.size >= SelectSpaceViewModel.MAX_SPACE_COUNT @@ -170,8 +184,10 @@ fun VaultScreen( } +@OptIn(ExperimentalGlideComposeApi::class) @Composable fun VaultScreenToolbar( + profile: AccountProfile, spaceCountLimitReached: Boolean = false, onPlusClicked: () -> Unit, onSettingsClicked: () -> Unit @@ -187,16 +203,57 @@ fun VaultScreenToolbar( color = colorResource(id = R.color.text_primary), modifier = Modifier.align(Alignment.Center) ) - Image( - painter = painterResource(id = R.drawable.ic_vault_settings), - contentDescription = "Settings icon", - modifier = Modifier - .align(Alignment.CenterStart) - .padding(start = 16.dp) - .noRippleClickable { - onSettingsClicked() + when(profile) { + is AccountProfile.Data -> { + Box( + Modifier + .align(Alignment.CenterStart) + .padding(start = 16.dp) + .size(28.dp) + .noRippleClickable { + onSettingsClicked() + } + ) { + when(val icon = profile.icon) { + is ProfileIconView.Image -> { + GlideImage( + model = icon.url, + contentDescription = "Custom image profile", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + ) + } + else -> { + val nameFirstChar = if (profile.name.isEmpty()) { + stringResource(id = com.anytypeio.anytype.ui_settings.R.string.account_default_name) + } else { + profile.name.first().uppercaseChar().toString() + } + Box( + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + .background(colorResource(id = com.anytypeio.anytype.ui_settings.R.color.text_tertiary)) + ) { + Text( + text = nameFirstChar, + style = MaterialTheme.typography.h3.copy( + color = colorResource(id = com.anytypeio.anytype.ui_settings.R.color.text_white), + fontSize = 20.sp + ), + modifier = Modifier.align(Alignment.Center) + ) + } + } + } } - ) + } + AccountProfile.Idle -> { + // Draw nothing + } + } if (!spaceCountLimitReached) { Image( painter = painterResource(id = R.drawable.ic_vault_top_toolbar_plus), @@ -411,7 +468,8 @@ fun LoadingSpaceCardPreview() { fun VaultScreenToolbarPreview() { VaultScreenToolbar( onPlusClicked = {}, - onSettingsClicked = {} + onSettingsClicked = {}, + profile = AccountProfile.Idle ) } @@ -449,6 +507,7 @@ fun VaultScreenPreview() { onSpaceClicked = {}, onCreateSpaceClicked = {}, onSettingsClicked = {}, - onOrderChanged = {} + onOrderChanged = {}, + profile = AccountProfile.Idle ) } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/profile/ParticipantScreen.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/profile/ParticipantScreen.kt index 7a81f6d859..f9fa17ce83 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/profile/ParticipantScreen.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/profile/ParticipantScreen.kt @@ -249,7 +249,7 @@ fun ParticipantScreenPreview() { ParticipantScreen( uiState = UiParticipantScreenState( name = "Jetpack Compose", - icon = ProfileIconView.Emoji("M"), + icon = ProfileIconView.Placeholder("M"), identity = "AnyId43", description = "some description", isOwner = true diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt index 28c4579585..6912383ae3 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt @@ -59,6 +59,7 @@ import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.core_ui.views.Caption1Regular import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.core_models.membership.MembershipStatus +import com.anytypeio.anytype.presentation.profile.AccountProfile import com.anytypeio.anytype.presentation.profile.ProfileIconView import com.anytypeio.anytype.ui_settings.R import kotlinx.coroutines.FlowPreview @@ -73,7 +74,7 @@ fun ProfileSettingsScreen( isLogoutInProgress: Boolean, onNameChange: (String) -> Unit, onProfileIconClick: () -> Unit, - account: ProfileSettingsViewModel.AccountProfile, + account: AccountProfile, onAppearanceClicked: () -> Unit, onDataManagementClicked: () -> Unit, onAboutClicked: () -> Unit, @@ -269,12 +270,12 @@ fun ActionWithProgressBar( @Composable private fun Header( modifier: Modifier = Modifier, - account: ProfileSettingsViewModel.AccountProfile, + account: AccountProfile, onProfileIconClick: () -> Unit, onNameSet: (String) -> Unit ) { when (account) { - is ProfileSettingsViewModel.AccountProfile.Data -> { + is AccountProfile.Data -> { Box(modifier = modifier.padding(vertical = 6.dp)) { Dragger() } @@ -290,7 +291,7 @@ private fun Header( } ProfileNameBlock(name = account.name, onNameSet = onNameSet) } - is ProfileSettingsViewModel.AccountProfile.Idle -> {} + is AccountProfile.Idle -> {} } } @@ -448,7 +449,7 @@ private fun ProfileSettingPreview() { isLogoutInProgress = false, onNameChange = {}, onProfileIconClick = {}, - account = ProfileSettingsViewModel.AccountProfile.Data( + account = AccountProfile.Data( "Walter", icon = ProfileIconView.Placeholder("Walter") ), diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt index 908e28062e..e47db809e3 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt @@ -28,6 +28,7 @@ import com.anytypeio.anytype.presentation.extension.sendScreenSettingsDeleteEven import com.anytypeio.anytype.core_models.membership.MembershipStatus import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider +import com.anytypeio.anytype.presentation.profile.AccountProfile import com.anytypeio.anytype.presentation.profile.ProfileIconView import com.anytypeio.anytype.presentation.profile.profileIcon import kotlinx.coroutines.Job @@ -149,14 +150,6 @@ class ProfileSettingsViewModel( } } - sealed class AccountProfile { - data object Idle: AccountProfile() - class Data( - val name: String, - val icon: ProfileIconView - ): AccountProfile() - } - class Factory( private val analytics: Analytics, private val container: StorelessSubscriptionContainer, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt index db66ce840e..29545568c1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt @@ -7,7 +7,6 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder sealed class ProfileIconView { object Loading : ProfileIconView() data class Placeholder(val name: String?) : ProfileIconView() - data class Emoji(val unicode: String) : ProfileIconView() data class Image(val url: Url) : ProfileIconView() } @@ -19,4 +18,12 @@ fun ObjectWrapper.Basic.profileIcon(builder: UrlBuilder): ProfileIconView = when else -> ProfileIconView.Placeholder( name = name?.trim()?.ifEmpty { null } ) +} + +sealed class AccountProfile { + data object Idle: AccountProfile() + class Data( + val name: String, + val icon: ProfileIconView + ): AccountProfile() } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt index c328ab488e..e75fc66fe9 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt @@ -21,6 +21,7 @@ import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager import com.anytypeio.anytype.domain.spaces.SaveCurrentSpace import com.anytypeio.anytype.domain.vault.GetVaultSettings import com.anytypeio.anytype.domain.vault.ObserveVaultSettings @@ -34,6 +35,8 @@ import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.home.navigation import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate import com.anytypeio.anytype.presentation.navigation.NavigationViewModel +import com.anytypeio.anytype.presentation.profile.AccountProfile +import com.anytypeio.anytype.presentation.profile.profileIcon import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider import com.anytypeio.anytype.presentation.spaces.SpaceIconView import com.anytypeio.anytype.presentation.spaces.spaceIcon @@ -42,10 +45,13 @@ import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import timber.log.Timber @@ -63,12 +69,24 @@ class VaultViewModel( private val analytics: Analytics, private val deepLinkToObjectDelegate: DeepLinkToObjectDelegate, private val appActionManager: AppActionManager, - private val spaceInviteResolver: SpaceInviteResolver + private val spaceInviteResolver: SpaceInviteResolver, + private val profileContainer: ProfileSubscriptionManager ) : NavigationViewModel(), DeepLinkToObjectDelegate by deepLinkToObjectDelegate { val spaces = MutableStateFlow>(emptyList()) val commands = MutableSharedFlow(replay = 0) + val profileView = profileContainer.observe().map { obj -> + AccountProfile.Data( + name = obj.name.orEmpty(), + icon = obj.profileIcon(urlBuilder) + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(1000L), + AccountProfile.Idle + ) + init { Timber.i("VaultViewModel, init") viewModelScope.launch { @@ -343,7 +361,8 @@ class VaultViewModel( private val analytics: Analytics, private val deepLinkToObjectDelegate: DeepLinkToObjectDelegate, private val appActionManager: AppActionManager, - private val spaceInviteResolver: SpaceInviteResolver + private val spaceInviteResolver: SpaceInviteResolver, + private val profileContainer: ProfileSubscriptionManager ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -361,7 +380,8 @@ class VaultViewModel( analytics = analytics, deepLinkToObjectDelegate = deepLinkToObjectDelegate, appActionManager = appActionManager, - spaceInviteResolver = spaceInviteResolver + spaceInviteResolver = spaceInviteResolver, + profileContainer = profileContainer ) as T }