From 7983260d81fca8c97f5fa4fcdfb07bcfec555c32 Mon Sep 17 00:00:00 2001 From: Damian Kaczmarek <76782439+damian-kaczmarek@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:45:49 +0100 Subject: [PATCH] feat: add on profile account details btn WPB-970 (#3654) --- .../wire/android/mapper/OtherAccountMapper.kt | 5 +- .../ui/userprofile/self/OtherAccounts.kt | 67 ++++++ .../userprofile/self/SelfUserProfileScreen.kt | 217 ++++++++++-------- .../self/SelfUserProfileViewModel.kt | 2 +- .../ui/userprofile/self/model/OtherAccount.kt | 2 +- app/src/main/res/values/strings.xml | 4 +- .../android/mapper/OtherAccountMapperTest.kt | 11 +- 7 files changed, 201 insertions(+), 107 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/self/OtherAccounts.kt diff --git a/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt index 3a9b0d2889e..1c4fdf946b9 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/OtherAccountMapper.kt @@ -20,15 +20,14 @@ package com.wire.android.mapper import com.wire.android.ui.home.conversations.avatar import com.wire.android.ui.userprofile.self.model.OtherAccount -import com.wire.kalium.logic.data.team.Team import com.wire.kalium.logic.data.user.SelfUser import javax.inject.Inject class OtherAccountMapper @Inject constructor() { - fun toOtherAccount(selfUser: SelfUser, team: Team?): OtherAccount = OtherAccount( + fun toOtherAccount(selfUser: SelfUser): OtherAccount = OtherAccount( id = selfUser.id, fullName = selfUser.name ?: "", avatarData = selfUser.avatar(selfUser.connectionStatus), - teamName = team?.name + handle = selfUser.handle ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/OtherAccounts.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/OtherAccounts.kt new file mode 100644 index 00000000000..a566c94cd60 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/OtherAccounts.kt @@ -0,0 +1,67 @@ +package com.wire.android.ui.userprofile.self + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.model.Clickable +import com.wire.android.ui.common.ArrowRightIcon +import com.wire.android.ui.common.RowItemTemplate +import com.wire.android.ui.common.avatar.UserProfileAvatar +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.home.conversations.search.HighlightName +import com.wire.android.ui.home.conversations.search.HighlightSubtitle +import com.wire.android.ui.home.conversationslist.common.FolderHeader +import com.wire.android.ui.theme.wireDimensions +import com.wire.android.ui.userprofile.self.model.OtherAccount + +@Composable +internal fun OtherAccountsHeader() { + FolderHeader(stringResource(id = R.string.user_profile_other_accs)) +} + +@Composable +internal fun OtherAccountItem( + account: OtherAccount, + clickable: Clickable = Clickable(enabled = true) {}, +) { + RowItemTemplate( + leadingIcon = { UserProfileAvatar(account.avatarData) }, + titleStartPadding = dimensions().spacing0x, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + HighlightName( + name = account.fullName, + modifier = Modifier.weight(weight = 1f, fill = false), + searchQuery = "" + ) + } + }, + subtitle = { + val subTitle = buildString { + append(account.handle ?: "") + if (account.id.domain.isNotBlank()) { + append("@${account.id.domain}") + } + } + HighlightSubtitle(subTitle = subTitle) + }, + actions = { + Box( + modifier = Modifier + .wrapContentWidth() + .padding(end = MaterialTheme.wireDimensions.spacing8x) + ) { + ArrowRightIcon(Modifier.align(Alignment.TopEnd), R.string.content_description_empty) + } + }, + clickable = clickable, + modifier = Modifier.padding(start = dimensions().spacing8x) + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index 70a44582e10..e09da4b1532 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -27,16 +27,16 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.Divider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -44,6 +44,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties @@ -60,12 +61,9 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination import com.wire.android.navigation.style.PopUpNavigationAnimation -import com.wire.android.ui.common.ArrowRightIcon -import com.wire.android.ui.common.RowItemTemplate -import com.wire.android.ui.common.avatar.UserProfileAvatar -import com.wire.android.ui.common.avatar.UserStatusIndicator import com.wire.android.ui.common.VisibilityState import com.wire.android.ui.common.WireDropDown +import com.wire.android.ui.common.avatar.UserStatusIndicator import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireSecondaryButton @@ -73,16 +71,16 @@ import com.wire.android.ui.common.dialogs.ProgressDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.AppSettingsScreenDestination import com.wire.android.ui.destinations.AvatarPickerScreenDestination +import com.wire.android.ui.destinations.MyAccountScreenDestination import com.wire.android.ui.destinations.SelfQRCodeScreenDestination import com.wire.android.ui.destinations.TeamMigrationScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination -import com.wire.android.ui.home.conversations.search.HighlightName -import com.wire.android.ui.home.conversations.search.HighlightSubtitle import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.legalhold.banner.LegalHoldPendingBanner import com.wire.android.ui.legalhold.banner.LegalHoldSubjectBanner @@ -92,6 +90,7 @@ import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedState import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedViewModel import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectProfileSelfDialog import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.userprofile.common.EditableState import com.wire.android.ui.userprofile.common.UserProfileInfo @@ -123,7 +122,13 @@ fun SelfUserProfileScreen( state = viewModelSelf.userProfileState, onCloseClick = navigator::navigateBack, logout = { viewModelSelf.logout(it, NavigationSwitchAccountActions(navigator::navigate)) }, - onChangeUserProfilePicture = { navigator.navigate(NavigationCommand(AvatarPickerScreenDestination)) }, + onChangeUserProfilePicture = { + navigator.navigate( + NavigationCommand( + AvatarPickerScreenDestination + ) + ) + }, onEditClick = { navigator.navigate(NavigationCommand(AppSettingsScreenDestination)) }, onStatusClicked = viewModelSelf::changeStatusClick, onAddAccountClick = { navigator.navigate(NavigationCommand(WelcomeScreenDestination)) }, @@ -133,7 +138,12 @@ fun SelfUserProfileScreen( onMessageShown = viewModelSelf::clearErrorMessage, onLegalHoldAcceptClick = legalHoldRequestedViewModel::show, onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, - onOtherAccountClick = { viewModelSelf.switchAccount(it, NavigationSwitchAccountActions(navigator::navigate)) }, + onOtherAccountClick = { + viewModelSelf.switchAccount( + it, + NavigationSwitchAccountActions(navigator::navigate) + ) + }, onQrCodeClick = { viewModelSelf.trackQrCodeClick() navigator.navigate(NavigationCommand(SelfQRCodeScreenDestination(viewModelSelf.userProfileState.userName))) @@ -142,6 +152,7 @@ fun SelfUserProfileScreen( viewModelSelf.sendPersonalToTeamMigrationEvent() navigator.navigate(NavigationCommand(TeamMigrationScreenDestination)) }, + onAccountDetailsClick = { navigator.navigate(NavigationCommand(MyAccountScreenDestination)) }, isUserInCall = viewModelSelf::isUserInCall, ) @@ -193,6 +204,7 @@ private fun SelfUserProfileContent( onOtherAccountClick: (UserId) -> Unit = {}, onQrCodeClick: () -> Unit = {}, onCreateAccount: () -> Unit = {}, + onAccountDetailsClick: () -> Unit = {}, isUserInCall: () -> Boolean ) { val snackbarHostState = LocalSnackbarHostState.current @@ -212,7 +224,11 @@ private fun SelfUserProfileContent( SelfUserProfileTopBar( onCloseClick = onCloseClick, onLogoutClick = remember { - { logoutOptionsDialogState.show(logoutOptionsDialogState.savedState ?: LogoutOptionsDialogState()) } + { + logoutOptionsDialogState.show( + logoutOptionsDialogState.savedState ?: LogoutOptionsDialogState() + ) + } } ) } @@ -284,12 +300,19 @@ private fun SelfUserProfileContent( stickyHeader { CurrentSelfUserStatus( userStatus = status, - onStatusClicked = onStatusClicked + onStatusClicked = onStatusClicked, ) } } + stickyHeader { + VerticalSpace.x8() + Box(modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.spacing16x)) { + AccountDetailButton(onAccountDetailsClick = onAccountDetailsClick) + } + } if (state.otherAccounts.isNotEmpty()) { stickyHeader { + VerticalSpace.x16() OtherAccountsHeader() } items( @@ -298,24 +321,33 @@ private fun SelfUserProfileContent( OtherAccountItem( account = account, clickable = remember { - Clickable(enabled = true, onClickDescription = selectLabel, onClick = { - if (isUserInCall()) { - Toast.makeText( - context, - context.getString(R.string.cant_switch_account_in_call), - Toast.LENGTH_SHORT - ).show() - } else { - onOtherAccountClick(account.id) + Clickable( + enabled = true, + onClickDescription = selectLabel, + onClick = { + if (isUserInCall()) { + Toast.makeText( + context, + context.getString(R.string.cant_switch_account_in_call), + Toast.LENGTH_SHORT + ).show() + } else { + onOtherAccountClick(account.id) + } } - }) + ) } ) } ) } } - NewTeamButton(onAddAccountClick, isUserInCall, context) + + Divider(color = MaterialTheme.wireColorScheme.outline) + + Box(modifier = Modifier.padding(dimensions().spacing16x)) { + NewTeamButton(onAddAccountClick, isUserInCall, context) + } } ChangeStatusDialogContent( data = statusDialogData, @@ -362,7 +394,10 @@ private fun SelfUserProfileTopBar( minSize = MaterialTheme.wireDimensions.buttonSmallMinSize, minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, state = WireButtonState.Error, - clickBlockParams = ClickBlockParams(blockWhenSyncing = false, blockWhenConnecting = false), + clickBlockParams = ClickBlockParams( + blockWhenSyncing = false, + blockWhenConnecting = false + ), ) } ) @@ -371,7 +406,7 @@ private fun SelfUserProfileTopBar( @Composable private fun CurrentSelfUserStatus( userStatus: UserAvailabilityStatus, - onStatusClicked: (UserAvailabilityStatus) -> Unit + onStatusClicked: (UserAvailabilityStatus) -> Unit, ) { val items = listOf( UserAvailabilityStatus.AVAILABLE, @@ -394,11 +429,7 @@ private fun CurrentSelfUserStatus( }, defaultItemIndex = items.indexOf(userStatus), label = null, - modifier = Modifier.padding( - bottom = MaterialTheme.wireDimensions.spacing16x, - start = MaterialTheme.wireDimensions.spacing16x, - end = MaterialTheme.wireDimensions.spacing16x - ), + modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.spacing16x), autoUpdateSelection = false, showDefaultTextIndicator = false, leadingCompose = { index -> UserStatusIndicator(items[index]) }, @@ -409,75 +440,52 @@ private fun CurrentSelfUserStatus( } } -@Composable -private fun OtherAccountsHeader() { - FolderHeader(stringResource(id = R.string.user_profile_other_accs)) -} - @Composable private fun NewTeamButton( onAddAccountClick: () -> Unit, isUserIdCall: () -> Boolean, context: Context ) { - Surface(shadowElevation = dimensions().spacing8x) { - WirePrimaryButton( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .padding(dimensions().spacing16x) - .testTag("New Team or Account"), - text = stringResource(R.string.user_profile_new_account_text), - onClickDescription = stringResource(R.string.content_description_self_profile_new_account_btn), - onClick = remember { - { - if (isUserIdCall()) { - Toast.makeText( - context, - context.getString(R.string.cant_switch_account_in_call), - Toast.LENGTH_SHORT - ).show() - } else { - onAddAccountClick() - } + WirePrimaryButton( + modifier = Modifier + .testTag("New Team or Account"), + text = stringResource(R.string.user_profile_new_account_text), + onClickDescription = stringResource(R.string.content_description_self_profile_new_account_btn), + onClick = remember { + { + if (isUserIdCall()) { + Toast.makeText( + context, + context.getString(R.string.cant_switch_account_in_call), + Toast.LENGTH_SHORT + ).show() + } else { + onAddAccountClick() } } - ) - } + } + ) } @Composable -private fun OtherAccountItem( - account: OtherAccount, - clickable: Clickable = Clickable(enabled = true) {} +private fun AccountDetailButton( + onAccountDetailsClick: () -> Unit, ) { - RowItemTemplate( - leadingIcon = { UserProfileAvatar(account.avatarData) }, - titleStartPadding = dimensions().spacing0x, - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - HighlightName( - name = account.fullName, - modifier = Modifier.weight(weight = 1f, fill = false), - searchQuery = "" - ) - } - }, - subtitle = { - if (account.teamName != null) { - HighlightSubtitle(subTitle = account.teamName, prefix = "") - } - }, - actions = { - Box( + WireSecondaryButton( + modifier = Modifier + .testTag("Account details"), + text = stringResource(R.string.settings_your_account_label), + trailingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_right), + contentDescription = "", + tint = MaterialTheme.wireColorScheme.onSecondaryButtonEnabled, modifier = Modifier - .wrapContentWidth() - .padding(end = MaterialTheme.wireDimensions.spacing8x) - ) { - ArrowRightIcon(Modifier.align(Alignment.TopEnd), R.string.content_description_empty) - } + .defaultMinSize(dimensions().wireIconButtonSize) + .padding(end = dimensions().spacing8x) + ) }, - clickable = clickable, - modifier = Modifier.padding(start = dimensions().spacing8x) + onClick = onAccountDetailsClick, ) } @@ -486,7 +494,11 @@ private fun LoggingOutDialog(isLoggingOut: Boolean) { if (isLoggingOut) { ProgressDialog( title = stringResource(R.string.user_profile_logging_out_progress), - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false, usePlatformDefaultWidth = true) + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + usePlatformDefaultWidth = true + ) ) } } @@ -503,13 +515,21 @@ fun PreviewSelfUserProfileScreen() { userName = "userName_long_long_long_long_long_long_long_long_long_long", teamName = "Best team ever long long long long long long long long long ", otherAccounts = listOf( - OtherAccount(id = UserId("id1", "domain"), fullName = "Other Name", teamName = "team A"), - OtherAccount(id = UserId("id2", "domain"), fullName = "New Name") + OtherAccount( + id = UserId("id1", "domain"), + fullName = "Other Name", + handle = "userName", + ), + OtherAccount( + id = UserId("id2", "domain"), + fullName = "New Name", + handle = "userName", + ) ), statusDialogData = null, legalHoldStatus = LegalHoldUIState.Active, ), - isUserInCall = { false } + isUserInCall = { false }, ) } } @@ -526,8 +546,16 @@ fun PersonalSelfUserProfileScreenPreview() { userName = "some-user", teamName = null, otherAccounts = listOf( - OtherAccount(id = UserId("id1", "domain"), fullName = "Other Name", teamName = "team A"), - OtherAccount(id = UserId("id2", "domain"), fullName = "New Name") + OtherAccount( + id = UserId("id1", "domain"), + fullName = "Other Name", + handle = "userName", + ), + OtherAccount( + id = UserId("id2", "domain"), + fullName = "New Name", + handle = "userName", + ) ), statusDialogData = null, legalHoldStatus = LegalHoldUIState.Active, @@ -541,7 +569,10 @@ fun PersonalSelfUserProfileScreenPreview() { @Composable fun PreviewCurrentSelfUserStatus() { WireTheme { - CurrentSelfUserStatus(UserAvailabilityStatus.AVAILABLE, onStatusClicked = {}) + CurrentSelfUserStatus( + UserAvailabilityStatus.AVAILABLE, + onStatusClicked = {}, + ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 36439af8e3c..7ea1e925b6b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -161,7 +161,7 @@ class SelfUserProfileViewModel @Inject constructor( Pair( selfUser, list.filter { it.first.id != selfUser.id } - .map { (selfUser, team) -> otherAccountMapper.toOtherAccount(selfUser, team) } + .map { (selfUser, _) -> otherAccountMapper.toOtherAccount(selfUser) } ) } .distinctUntilChanged() diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/model/OtherAccount.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/model/OtherAccount.kt index d9d273f62c9..26d22260595 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/model/OtherAccount.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/model/OtherAccount.kt @@ -26,5 +26,5 @@ data class OtherAccount( val id: UserId, val fullName: String, val avatarData: UserAvatarData = UserAvatarData(), - val teamName: String? = null + val handle: String?, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 95fafc74212..9309f671a99 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -594,14 +594,14 @@ User Profile Close User profile - Log out + Logout Your Other Accounts Availability Available Busy Away None - New Team or Account + New Team or Add Account Details Devices Group diff --git a/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt index 2f619df0c9b..3f02c87dacb 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/OtherAccountMapperTest.kt @@ -23,11 +23,9 @@ import com.wire.android.ui.userprofile.self.model.OtherAccount import com.wire.kalium.logic.data.team.Team import com.wire.kalium.logic.data.user.SelfUser import io.mockk.MockKAnnotations -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test -@OptIn(ExperimentalCoroutinesApi::class) class OtherAccountMapperTest { @Test @@ -39,23 +37,22 @@ class OtherAccountMapperTest { testSelfUser(1) to null ) // When - val results = data.map { (selfUser, team) -> mapper.toOtherAccount(selfUser, team) } + val results = data.map { (selfUser, _) -> mapper.toOtherAccount(selfUser) } // Then results.forEachIndexed { index, result -> - val (selfUser, team) = data[index] - assert(compareResult(selfUser, team, result)) + val (selfUser, _) = data[index] + assert(compareResult(selfUser, result)) } } private fun compareResult( selfUser: SelfUser, - team: Team?, otherAccount: OtherAccount ): Boolean = selfUser.id == otherAccount.id && selfUser.name == otherAccount.fullName && selfUser.avatar(selfUser.connectionStatus) == otherAccount.avatarData - && team?.name == otherAccount.teamName + && selfUser.handle == otherAccount.handle private class Arrangement {