From 612d7e9d638917094c277c5cf40fb86c48935081 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 17 Jun 2026 14:10:22 +0200 Subject: [PATCH 01/18] feat: handle user status change set from new ui set status --- .../android/di/accountScoped/UserModule.kt | 5 + .../di/metro/WireMetroViewModelBindings.kt | 7 + .../home/settings/SettingsViewModelFactory.kt | 3 + .../home/settings/SettingsViewModelGraph.kt | 5 + .../ui/userprofile/common/UserProfileInfo.kt | 15 + .../userprofile/self/SelfUserProfileScreen.kt | 98 ++---- .../userprofile/self/SelfUserProfileState.kt | 1 + .../self/SelfUserProfileViewModel.kt | 1 + .../self/status/SelfUserStatusScreen.kt | 294 ++++++++++++++++++ .../self/status/SelfUserStatusState.kt | 34 ++ .../self/status/SelfUserStatusViewModel.kt | 183 +++++++++++ app/src/main/res/values/strings.xml | 6 + .../status/SelfUserStatusViewModelTest.kt | 49 +++ kalium | 2 +- 14 files changed, 637 insertions(+), 66 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt index e1c6cac7b41..b992521e48d 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt @@ -54,6 +54,7 @@ import com.wire.kalium.logic.feature.user.UpdateAccentColorUseCase import com.wire.kalium.logic.feature.user.UpdateDisplayNameUseCase import com.wire.kalium.logic.feature.user.UpdateEmailUseCase import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase +import com.wire.kalium.logic.feature.user.UpdateSelfTextStatusUseCase import com.wire.kalium.logic.feature.user.UploadUserAvatarUseCase import com.wire.kalium.logic.feature.user.UserScope import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCase @@ -170,6 +171,10 @@ class UserModule { fun provideUpdateSelfAvailabilityStatusUseCase(userScope: UserScope): UpdateSelfAvailabilityStatusUseCase = userScope.updateSelfAvailabilityStatus + @Provides + fun provideUpdateSelfTextStatusUseCase(userScope: UserScope): UpdateSelfTextStatusUseCase = + userScope.updateSelfTextStatus + @Provides fun provideGetAllContactsUseCase( userScope: UserScope diff --git a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt index 9caf91f352a..2867fdbfd4f 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt @@ -162,6 +162,7 @@ import com.wire.android.ui.userprofile.avatarpicker.AvatarPickerViewModel import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModel import com.wire.android.ui.userprofile.qr.SelfQRCodeViewModel import com.wire.android.ui.userprofile.self.SelfUserProfileViewModel +import com.wire.android.ui.userprofile.self.status.SelfUserStatusViewModel import com.wire.android.ui.userprofile.service.ServiceDetailsViewModelImpl import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel import com.wire.kalium.logic.data.id.ConversationId @@ -922,6 +923,12 @@ object WireMetroViewModelBindings { fun selfUserProfileViewModel(factory: SettingsViewModelFactory): ViewModel = factory.selfUserProfileViewModel() + @Provides + @IntoMap + @ViewModelKey(SelfUserStatusViewModel::class) + fun selfUserStatusViewModel(factory: SettingsViewModelFactory): ViewModel = + factory.selfUserStatusViewModel() + @Provides @IntoMap @ViewModelKey(TeamMigrationViewModel::class) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt index 3ab2d6d90f6..d7d7292bd11 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt @@ -45,6 +45,7 @@ import com.wire.android.ui.userprofile.avatarpicker.AvatarPickerViewModel import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModel import com.wire.android.ui.userprofile.qr.SelfQRCodeViewModel import com.wire.android.ui.userprofile.self.SelfUserProfileViewModel +import com.wire.android.ui.userprofile.self.status.SelfUserStatusViewModel import com.wire.android.ui.userprofile.service.ServiceDetailsViewModelImpl import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel import com.wire.android.util.dispatchers.DispatcherProvider @@ -101,6 +102,7 @@ class SettingsViewModelFactory @Inject constructor( private val selfDevicesViewModel: Provider, private val avatarPickerViewModel: Provider, private val selfUserProfileViewModel: Provider, + private val selfUserStatusViewModel: Provider, private val teamMigrationViewModel: Provider, private val updateEmail: Provider, private val getSelfUser: Provider, @@ -155,6 +157,7 @@ class SettingsViewModelFactory @Inject constructor( fun selfDevicesViewModel(): SelfDevicesViewModel = selfDevicesViewModel.invoke() fun avatarPickerViewModel(): AvatarPickerViewModel = avatarPickerViewModel.invoke() fun selfUserProfileViewModel(): SelfUserProfileViewModel = selfUserProfileViewModel.invoke() + fun selfUserStatusViewModel(): SelfUserStatusViewModel = selfUserStatusViewModel.invoke() fun teamMigrationViewModel(): TeamMigrationViewModel = teamMigrationViewModel.invoke() fun verifyEmailViewModel(savedStateHandle: SavedStateHandle): VerifyEmailViewModel = VerifyEmailViewModel( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelGraph.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelGraph.kt index 198d93ebb6a..dfd3cb9537e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelGraph.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelGraph.kt @@ -46,6 +46,7 @@ import com.wire.android.ui.userprofile.avatarpicker.AvatarPickerViewModel import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModel import com.wire.android.ui.userprofile.qr.SelfQRCodeViewModel import com.wire.android.ui.userprofile.self.SelfUserProfileViewModel +import com.wire.android.ui.userprofile.self.status.SelfUserStatusViewModel import com.wire.android.ui.userprofile.service.ServiceDetailsViewModel import com.wire.android.ui.userprofile.service.ServiceDetailsViewModelImpl import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel @@ -153,6 +154,10 @@ fun avatarPickerViewModel(): AvatarPickerViewModel = fun selfUserProfileViewModel(): SelfUserProfileViewModel = settingsViewModel() +@Composable +fun selfUserStatusViewModel(): SelfUserStatusViewModel = + settingsViewModel() + @Composable fun selfQRCodeViewModel(): SelfQRCodeViewModel = settingsSavedStateViewModel() diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index b72ea84c595..aab93233aa5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -119,6 +119,7 @@ fun UserProfileInfo( onQrCodeClick: (() -> Unit)? = null, accentId: Int = -1, showQrCode: Boolean = true, + textStatus: String? = null, ) { Column( horizontalAlignment = CenterHorizontally, @@ -290,6 +291,20 @@ fun UserProfileInfo( ) } + if (teamName != null && !textStatus.isNullOrBlank()) { + Text( + modifier = Modifier + .padding(vertical = dimensions().spacing8x) + .padding(horizontal = dimensions().spacing16x), + text = textStatus, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.colorScheme.onBackground + ) + } + userId?.also { SecurityClassificationBannerForUser( userId = it, 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 7e53eee3718..d82c648b8ed 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 @@ -62,8 +62,6 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.ramcosta.composedestinations.generated.app.navgraphs.PersonalToTeamMigrationGraph 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 @@ -81,8 +79,8 @@ import com.ramcosta.composedestinations.generated.app.destinations.AvatarPickerS import com.ramcosta.composedestinations.generated.app.destinations.MyAccountScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.SelfQRCodeScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.SelfUserStatusScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination -import com.wire.android.ui.common.rowitem.SectionHeader import com.wire.android.ui.legalhold.banner.LegalHoldPendingBanner import com.wire.android.ui.legalhold.banner.LegalHoldSubjectBanner import com.wire.android.ui.legalhold.banner.LegalHoldUIState @@ -98,7 +96,6 @@ import com.wire.android.ui.userprofile.common.EditableState import com.wire.android.ui.userprofile.common.UserProfileInfo import com.wire.android.ui.userprofile.self.SelfUserProfileViewModel.ErrorCodes import com.wire.android.ui.userprofile.self.SelfUserProfileViewModel.ErrorCodes.DownloadUserInfoError -import com.wire.android.ui.userprofile.self.dialog.ChangeStatusDialogContent import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialog import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialogState import com.wire.android.ui.userprofile.self.model.OtherAccount @@ -145,14 +142,13 @@ fun SelfUserProfileScreen( ) }, onEditClick = { navigator.navigate(NavigationCommand(AppSettingsScreenDestination)) }, - onStatusClicked = viewModelSelf::changeStatusClick, + onUpdateStatusClick = { + navigator.navigate(NavigationCommand(SelfUserStatusScreenDestination)) + }, onAddAccountClick = { val destination = if (loginTypeSelector.canUseNewLogin()) NewLoginScreenDestination() else WelcomeScreenDestination() navigator.navigate(NavigationCommand(destination)) }, - dismissStatusDialog = viewModelSelf::dismissStatusDialog, - onStatusChange = viewModelSelf::changeStatus, - onNotShowRationaleAgainChange = viewModelSelf::dialogCheckBoxStateChanged, onMessageShown = viewModelSelf::clearErrorMessage, onLegalHoldAcceptClick = legalHoldRequestedViewModel::show, onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, @@ -213,11 +209,8 @@ private fun SelfUserProfileContent( logout: (Boolean) -> Unit = {}, onChangeUserProfilePicture: () -> Unit = {}, onEditClick: () -> Unit = {}, - onStatusClicked: (UserAvailabilityStatus) -> Unit = {}, + onUpdateStatusClick: () -> Unit = {}, onAddAccountClick: () -> Unit = {}, - dismissStatusDialog: () -> Unit = {}, - onStatusChange: (UserAvailabilityStatus) -> Unit = {}, - onNotShowRationaleAgainChange: (Boolean) -> Unit = {}, onMessageShown: () -> Unit = {}, onLegalHoldAcceptClick: () -> Unit = {}, onLegalHoldLearnMoreClick: () -> Unit = {}, @@ -288,6 +281,7 @@ private fun SelfUserProfileContent( fullName = fullName, userName = userName, teamName = teamName, + textStatus = textStatus, onUserProfileClick = onChangeUserProfilePicture, editableState = EditableState.IsEditable(onEditClick), onQrCodeClick = onQrCodeClick, @@ -311,10 +305,9 @@ private fun SelfUserProfileContent( } } if (!state.teamName.isNullOrBlank()) { - CurrentSelfUserStatus( - userStatus = status, - onStatusClicked = onStatusClicked, - ) + Box(modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.spacing16x)) { + UpdateStatusButton(onUpdateStatusClick = onUpdateStatusClick) + } } VerticalSpace.x8() Box(modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.spacing16x)) { @@ -360,13 +353,6 @@ private fun SelfUserProfileContent( NewTeamButton(onAddAccountClick, isUserInCall, context) } } - ChangeStatusDialogContent( - data = statusDialogData, - dismiss = dismissStatusDialog, - onStatusChange = onStatusChange, - onNotShowRationaleAgainChange = onNotShowRationaleAgainChange - ) - LogoutOptionsDialog( dialogState = logoutOptionsDialogState, logout = logout @@ -414,43 +400,6 @@ private fun SelfUserProfileTopBar( ) } -@Composable -private fun CurrentSelfUserStatus( - userStatus: UserAvailabilityStatus, - onStatusClicked: (UserAvailabilityStatus) -> Unit, -) { - val items = listOf( - UserAvailabilityStatus.AVAILABLE, - UserAvailabilityStatus.BUSY, - UserAvailabilityStatus.AWAY, - UserAvailabilityStatus.NONE - ) - - Column(modifier = Modifier.fillMaxWidth()) { - SectionHeader(stringResource(R.string.user_profile_status_availability)) - - WireDropDown( - items = items.map { - when (it) { - UserAvailabilityStatus.AVAILABLE -> stringResource(UICommonR.string.user_profile_status_available) - UserAvailabilityStatus.BUSY -> stringResource(UICommonR.string.user_profile_status_busy) - UserAvailabilityStatus.AWAY -> stringResource(UICommonR.string.user_profile_status_away) - UserAvailabilityStatus.NONE -> stringResource(UICommonR.string.user_profile_status_none) - } - }, - defaultItemIndex = items.indexOf(userStatus), - label = null, - modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.spacing16x), - autoUpdateSelection = false, - showDefaultTextIndicator = false, - leadingCompose = { index -> UserStatusIndicator(items[index]) }, - onChangeClickDescription = stringResource(R.string.content_description_self_profile_change_status) - ) { selectedIndex -> - onStatusClicked(items[selectedIndex]) - } - } -} - @Composable private fun ManageTeamButton( onManageTeamClick: () -> Unit @@ -489,6 +438,29 @@ private fun NewTeamButton( ) } +@Composable +private fun UpdateStatusButton( + onUpdateStatusClick: () -> Unit, +) { + WireSecondaryButton( + modifier = Modifier + .testTag("Update Status"), + text = stringResource(R.string.user_profile_update_status), + trailingIcon = { + Icon( + painter = painterResource(id = UICommonR.drawable.ic_arrow_right), + contentDescription = "", + tint = MaterialTheme.wireColorScheme.onSecondaryButtonEnabled, + modifier = Modifier + .defaultMinSize(dimensions().wireIconButtonSize) + .padding(end = dimensions().spacing8x) + ) + }, + onClickDescription = stringResource(R.string.content_description_self_profile_update_status), + onClick = onUpdateStatusClick, + ) +} + @Composable private fun AccountDetailButton( onAccountDetailsClick: () -> Unit, @@ -562,7 +534,6 @@ fun PersonalSelfUserProfileScreenPreview() { SelfUserProfileContent( SelfUserProfileState( userId = UserId("value", "domain"), - status = UserAvailabilityStatus.BUSY, fullName = "Some User", userName = "some-user", teamName = null, @@ -591,10 +562,7 @@ fun PersonalSelfUserProfileScreenPreview() { @Composable fun PreviewCurrentSelfUserStatus() { WireTheme { - CurrentSelfUserStatus( - UserAvailabilityStatus.AVAILABLE, - onStatusClicked = {}, - ) + UpdateStatusButton(onUpdateStatusClick = {}) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt index f25e5c3b8e7..eac46bfbf38 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt @@ -31,6 +31,7 @@ data class SelfUserProfileState( val avatarAsset: UserAvatarAsset? = null, val errorMessageCode: ErrorCodes? = null, val status: UserAvailabilityStatus = UserAvailabilityStatus.NONE, + val textStatus: String? = null, val fullName: String = "", val userName: String = "", val teamName: String? = "", // maybe teamId is better here 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 eb358315882..23a2b58cdff 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 @@ -172,6 +172,7 @@ class SelfUserProfileViewModel @Inject constructor( // Update user data state userProfileState = userProfileState.copy( status = availabilityStatus, + textStatus = textStatus, fullName = name.orEmpty(), userName = handle.orEmpty(), teamName = selfTeam?.name, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt new file mode 100644 index 00000000000..41cce9c1b6f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -0,0 +1,294 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.userprofile.self.status + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import com.wire.android.R +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.annotation.app.WireRootDestination +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.WireDropDown +import com.wire.android.ui.common.avatar.UserStatusIndicator +import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState +import com.wire.android.ui.common.bottomsheet.show +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.rowitem.SectionHeader +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.spacers.VerticalSpace +import com.wire.android.ui.common.textfield.WireTextField +import com.wire.android.ui.common.textfield.WireTextFieldState +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.emoji.EmojiPickerBottomSheet +import com.wire.android.ui.home.settings.selfUserStatusViewModel +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.theme.wireTypography +import com.wire.android.ui.userprofile.self.dialog.ChangeStatusDialogContent +import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import com.wire.android.ui.common.R as UICommonR + +@WireRootDestination( + style = PopUpNavigationAnimation::class, +) +@Composable +@SuppressLint("ComposeModifierMissing") +fun SelfUserStatusScreen( + navigator: Navigator, + viewModel: SelfUserStatusViewModel = selfUserStatusViewModel(), +) { + SelfUserStatusContent( + state = viewModel.state, + onBackClick = navigator::navigateBack, + onAvailabilityStatusClicked = viewModel::changeAvailabilityStatusClick, + onAvailabilityStatusChange = viewModel::changeAvailabilityStatus, + onDismissStatusDialog = viewModel::dismissStatusDialog, + onNotShowRationaleAgainChange = viewModel::dialogCheckBoxStateChanged, + onEmojiSelected = viewModel::onEmojiSelected, + onUpdateCustomStatus = viewModel::updateCustomStatus, + ) +} + +@Composable +private fun SelfUserStatusContent( + state: SelfUserStatusState, + onBackClick: () -> Unit, + onAvailabilityStatusClicked: (UserAvailabilityStatus) -> Unit, + onAvailabilityStatusChange: (UserAvailabilityStatus) -> Unit, + onDismissStatusDialog: () -> Unit, + onNotShowRationaleAgainChange: (Boolean) -> Unit, + onEmojiSelected: (String) -> Unit, + onUpdateCustomStatus: (String) -> Unit, +) { + val textState = rememberTextFieldState(state.message) + val emojiPickerState = rememberWireModalSheetState(skipPartiallyExpanded = false) + + LaunchedEffect(state.message) { + if (textState.text.toString() != state.message) { + textState.setTextAndPlaceCursorAtEnd(state.message) + } + } + + WireScaffold( + topBar = { + WireCenterAlignedTopAppBar( + onNavigationPressed = onBackClick, + title = stringResource(R.string.user_profile_status_title), + navigationIconType = NavigationIconType.Back(), + elevation = 0.dp, + ) + } + ) { internalPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(internalPadding) + ) { + AvailabilitySection( + userStatus = state.availabilityStatus, + onStatusClicked = onAvailabilityStatusClicked, + ) + SelectedAvailabilityDescription(state.availabilityStatus) + VerticalSpace.x16() + if (state.isTeamMember) { + CustomStatusSection( + emoji = state.emoji, + textState = textState, + messageLength = textState.text.length, + onEmojiClick = { emojiPickerState.show() }, + onUpdateCustomStatus = { onUpdateCustomStatus(textState.text.toString()) }, + isSaving = state.isSaving, + ) + } + } + + ChangeStatusDialogContent( + data = state.statusDialogData, + dismiss = onDismissStatusDialog, + onStatusChange = onAvailabilityStatusChange, + onNotShowRationaleAgainChange = onNotShowRationaleAgainChange + ) + + EmojiPickerBottomSheet( + sheetState = emojiPickerState, + onEmojiSelected = { emoji, _ -> + emojiPickerState.hide() + onEmojiSelected(emoji) + } + ) + } +} + +@SuppressLint("ComposeModifierMissing") +@Composable +fun SelectedAvailabilityDescription(status: UserAvailabilityStatus) { + val description = when (status) { + UserAvailabilityStatus.NONE -> R.string.user_profile_change_status_dialog_none_text + UserAvailabilityStatus.AVAILABLE -> R.string.user_profile_change_status_dialog_available_text + UserAvailabilityStatus.BUSY -> R.string.user_profile_change_status_dialog_busy_text + UserAvailabilityStatus.AWAY -> R.string.user_profile_change_status_dialog_away_text + } + Text( + text = stringResource(description), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x), + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.secondaryText, + ) +} + +@Composable +private fun AvailabilitySection( + userStatus: UserAvailabilityStatus, + onStatusClicked: (UserAvailabilityStatus) -> Unit, +) { + val items = listOf( + UserAvailabilityStatus.AVAILABLE, + UserAvailabilityStatus.BUSY, + UserAvailabilityStatus.AWAY, + UserAvailabilityStatus.NONE + ) + + Column(modifier = Modifier.fillMaxWidth()) { + SectionHeader(stringResource(R.string.user_profile_status_availability)) + WireDropDown( + items = items.map { + when (it) { + UserAvailabilityStatus.AVAILABLE -> stringResource(UICommonR.string.user_profile_status_available) + UserAvailabilityStatus.BUSY -> stringResource(UICommonR.string.user_profile_status_busy) + UserAvailabilityStatus.AWAY -> stringResource(UICommonR.string.user_profile_status_away) + UserAvailabilityStatus.NONE -> stringResource(UICommonR.string.user_profile_status_none) + } + }, + defaultItemIndex = items.indexOf(userStatus), + label = null, + modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.spacing16x), + autoUpdateSelection = false, + showDefaultTextIndicator = false, + leadingCompose = { index -> UserStatusIndicator(items[index]) }, + onChangeClickDescription = stringResource(R.string.content_description_self_profile_change_status) + ) { selectedIndex -> + onStatusClicked(items[selectedIndex]) + } + } +} + +@Composable +private fun CustomStatusSection( + emoji: String, + textState: TextFieldState, + messageLength: Int, + onEmojiClick: () -> Unit, + onUpdateCustomStatus: () -> Unit, + isSaving: Boolean, +) { + Column(modifier = Modifier.fillMaxWidth()) { + SectionHeader(stringResource(R.string.user_profile_custom_status_header)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + verticalAlignment = Alignment.CenterVertically, + ) { + WireSecondaryButton( + text = emoji, + onClick = onEmojiClick, + fillMaxWidth = false, + minSize = MaterialTheme.wireDimensions.buttonSmallMinSize, + minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, + ) + WireTextField( + textState = textState, + modifier = Modifier.weight(1f), + placeholderText = stringResource(R.string.user_profile_custom_status_placeholder), + leadingIcon = null, + state = WireTextFieldState.Default, + inputTransformation = InputTransformation.maxLength(MAX_STATUS_TEXT_LENGTH), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + ) + } + Text( + text = stringResource(R.string.user_profile_custom_status_counter, messageLength, MAX_STATUS_TEXT_LENGTH), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x), + style = MaterialTheme.wireTypography.body02, + color = MaterialTheme.wireColorScheme.secondaryText, + ) + WirePrimaryButton( + modifier = Modifier.padding(horizontal = dimensions().spacing16x), + text = stringResource(R.string.user_profile_update_status), + loading = isSaving, + onClick = onUpdateCustomStatus, + ) + } +} + +@PreviewMultipleThemes +@Composable +private fun SelfUserStatusContentPreview() { + WireTheme { + SelfUserStatusContent( + state = SelfUserStatusState( + availabilityStatus = UserAvailabilityStatus.AVAILABLE, + emoji = "👋", + message = "Working from home", + isTeamMember = true, + ), + onBackClick = {}, + onAvailabilityStatusClicked = {}, + onAvailabilityStatusChange = {}, + onDismissStatusDialog = {}, + onNotShowRationaleAgainChange = {}, + onEmojiSelected = {}, + onUpdateCustomStatus = {}, + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt new file mode 100644 index 00000000000..80bd429bc1d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt @@ -0,0 +1,34 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.userprofile.self.status + +import com.wire.android.ui.userprofile.self.dialog.StatusDialogData +import com.wire.kalium.logic.data.user.UserAvailabilityStatus + +data class SelfUserStatusState( + val availabilityStatus: UserAvailabilityStatus = UserAvailabilityStatus.NONE, + val emoji: String = DEFAULT_STATUS_EMOJI, + val message: String = "", + val isTeamMember: Boolean = false, + val statusDialogData: StatusDialogData? = null, + val isSaving: Boolean = false, +) + +const val DEFAULT_STATUS_EMOJI = "\uD83D\uDCAC" +const val MAX_STATUS_TEXT_LENGTH = 50 diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt new file mode 100644 index 00000000000..264aa796095 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -0,0 +1,183 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.userprofile.self.status + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.CurrentAccount +import com.wire.android.ui.userprofile.self.dialog.StatusDialogData +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase +import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase +import com.wire.kalium.logic.feature.user.UpdateSelfTextStatusUseCase +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +class SelfUserStatusViewModel @Inject constructor( + @CurrentAccount selfUserId: UserId, + userDataStoreProvider: UserDataStoreProvider, + private val observeSelfUserWithTeam: ObserveSelfUserWithTeamUseCase, + private val updateAvailabilityStatus: UpdateSelfAvailabilityStatusUseCase, + private val updateTextStatus: UpdateSelfTextStatusUseCase, + private val dispatchers: DispatcherProvider, +) : ViewModel() { + private val dataStore = userDataStoreProvider.getOrCreate(selfUserId) + + var state by mutableStateOf(SelfUserStatusState()) + private set + + init { + observeSelfUser() + } + + fun onEmojiSelected(emoji: String) { + state = state.copy(emoji = emoji) + } + + fun dismissStatusDialog() { + state = state.copy(statusDialogData = null) + } + + fun dialogCheckBoxStateChanged(isChecked: Boolean) { + state = state.copy(statusDialogData = state.statusDialogData?.changeCheckBoxState(isChecked)) + } + + fun changeAvailabilityStatusClick(status: UserAvailabilityStatus) { + if (state.availabilityStatus == status) return + viewModelScope.launch { + if (shouldShowStatusRationaleDialog(status)) { + state = state.copy(statusDialogData = status.toDialogData()) + } else { + changeAvailabilityStatus(status) + } + } + } + + fun changeAvailabilityStatus(status: UserAvailabilityStatus) { + setNotShowStatusRationaleAgainIfNeeded(status) + viewModelScope.launch { updateAvailabilityStatus(status) } + dismissStatusDialog() + } + + fun updateCustomStatus(message: String) { + if (!state.isTeamMember) return + val trimmedMessage = message.trim().take(MAX_STATUS_TEXT_LENGTH) + val bundledStatus = if (trimmedMessage.isBlank()) null else "${state.emoji} $trimmedMessage" + viewModelScope.launch { + state = state.copy(isSaving = true) + updateTextStatus(bundledStatus) + state = state.copy(isSaving = false) + } + } + + private fun observeSelfUser() { + viewModelScope.launch { + observeSelfUserWithTeam() + .flowOn(dispatchers.io()) + .collect { (selfUser, team) -> + val parsedStatus = parseTextStatus(selfUser.textStatus) + state = state.copy( + availabilityStatus = selfUser.availabilityStatus, + emoji = parsedStatus.emoji, + message = parsedStatus.message, + isTeamMember = team != null, + ) + } + } + } + + private fun setNotShowStatusRationaleAgainIfNeeded(status: UserAvailabilityStatus) { + if (state.statusDialogData?.isCheckBoxChecked == true) { + viewModelScope.launch { dataStore.dontShowStatusRationaleAgain(status) } + } + } + + private suspend fun shouldShowStatusRationaleDialog(status: UserAvailabilityStatus): Boolean = + dataStore.shouldShowStatusRationaleFlow(status).first() + + private fun UserAvailabilityStatus.toDialogData(): StatusDialogData = when (this) { + UserAvailabilityStatus.AVAILABLE -> StatusDialogData.StateAvailable() + UserAvailabilityStatus.BUSY -> StatusDialogData.StateBusy() + UserAvailabilityStatus.AWAY -> StatusDialogData.StateAway() + UserAvailabilityStatus.NONE -> StatusDialogData.StateNone() + } +} + +data class ParsedTextStatus(val emoji: String, val message: String) + +fun parseTextStatus(textStatus: String?): ParsedTextStatus { + val status = textStatus.orEmpty().trim() + if (status.isBlank()) return ParsedTextStatus(DEFAULT_STATUS_EMOJI, "") + + val emojiEnd = emojiClusterEnd(status) + return if (emojiEnd > 0) { + ParsedTextStatus( + emoji = status.substring(0, emojiEnd), + message = status.substring(emojiEnd).trim().take(MAX_STATUS_TEXT_LENGTH) + ) + } else { + ParsedTextStatus(DEFAULT_STATUS_EMOJI, status.take(MAX_STATUS_TEXT_LENGTH)) + } +} + +private fun emojiClusterEnd(value: String): Int { + val firstCodePoint = value.codePointAt(0) + if (!isEmojiLike(firstCodePoint)) return 0 + + var index = Character.charCount(firstCodePoint) + while (index < value.length) { + val codePoint = value.codePointAt(index) + val codePointLength = Character.charCount(codePoint) + when { + codePoint == VARIATION_SELECTOR || isEmojiModifier(codePoint) -> index += codePointLength + codePoint == ZERO_WIDTH_JOINER && index + codePointLength < value.length -> { + val nextCodePoint = value.codePointAt(index + codePointLength) + if (!isEmojiLike(nextCodePoint)) return index + index += codePointLength + Character.charCount(nextCodePoint) + } + else -> return index + } + } + return index +} + +private fun isEmojiLike(codePoint: Int): Boolean = + Character.getType(codePoint) == Character.OTHER_SYMBOL.toInt() || + codePoint in MISC_SYMBOLS_AND_PICTOGRAPHS || + codePoint in EMOTICONS || + codePoint in TRANSPORT_AND_MAP || + codePoint in SUPPLEMENTAL_SYMBOLS_AND_PICTOGRAPHS + +private fun isEmojiModifier(codePoint: Int): Boolean = codePoint in EMOJI_MODIFIERS + +private const val ZERO_WIDTH_JOINER = 0x200D +private const val VARIATION_SELECTOR = 0xFE0F +private val EMOJI_MODIFIERS = 0x1F3FB..0x1F3FF +private val EMOTICONS = 0x1F600..0x1F64F +private val MISC_SYMBOLS_AND_PICTOGRAPHS = 0x1F300..0x1F5FF +private val TRANSPORT_AND_MAP = 0x1F680..0x1F6FF +private val SUPPLEMENTAL_SYMBOLS_AND_PICTOGRAPHS = 0x1F900..0x1F9FF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99b9479c8de..62fdeb05e4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -246,6 +246,7 @@ Username, %s Team name, %s change availability status + update status Create a new team or personal account or log in Manage team Go back to your profile overview @@ -713,6 +714,11 @@ Log out Your Other Accounts Availability + Status + Update Status + Custom Status + Enter custom status message + %1$d/%2$d New Team or Add Account Manage Team Details diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt new file mode 100644 index 00000000000..9d4adadcbaf --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.userprofile.self.status + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SelfUserStatusViewModelTest { + + @Test + fun givenTextStatusWithEmoji_whenParsing_thenSplitsEmojiAndMessage() { + val result = parseTextStatus("\uD83C\uDFDD\uFE0F On vacation") + + assertEquals("\uD83C\uDFDD\uFE0F", result.emoji) + assertEquals("On vacation", result.message) + } + + @Test + fun givenTextStatusWithoutEmoji_whenParsing_thenUsesDefaultEmojiAndFullText() { + val result = parseTextStatus("Working remotely") + + assertEquals(DEFAULT_STATUS_EMOJI, result.emoji) + assertEquals("Working remotely", result.message) + } + + @Test + fun givenBlankTextStatus_whenParsing_thenUsesEmptyMessage() { + val result = parseTextStatus(null) + + assertEquals(DEFAULT_STATUS_EMOJI, result.emoji) + assertEquals("", result.message) + } +} diff --git a/kalium b/kalium index 7fed478f72d..07e05ccc61e 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 7fed478f72d9f885211a0f088ab40c96cc5670ec +Subproject commit 07e05ccc61edc5c6bcdbad2e0bbb0eb3e1487da6 From 3c3b27f55f7810deb43e6993ab4ce9030e11af54 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 17 Jun 2026 15:24:39 +0200 Subject: [PATCH 02/18] feat: ui status with emoji and default behavior --- .../self/status/SelfUserStatusScreen.kt | 28 +++++++++--- .../self/status/SelfUserStatusState.kt | 20 ++++++++- .../self/status/SelfUserStatusViewModel.kt | 3 +- .../status/SelfUserStatusViewModelTest.kt | 43 +++++++++++++++++++ 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index 41cce9c1b6f..c7a7df6d997 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -31,12 +31,14 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.maxLength import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -57,6 +59,7 @@ import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.emoji.EmojiPickerBottomSheet @@ -68,6 +71,7 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.ui.userprofile.self.dialog.ChangeStatusDialogContent import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import kotlinx.coroutines.flow.distinctUntilChanged import com.wire.android.ui.common.R as UICommonR @WireRootDestination( @@ -111,6 +115,14 @@ private fun SelfUserStatusContent( } } + LaunchedEffect(textState, state.emoji) { + textState.textAsFlow().distinctUntilChanged().collect { text -> + if (text.isNotBlank() && state.emoji == null) { + onEmojiSelected(DEFAULT_STATUS_EMOJI) + } + } + } + WireScaffold( topBar = { WireCenterAlignedTopAppBar( @@ -134,7 +146,7 @@ private fun SelfUserStatusContent( VerticalSpace.x16() if (state.isTeamMember) { CustomStatusSection( - emoji = state.emoji, + emoji = resolveStatusEmoji(state.emoji, textState.text.toString()), textState = textState, messageLength = textState.text.length, onEmojiClick = { emojiPickerState.show() }, @@ -218,7 +230,7 @@ private fun AvailabilitySection( @Composable private fun CustomStatusSection( - emoji: String, + emoji: String?, textState: TextFieldState, messageLength: Int, onEmojiClick: () -> Unit, @@ -235,12 +247,18 @@ private fun CustomStatusSection( verticalAlignment = Alignment.CenterVertically, ) { WireSecondaryButton( - text = emoji, + leadingIcon = { + if (emoji == null) { + Icon(painter = painterResource(id = R.drawable.ic_emoticon), contentDescription = "") + } else { + Text(emoji) + } + }, onClick = onEmojiClick, fillMaxWidth = false, - minSize = MaterialTheme.wireDimensions.buttonSmallMinSize, minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, ) + WireTextField( textState = textState, modifier = Modifier.weight(1f), @@ -278,7 +296,7 @@ private fun SelfUserStatusContentPreview() { SelfUserStatusContent( state = SelfUserStatusState( availabilityStatus = UserAvailabilityStatus.AVAILABLE, - emoji = "👋", + emoji = DEFAULT_STATUS_EMOJI, message = "Working from home", isTeamMember = true, ), diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt index 80bd429bc1d..74b79308424 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt @@ -23,7 +23,7 @@ import com.wire.kalium.logic.data.user.UserAvailabilityStatus data class SelfUserStatusState( val availabilityStatus: UserAvailabilityStatus = UserAvailabilityStatus.NONE, - val emoji: String = DEFAULT_STATUS_EMOJI, + val emoji: String? = null, val message: String = "", val isTeamMember: Boolean = false, val statusDialogData: StatusDialogData? = null, @@ -32,3 +32,21 @@ data class SelfUserStatusState( const val DEFAULT_STATUS_EMOJI = "\uD83D\uDCAC" const val MAX_STATUS_TEXT_LENGTH = 50 + +fun resolveStatusEmoji(emoji: String?, message: String): String? = + if (!emoji.isNullOrBlank()) { + emoji + } else if (message.trim().isBlank()) { + null + } else { + DEFAULT_STATUS_EMOJI + } + +fun buildTextStatus(emoji: String?, message: String): String? { + val trimmedMessage = message.trim().take(MAX_STATUS_TEXT_LENGTH) + val trimmedEmoji = emoji?.takeIf { it.isNotBlank() } + if (trimmedMessage.isBlank()) return trimmedEmoji + + val resolvedEmoji = resolveStatusEmoji(trimmedEmoji, trimmedMessage) ?: return trimmedMessage + return "$resolvedEmoji $trimmedMessage" +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt index 264aa796095..faf7d7824f6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -85,8 +85,7 @@ class SelfUserStatusViewModel @Inject constructor( fun updateCustomStatus(message: String) { if (!state.isTeamMember) return - val trimmedMessage = message.trim().take(MAX_STATUS_TEXT_LENGTH) - val bundledStatus = if (trimmedMessage.isBlank()) null else "${state.emoji} $trimmedMessage" + val bundledStatus = buildTextStatus(state.emoji, message) viewModelScope.launch { state = state.copy(isSaving = true) updateTextStatus(bundledStatus) diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt index 9d4adadcbaf..2f19a1cf478 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt @@ -19,6 +19,7 @@ package com.wire.android.ui.userprofile.self.status import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test class SelfUserStatusViewModelTest { @@ -46,4 +47,46 @@ class SelfUserStatusViewModelTest { assertEquals(DEFAULT_STATUS_EMOJI, result.emoji) assertEquals("", result.message) } + + @Test + fun givenTextWithoutEmoji_whenResolvingEmoji_thenUsesDefaultEmoji() { + val result = resolveStatusEmoji(emoji = null, message = "Working remotely") + + assertEquals(DEFAULT_STATUS_EMOJI, result) + } + + @Test + fun givenSelectedEmoji_whenResolvingEmoji_thenKeepsSelectedEmoji() { + val result = resolveStatusEmoji(emoji = "\uD83C\uDFDD\uFE0F", message = "Working remotely") + + assertEquals("\uD83C\uDFDD\uFE0F", result) + } + + @Test + fun givenBlankMessage_whenResolvingEmoji_thenReturnsNull() { + val result = resolveStatusEmoji(emoji = null, message = " ") + + assertNull(result) + } + + @Test + fun givenTextWithoutEmoji_whenBuildingTextStatus_thenUsesDefaultEmoji() { + val result = buildTextStatus(emoji = null, message = "Working remotely") + + assertEquals("$DEFAULT_STATUS_EMOJI Working remotely", result) + } + + @Test + fun givenSelectedEmoji_whenBuildingTextStatus_thenKeepsSelectedEmoji() { + val result = buildTextStatus(emoji = "\uD83C\uDFDD\uFE0F", message = "Working remotely") + + assertEquals("\uD83C\uDFDD\uFE0F Working remotely", result) + } + + @Test + fun givenSelectedEmojiAndBlankMessage_whenBuildingTextStatus_thenReturnsOnlyEmoji() { + val result = buildTextStatus(emoji = "\uD83C\uDFDD\uFE0F", message = " ") + + assertEquals("\uD83C\uDFDD\uFE0F", result) + } } From 77be9223e28161aaa4f89ab91d31929406aa1200 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 17 Jun 2026 15:30:41 +0200 Subject: [PATCH 03/18] feat: add clear behavior --- .../self/status/SelfUserStatusScreen.kt | 41 +++++++++++++++++++ .../self/status/SelfUserStatusViewModel.kt | 10 +++++ .../status/SelfUserStatusViewModelTest.kt | 7 ++++ 3 files changed, 58 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index c7a7df6d997..027d54cf11a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -19,19 +19,26 @@ package com.wire.android.ui.userprofile.self.status import android.annotation.SuppressLint +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.maxLength import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -91,6 +98,7 @@ fun SelfUserStatusScreen( onDismissStatusDialog = viewModel::dismissStatusDialog, onNotShowRationaleAgainChange = viewModel::dialogCheckBoxStateChanged, onEmojiSelected = viewModel::onEmojiSelected, + onClearCustomStatus = viewModel::clearCustomStatus, onUpdateCustomStatus = viewModel::updateCustomStatus, ) } @@ -104,6 +112,7 @@ private fun SelfUserStatusContent( onDismissStatusDialog: () -> Unit, onNotShowRationaleAgainChange: (Boolean) -> Unit, onEmojiSelected: (String) -> Unit, + onClearCustomStatus: () -> Unit, onUpdateCustomStatus: (String) -> Unit, ) { val textState = rememberTextFieldState(state.message) @@ -150,6 +159,10 @@ private fun SelfUserStatusContent( textState = textState, messageLength = textState.text.length, onEmojiClick = { emojiPickerState.show() }, + onClearCustomStatus = { + textState.clearText() + onClearCustomStatus() + }, onUpdateCustomStatus = { onUpdateCustomStatus(textState.text.toString()) }, isSaving = state.isSaving, ) @@ -234,9 +247,12 @@ private fun CustomStatusSection( textState: TextFieldState, messageLength: Int, onEmojiClick: () -> Unit, + onClearCustomStatus: () -> Unit, onUpdateCustomStatus: () -> Unit, isSaving: Boolean, ) { + val hasStatus = emoji != null || textState.text.isNotBlank() + Column(modifier = Modifier.fillMaxWidth()) { SectionHeader(stringResource(R.string.user_profile_custom_status_header)) Row( @@ -265,6 +281,30 @@ private fun CustomStatusSection( placeholderText = stringResource(R.string.user_profile_custom_status_placeholder), leadingIcon = null, state = WireTextFieldState.Default, + trailingIcon = { + Box( + modifier = Modifier + .width(dimensions().spacing64x) + .height(dimensions().spacing40x), + contentAlignment = Alignment.CenterEnd + ) { + androidx.compose.animation.AnimatedVisibility( + visible = hasStatus, + enter = fadeIn(), + exit = fadeOut() + ) { + IconButton( + modifier = Modifier.padding(start = dimensions().spacing12x), + onClick = onClearCustomStatus, + ) { + Icon( + painter = painterResource(id = UICommonR.drawable.ic_clear_search), + contentDescription = stringResource(UICommonR.string.content_description_clear_content) + ) + } + } + } + }, inputTransformation = InputTransformation.maxLength(MAX_STATUS_TEXT_LENGTH), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Sentences, @@ -306,6 +346,7 @@ private fun SelfUserStatusContentPreview() { onDismissStatusDialog = {}, onNotShowRationaleAgainChange = {}, onEmojiSelected = {}, + onClearCustomStatus = {}, onUpdateCustomStatus = {}, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt index faf7d7824f6..9344c7ed2b6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -93,6 +93,16 @@ class SelfUserStatusViewModel @Inject constructor( } } + fun clearCustomStatus() { + if (!state.isTeamMember) return + state = state.copy(emoji = null, message = "") + viewModelScope.launch { + state = state.copy(isSaving = true) + updateTextStatus(null) + state = state.copy(isSaving = false) + } + } + private fun observeSelfUser() { viewModelScope.launch { observeSelfUserWithTeam() diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt index 2f19a1cf478..37160fa8fa5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt @@ -89,4 +89,11 @@ class SelfUserStatusViewModelTest { assertEquals("\uD83C\uDFDD\uFE0F", result) } + + @Test + fun givenNoEmojiAndBlankMessage_whenBuildingTextStatus_thenReturnsNull() { + val result = buildTextStatus(emoji = null, message = " ") + + assertNull(result) + } } From 4ac75b22343699cb854d91ece7682de4946b34ab Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 17 Jun 2026 15:34:15 +0200 Subject: [PATCH 04/18] feat: add clear behavior - default no emoji --- .../ui/userprofile/self/status/SelfUserStatusViewModel.kt | 4 ++-- .../ui/userprofile/self/status/SelfUserStatusViewModelTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt index 9344c7ed2b6..0b9d0e85332 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -136,11 +136,11 @@ class SelfUserStatusViewModel @Inject constructor( } } -data class ParsedTextStatus(val emoji: String, val message: String) +data class ParsedTextStatus(val emoji: String?, val message: String) fun parseTextStatus(textStatus: String?): ParsedTextStatus { val status = textStatus.orEmpty().trim() - if (status.isBlank()) return ParsedTextStatus(DEFAULT_STATUS_EMOJI, "") + if (status.isBlank()) return ParsedTextStatus(null, "") val emojiEnd = emojiClusterEnd(status) return if (emojiEnd > 0) { diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt index 37160fa8fa5..48c8fac4f18 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt @@ -44,7 +44,7 @@ class SelfUserStatusViewModelTest { fun givenBlankTextStatus_whenParsing_thenUsesEmptyMessage() { val result = parseTextStatus(null) - assertEquals(DEFAULT_STATUS_EMOJI, result.emoji) + assertNull(result.emoji) assertEquals("", result.message) } From 109376a6b744035f5e924383866dd942e31a70b6 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 17 Jun 2026 15:54:05 +0200 Subject: [PATCH 05/18] feat: padding adjustments when no status --- .../com/wire/android/ui/userprofile/common/UserProfileInfo.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index aab93233aa5..bb97ceff290 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -284,7 +284,7 @@ fun UserProfileInfo( val teamDescription = stringResource(R.string.content_description_self_profile_team, teamName) TeamInformation( modifier = Modifier - .padding(top = dimensions().spacing8x) + .padding(vertical = dimensions().spacing8x) .padding(horizontal = dimensions().spacing16x) .semantics(mergeDescendants = true) { contentDescription = teamDescription }, teamName = teamName From 7e8ab8d67048351e289546ced2b67a0a6aa552e9 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Wed, 17 Jun 2026 17:49:03 +0200 Subject: [PATCH 06/18] feat: add indicator to profile self screen --- .../com/wire/android/ui/userprofile/common/UserProfileInfo.kt | 4 +++- .../wire/android/ui/userprofile/self/SelfUserProfileScreen.kt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index bb97ceff290..970368a5868 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -120,6 +120,7 @@ fun UserProfileInfo( accentId: Int = -1, showQrCode: Boolean = true, textStatus: String? = null, + availabilityStatus: UserAvailabilityStatus = UserAvailabilityStatus.NONE, ) { Column( horizontalAlignment = CenterHorizontally, @@ -132,6 +133,7 @@ fun UserProfileInfo( Box(contentAlignment = Alignment.Center) { val userAvatarData = UserAvatarData( asset = avatarAsset, + availabilityStatus = availabilityStatus, connectionState = connection, membership = membership, nameBasedAvatar = NameBasedAvatar(fullName, accentId) @@ -168,7 +170,7 @@ fun UserProfileInfo( showPlaceholderIfNoAsset = showPlaceholderIfNoAsset, withCrossfadeAnimation = true, type = expiresAt?.let { UserProfileAvatarType.WithIndicators.TemporaryUser(expiresAt) } - ?: UserProfileAvatarType.WithoutIndicators, + ?: UserProfileAvatarType.WithIndicators.RegularUser(legalHoldIndicatorVisible = false), contentDescription = contentDescription ) } 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 d82c648b8ed..707ac144499 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 @@ -287,6 +287,7 @@ private fun SelfUserProfileContent( onQrCodeClick = onQrCodeClick, accentId = accentId, showQrCode = state.showQrCode, + availabilityStatus = status, ) if (state.legalHoldStatus != LegalHoldUIState.None) { Box( From 9bdabf2af0866cb66cabedf09483c1752175303a Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 09:36:54 +0200 Subject: [PATCH 07/18] feat: handle update event --- .../android/ui/userprofile/other/OtherUserProfileScreen.kt | 3 ++- .../ui/userprofile/other/OtherUserProfileScreenViewModel.kt | 1 + .../wire/android/ui/userprofile/other/OtherUserProfileState.kt | 1 + kalium | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index a978eebf42d..1b5dd0916eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -390,7 +390,8 @@ private fun TopBarCollapsing( isProteusVerified = targetState.isProteusVerified, isMLSVerified = targetState.isMLSVerified, expiresAt = targetState.expiresAt, - accentId = targetState.accentId + accentId = targetState.accentId, + textStatus = targetState.textStatus, ) if (state.isUnderLegalHold) { LegalHoldSubjectBanner( diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index db6b84886c2..988c0991935 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -228,6 +228,7 @@ class OtherUserProfileScreenViewModel @Inject constructor( fullName = otherUser.name.orEmpty(), userName = fromOtherUser(otherUser), teamName = userResult.team?.name.orEmpty(), + textStatus = otherUser.textStatus, email = otherUser.email.orEmpty(), phone = otherUser.phone.orEmpty(), connectionState = otherUser.connectionStatus, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt index 42fd7a36982..f218aee2c87 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt @@ -41,6 +41,7 @@ data class OtherUserProfileState( val fullName: String = "", val userName: String = "", val teamName: String = "", + val textStatus: String? = null, val email: String = "", val phone: String = "", val connectionState: ConnectionState = ConnectionState.NOT_CONNECTED, diff --git a/kalium b/kalium index 07e05ccc61e..9e46a646b27 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 07e05ccc61edc5c6bcdbad2e0bbb0eb3e1487da6 +Subproject commit 9e46a646b27c97f25ce26fed7c8ec2cee5c48e80 From 3c24dd138189d9b72036056b86189c06d810a33d Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 10:08:20 +0200 Subject: [PATCH 08/18] feat: handle update event, send space to delete --- .../android/ui/userprofile/self/status/SelfUserStatusState.kt | 2 +- .../ui/userprofile/self/status/SelfUserStatusViewModel.kt | 2 +- .../ui/userprofile/self/status/SelfUserStatusViewModelTest.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt index 74b79308424..2e7b8824439 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt @@ -45,7 +45,7 @@ fun resolveStatusEmoji(emoji: String?, message: String): String? = fun buildTextStatus(emoji: String?, message: String): String? { val trimmedMessage = message.trim().take(MAX_STATUS_TEXT_LENGTH) val trimmedEmoji = emoji?.takeIf { it.isNotBlank() } - if (trimmedMessage.isBlank()) return trimmedEmoji + if (trimmedMessage.isBlank()) return trimmedEmoji ?: " " val resolvedEmoji = resolveStatusEmoji(trimmedEmoji, trimmedMessage) ?: return trimmedMessage return "$resolvedEmoji $trimmedMessage" diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt index 0b9d0e85332..dbf4c753de8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -98,7 +98,7 @@ class SelfUserStatusViewModel @Inject constructor( state = state.copy(emoji = null, message = "") viewModelScope.launch { state = state.copy(isSaving = true) - updateTextStatus(null) + updateTextStatus(" ") state = state.copy(isSaving = false) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt index 48c8fac4f18..98343855820 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModelTest.kt @@ -91,9 +91,9 @@ class SelfUserStatusViewModelTest { } @Test - fun givenNoEmojiAndBlankMessage_whenBuildingTextStatus_thenReturnsNull() { + fun givenNoEmojiAndBlankMessage_whenBuildingTextStatus_thenReturnsSingleSpace() { val result = buildTextStatus(emoji = null, message = " ") - assertNull(result) + assertEquals(" ", result) } } From dc5202f319801b8013f992df063d5c1015d425a3 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 11:17:40 +0200 Subject: [PATCH 09/18] feat: handle mapping into conversation --- .../kotlin/com/wire/android/mapper/MessageMapper.kt | 1 + .../conversations/messages/item/MessageAuthorRow.kt | 10 ++++++++++ .../android/ui/home/conversations/model/UIMessage.kt | 3 ++- kalium | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt index 6ed92f5f91d..71a24aa2837 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt @@ -168,6 +168,7 @@ class MessageMapper @Inject constructor( clientId = (message as? Message.Sendable)?.senderClientId, accent = Accent.fromAccentId(sender?.accentId), guestExpiresAt = sender?.expiresAt, + textStatus = sender?.textStatus, senderId = when { (sender as? OtherUser)?.botService != null -> MessageSenderId.Bot(sender.botService!!) sender?.userType == UserTypeInfo.App -> MessageSenderId.App(sender.id) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageAuthorRow.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageAuthorRow.kt index e55396ae4c8..06099b785ba 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageAuthorRow.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageAuthorRow.kt @@ -59,6 +59,16 @@ fun MessageAuthorRow( messageStyle, modifier = Modifier.weight(weight = 1f, fill = false) ) + if (!textStatus.isNullOrBlank()) { + Text( + text = textStatus, + style = MaterialTheme.wireTypography.body02, + color = MaterialTheme.wireColorScheme.onBackground.copy(alpha = 0.6f), + modifier = Modifier.padding(start = dimensions().spacing4x), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } UserBadge( membership = membership, connectionState = connectionState, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index 0ec5d9b5265..8f618b15368 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -165,7 +165,8 @@ data class MessageHeader( val clientId: ClientId? = null, val accent: Accent = Accent.Unknown, val guestExpiresAt: Instant? = null, - val senderId: MessageSenderId? = null + val senderId: MessageSenderId? = null, + val textStatus: String? = null ) sealed interface MessageSenderId { diff --git a/kalium b/kalium index 9e46a646b27..060dbc65956 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 9e46a646b27c97f25ce26fed7c8ec2cee5c48e80 +Subproject commit 060dbc659569653ab987fac66cf935b88b9c56c1 From 1d55ce8dfd31a58c377af352447fe7c6ee4c42a4 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 11:58:50 +0200 Subject: [PATCH 10/18] feat: ui moving button to right place --- .../self/status/SelfUserStatusScreen.kt | 40 +++++++++++++------ .../self/status/SelfUserStatusViewModel.kt | 16 +++++++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index 027d54cf11a..2808fd479b0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -40,11 +40,13 @@ import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -63,6 +65,7 @@ import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.rowitem.SectionHeader 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.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState @@ -90,6 +93,15 @@ fun SelfUserStatusScreen( navigator: Navigator, viewModel: SelfUserStatusViewModel = selfUserStatusViewModel(), ) { + val snackbarHostState = LocalSnackbarHostState.current + val context = LocalContext.current + + LaunchedEffect(viewModel) { + viewModel.confirmationMessage.collect { messageResId -> + snackbarHostState.showSnackbar(context.getString(messageResId)) + } + } + SelfUserStatusContent( state = viewModel.state, onBackClick = navigator::navigateBack, @@ -140,6 +152,20 @@ private fun SelfUserStatusContent( navigationIconType = NavigationIconType.Back(), elevation = 0.dp, ) + }, + bottomBar = { + Surface( + shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation, + color = MaterialTheme.wireColorScheme.background, + modifier = Modifier.fillMaxWidth(), + ) { + WirePrimaryButton( + modifier = Modifier.padding(dimensions().spacing16x), + text = stringResource(R.string.user_profile_update_status), + loading = state.isSaving, + onClick = { onUpdateCustomStatus(textState.text.toString()) }, + ) + } } ) { internalPadding -> Column( @@ -162,9 +188,7 @@ private fun SelfUserStatusContent( onClearCustomStatus = { textState.clearText() onClearCustomStatus() - }, - onUpdateCustomStatus = { onUpdateCustomStatus(textState.text.toString()) }, - isSaving = state.isSaving, + } ) } } @@ -247,9 +271,7 @@ private fun CustomStatusSection( textState: TextFieldState, messageLength: Int, onEmojiClick: () -> Unit, - onClearCustomStatus: () -> Unit, - onUpdateCustomStatus: () -> Unit, - isSaving: Boolean, + onClearCustomStatus: () -> Unit ) { val hasStatus = emoji != null || textState.text.isNotBlank() @@ -320,12 +342,6 @@ private fun CustomStatusSection( style = MaterialTheme.wireTypography.body02, color = MaterialTheme.wireColorScheme.secondaryText, ) - WirePrimaryButton( - modifier = Modifier.padding(horizontal = dimensions().spacing16x), - text = stringResource(R.string.user_profile_update_status), - loading = isSaving, - onClick = onUpdateCustomStatus, - ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt index dbf4c753de8..ce3e54448f7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -25,14 +25,18 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.CurrentAccount +import com.wire.android.R import com.wire.android.ui.userprofile.self.dialog.StatusDialogData import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.common.functional.fold import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase import com.wire.kalium.logic.feature.user.UpdateSelfTextStatusUseCase import dev.zacsweers.metro.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch @@ -46,6 +50,8 @@ class SelfUserStatusViewModel @Inject constructor( private val dispatchers: DispatcherProvider, ) : ViewModel() { private val dataStore = userDataStoreProvider.getOrCreate(selfUserId) + private val _confirmationMessage = MutableSharedFlow(extraBufferCapacity = 1) + val confirmationMessage = _confirmationMessage.asSharedFlow() var state by mutableStateOf(SelfUserStatusState()) private set @@ -88,7 +94,10 @@ class SelfUserStatusViewModel @Inject constructor( val bundledStatus = buildTextStatus(state.emoji, message) viewModelScope.launch { state = state.copy(isSaving = true) - updateTextStatus(bundledStatus) + updateTextStatus(bundledStatus).fold( + fnL = { }, + fnR = { _confirmationMessage.tryEmit(R.string.user_profile_status_updated) } + ) state = state.copy(isSaving = false) } } @@ -98,7 +107,10 @@ class SelfUserStatusViewModel @Inject constructor( state = state.copy(emoji = null, message = "") viewModelScope.launch { state = state.copy(isSaving = true) - updateTextStatus(" ") + updateTextStatus(" ").fold( + fnL = { }, + fnR = { _confirmationMessage.tryEmit(R.string.user_profile_status_updated) } + ) state = state.copy(isSaving = false) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62fdeb05e4b..b96095bf0cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -716,6 +716,7 @@ Availability Status Update Status + Status updated Custom Status Enter custom status message %1$d/%2$d From 64da832690449a579145e77711191f60e2c6a9ea Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 14:01:17 +0200 Subject: [PATCH 11/18] feat: add presets --- .../self/status/SelfUserStatusScreen.kt | 67 +++++++++++++++++++ .../self/status/SelfUserStatusState.kt | 23 +++++++ .../self/status/SelfUserStatusViewModel.kt | 8 +++ app/src/main/res/values/strings.xml | 4 ++ 4 files changed, 102 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index 2808fd479b0..7734ae6ef4a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldState @@ -51,8 +52,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.wire.android.R +import com.wire.android.model.Clickable import com.wire.android.navigation.Navigator import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -64,6 +67,7 @@ import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.rowitem.SectionHeader +import com.wire.android.ui.common.rowitem.RowItemTemplate import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.spacers.VerticalSpace @@ -110,6 +114,7 @@ fun SelfUserStatusScreen( onDismissStatusDialog = viewModel::dismissStatusDialog, onNotShowRationaleAgainChange = viewModel::dialogCheckBoxStateChanged, onEmojiSelected = viewModel::onEmojiSelected, + onQuickStatusSelected = viewModel::onQuickStatusSelected, onClearCustomStatus = viewModel::clearCustomStatus, onUpdateCustomStatus = viewModel::updateCustomStatus, ) @@ -124,6 +129,7 @@ private fun SelfUserStatusContent( onDismissStatusDialog: () -> Unit, onNotShowRationaleAgainChange: (Boolean) -> Unit, onEmojiSelected: (String) -> Unit, + onQuickStatusSelected: (String, String) -> Unit, onClearCustomStatus: () -> Unit, onUpdateCustomStatus: (String) -> Unit, ) { @@ -190,6 +196,11 @@ private fun SelfUserStatusContent( onClearCustomStatus() } ) + VerticalSpace.x16() + QuickStatusSection( + presets = state.quickStatusPresets, + onQuickStatusSelected = onQuickStatusSelected, + ) } } @@ -345,6 +356,61 @@ private fun CustomStatusSection( } } +@Composable +private fun QuickStatusSection( + presets: List, + onQuickStatusSelected: (String, String) -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth()) { + SectionHeader(stringResource(R.string.user_profile_quick_status_header)) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x), + verticalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + ) { + presets.forEach { preset -> + val presetLabel = stringResource(preset.labelResId) + Surface( + color = MaterialTheme.wireColorScheme.surface, + shape = RoundedCornerShape(dimensions().spacing16x), + modifier = Modifier.fillMaxWidth(), + ) { + RowItemTemplate( + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x), + ) { + Text( + text = preset.emoji, + style = MaterialTheme.wireTypography.title02, + ) + Text( + text = presetLabel, + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + titleStartPadding = dimensions().spacing16x, + contentTopPadding = dimensions().spacing16x, + contentBottomPadding = dimensions().spacing16x, + actionsEndPadding = dimensions().spacing0x, + divider = {}, + backgroundColor = MaterialTheme.wireColorScheme.surface, + clickable = Clickable(enabled = true) { + onQuickStatusSelected(preset.emoji, presetLabel) + } + ) + } + } + } + } +} + @PreviewMultipleThemes @Composable private fun SelfUserStatusContentPreview() { @@ -362,6 +428,7 @@ private fun SelfUserStatusContentPreview() { onDismissStatusDialog = {}, onNotShowRationaleAgainChange = {}, onEmojiSelected = {}, + onQuickStatusSelected = { _, _ -> }, onClearCustomStatus = {}, onUpdateCustomStatus = {}, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt index 2e7b8824439..6049396b774 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt @@ -18,6 +18,8 @@ package com.wire.android.ui.userprofile.self.status +import androidx.annotation.StringRes +import com.wire.android.R import com.wire.android.ui.userprofile.self.dialog.StatusDialogData import com.wire.kalium.logic.data.user.UserAvailabilityStatus @@ -26,10 +28,31 @@ data class SelfUserStatusState( val emoji: String? = null, val message: String = "", val isTeamMember: Boolean = false, + val quickStatusPresets: List = defaultQuickStatusPresets(), val statusDialogData: StatusDialogData? = null, val isSaving: Boolean = false, ) +data class QuickStatusPreset( + val emoji: String, + @StringRes val labelResId: Int, +) + +fun defaultQuickStatusPresets(): List = listOf( + QuickStatusPreset( + emoji = "\uD83C\uDFA7", + labelResId = R.string.user_profile_quick_status_in_meeting, + ), + QuickStatusPreset( + emoji = "\uD83E\uDD15", + labelResId = R.string.user_profile_quick_status_out_sick, + ), + QuickStatusPreset( + emoji = "\uD83D\uDCF5", + labelResId = R.string.user_profile_quick_status_out_of_office, + ), +) + const val DEFAULT_STATUS_EMOJI = "\uD83D\uDCAC" const val MAX_STATUS_TEXT_LENGTH = 50 diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt index ce3e54448f7..042f2ca1742 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -64,6 +64,14 @@ class SelfUserStatusViewModel @Inject constructor( state = state.copy(emoji = emoji) } + fun onQuickStatusSelected(emoji: String, message: String) { + if (!state.isTeamMember) return + state = state.copy( + emoji = emoji, + message = message.take(MAX_STATUS_TEXT_LENGTH) + ) + } + fun dismissStatusDialog() { state = state.copy(statusDialogData = null) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b96095bf0cb..00616c7bdc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -720,6 +720,10 @@ Custom Status Enter custom status message %1$d/%2$d + Quick Status + In a meeting + Out sick + Out of office New Team or Add Account Manage Team Details From d25af7b6a322dc8743f1a67fdb58599f943b93aa Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 14:24:54 +0200 Subject: [PATCH 12/18] feat: design adjstments --- .../self/status/SelfUserStatusScreen.kt | 203 +++++++++--------- 1 file changed, 106 insertions(+), 97 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index 7734ae6ef4a..1c1afd360d2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldState @@ -66,11 +65,11 @@ import com.wire.android.ui.common.bottomsheet.show import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions -import com.wire.android.ui.common.rowitem.SectionHeader +import com.wire.android.ui.common.divider.WireDivider import com.wire.android.ui.common.rowitem.RowItemTemplate +import com.wire.android.ui.common.rowitem.SectionHeader 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.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.common.textfield.textAsFlow @@ -151,6 +150,7 @@ private fun SelfUserStatusContent( } WireScaffold( + containerColor = MaterialTheme.wireColorScheme.background, topBar = { WireCenterAlignedTopAppBar( onNavigationPressed = onBackClick, @@ -183,8 +183,6 @@ private fun SelfUserStatusContent( userStatus = state.availabilityStatus, onStatusClicked = onAvailabilityStatusClicked, ) - SelectedAvailabilityDescription(state.availabilityStatus) - VerticalSpace.x16() if (state.isTeamMember) { CustomStatusSection( emoji = resolveStatusEmoji(state.emoji, textState.text.toString()), @@ -196,7 +194,6 @@ private fun SelfUserStatusContent( onClearCustomStatus() } ) - VerticalSpace.x16() QuickStatusSection( presets = state.quickStatusPresets, onQuickStatusSelected = onQuickStatusSelected, @@ -254,24 +251,35 @@ private fun AvailabilitySection( Column(modifier = Modifier.fillMaxWidth()) { SectionHeader(stringResource(R.string.user_profile_status_availability)) - WireDropDown( - items = items.map { - when (it) { - UserAvailabilityStatus.AVAILABLE -> stringResource(UICommonR.string.user_profile_status_available) - UserAvailabilityStatus.BUSY -> stringResource(UICommonR.string.user_profile_status_busy) - UserAvailabilityStatus.AWAY -> stringResource(UICommonR.string.user_profile_status_away) - UserAvailabilityStatus.NONE -> stringResource(UICommonR.string.user_profile_status_none) + Surface( + color = MaterialTheme.wireColorScheme.surface, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(dimensions().spacing16x) + ) { + WireDropDown( + items = items.map { + when (it) { + UserAvailabilityStatus.AVAILABLE -> stringResource(UICommonR.string.user_profile_status_available) + UserAvailabilityStatus.BUSY -> stringResource(UICommonR.string.user_profile_status_busy) + UserAvailabilityStatus.AWAY -> stringResource(UICommonR.string.user_profile_status_away) + UserAvailabilityStatus.NONE -> stringResource(UICommonR.string.user_profile_status_none) + } + }, + defaultItemIndex = items.indexOf(userStatus), + label = null, + autoUpdateSelection = false, + showDefaultTextIndicator = false, + leadingCompose = { index -> UserStatusIndicator(items[index]) }, + onChangeClickDescription = stringResource(R.string.content_description_self_profile_change_status) + ) { selectedIndex -> + onStatusClicked(items[selectedIndex]) } - }, - defaultItemIndex = items.indexOf(userStatus), - label = null, - modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.spacing16x), - autoUpdateSelection = false, - showDefaultTextIndicator = false, - leadingCompose = { index -> UserStatusIndicator(items[index]) }, - onChangeClickDescription = stringResource(R.string.content_description_self_profile_change_status) - ) { selectedIndex -> - onStatusClicked(items[selectedIndex]) + SelectedAvailabilityDescription(userStatus) + } } } } @@ -288,71 +296,78 @@ private fun CustomStatusSection( Column(modifier = Modifier.fillMaxWidth()) { SectionHeader(stringResource(R.string.user_profile_custom_status_header)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = dimensions().spacing16x), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), - verticalAlignment = Alignment.CenterVertically, + Surface( + color = MaterialTheme.wireColorScheme.surface, + modifier = Modifier.fillMaxWidth(), ) { - WireSecondaryButton( - leadingIcon = { - if (emoji == null) { - Icon(painter = painterResource(id = R.drawable.ic_emoticon), contentDescription = "") - } else { - Text(emoji) - } - }, - onClick = onEmojiClick, - fillMaxWidth = false, - minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, - ) + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing16x), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + verticalAlignment = Alignment.CenterVertically, + ) { + WireSecondaryButton( + leadingIcon = { + if (emoji == null) { + Icon(painter = painterResource(id = R.drawable.ic_emoticon), contentDescription = "") + } else { + Text(emoji) + } + }, + onClick = onEmojiClick, + fillMaxWidth = false, + minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, + ) - WireTextField( - textState = textState, - modifier = Modifier.weight(1f), - placeholderText = stringResource(R.string.user_profile_custom_status_placeholder), - leadingIcon = null, - state = WireTextFieldState.Default, - trailingIcon = { - Box( - modifier = Modifier - .width(dimensions().spacing64x) - .height(dimensions().spacing40x), - contentAlignment = Alignment.CenterEnd - ) { - androidx.compose.animation.AnimatedVisibility( - visible = hasStatus, - enter = fadeIn(), - exit = fadeOut() - ) { - IconButton( - modifier = Modifier.padding(start = dimensions().spacing12x), - onClick = onClearCustomStatus, + WireTextField( + textState = textState, + modifier = Modifier.weight(1f), + placeholderText = stringResource(R.string.user_profile_custom_status_placeholder), + leadingIcon = null, + state = WireTextFieldState.Default, + trailingIcon = { + Box( + modifier = Modifier + .width(dimensions().spacing64x) + .height(dimensions().spacing40x), + contentAlignment = Alignment.CenterEnd ) { - Icon( - painter = painterResource(id = UICommonR.drawable.ic_clear_search), - contentDescription = stringResource(UICommonR.string.content_description_clear_content) - ) + androidx.compose.animation.AnimatedVisibility( + visible = hasStatus, + enter = fadeIn(), + exit = fadeOut() + ) { + IconButton( + modifier = Modifier.padding(start = dimensions().spacing12x), + onClick = onClearCustomStatus, + ) { + Icon( + painter = painterResource(id = UICommonR.drawable.ic_clear_search), + contentDescription = stringResource(UICommonR.string.content_description_clear_content) + ) + } + } } - } - } - }, - inputTransformation = InputTransformation.maxLength(MAX_STATUS_TEXT_LENGTH), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - imeAction = ImeAction.Done - ), - ) + }, + inputTransformation = InputTransformation.maxLength(MAX_STATUS_TEXT_LENGTH), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + ) + } + Text( + text = stringResource(R.string.user_profile_custom_status_counter, messageLength, MAX_STATUS_TEXT_LENGTH), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x), + style = MaterialTheme.wireTypography.body02, + color = MaterialTheme.wireColorScheme.secondaryText, + ) + } } - Text( - text = stringResource(R.string.user_profile_custom_status_counter, messageLength, MAX_STATUS_TEXT_LENGTH), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x), - style = MaterialTheme.wireTypography.body02, - color = MaterialTheme.wireColorScheme.secondaryText, - ) } } @@ -363,19 +378,13 @@ private fun QuickStatusSection( ) { Column(modifier = Modifier.fillMaxWidth()) { SectionHeader(stringResource(R.string.user_profile_quick_status_header)) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = dimensions().spacing16x), - verticalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + Surface( + color = MaterialTheme.wireColorScheme.surface, + modifier = Modifier.fillMaxWidth(), ) { - presets.forEach { preset -> - val presetLabel = stringResource(preset.labelResId) - Surface( - color = MaterialTheme.wireColorScheme.surface, - shape = RoundedCornerShape(dimensions().spacing16x), - modifier = Modifier.fillMaxWidth(), - ) { + Column(modifier = Modifier.fillMaxWidth()) { + presets.forEachIndexed { index, preset -> + val presetLabel = stringResource(preset.labelResId) RowItemTemplate( title = { Row( @@ -398,8 +407,8 @@ private fun QuickStatusSection( titleStartPadding = dimensions().spacing16x, contentTopPadding = dimensions().spacing16x, contentBottomPadding = dimensions().spacing16x, - actionsEndPadding = dimensions().spacing0x, - divider = {}, + actionsEndPadding = dimensions().spacing12x, + divider = { WireDivider() }, backgroundColor = MaterialTheme.wireColorScheme.surface, clickable = Clickable(enabled = true) { onQuickStatusSelected(preset.emoji, presetLabel) From ae6aa694986209bbf4f24f43d137c0b05eb38220 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 14:41:37 +0200 Subject: [PATCH 13/18] feat: design adjstments --- .../android/ui/userprofile/self/status/SelfUserStatusScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index 1c1afd360d2..d079677bb27 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -231,7 +231,7 @@ fun SelectedAvailabilityDescription(status: UserAvailabilityStatus) { text = stringResource(description), modifier = Modifier .fillMaxWidth() - .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x), + .padding(horizontal = dimensions().spacing8x, vertical = dimensions().spacing8x), style = MaterialTheme.wireTypography.body01, color = MaterialTheme.wireColorScheme.secondaryText, ) From 7ee4b168d5a2dea5db4eb5e7f4296435a077ce6c Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 15:25:53 +0200 Subject: [PATCH 14/18] feat: design adjstments --- .../self/status/SelfUserStatusScreen.kt | 228 +++++++++--------- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 113 insertions(+), 117 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index d079677bb27..03732ab4931 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -19,17 +19,18 @@ package com.wire.android.ui.userprofile.self.status import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldState @@ -37,6 +38,11 @@ import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.maxLength import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -44,17 +50,14 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.wire.android.R -import com.wire.android.model.Clickable import com.wire.android.navigation.Navigator import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -63,16 +66,13 @@ import com.wire.android.ui.common.avatar.UserStatusIndicator import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.bottomsheet.show import com.wire.android.ui.common.button.WirePrimaryButton -import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions -import com.wire.android.ui.common.divider.WireDivider -import com.wire.android.ui.common.rowitem.RowItemTemplate -import com.wire.android.ui.common.rowitem.SectionHeader import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.common.textfield.textAsFlow +import com.wire.android.ui.common.textfield.wireTextFieldColors import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.emoji.EmojiPickerBottomSheet @@ -162,7 +162,7 @@ private fun SelfUserStatusContent( bottomBar = { Surface( shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation, - color = MaterialTheme.wireColorScheme.background, + color = MaterialTheme.wireColorScheme.surface, modifier = Modifier.fillMaxWidth(), ) { WirePrimaryButton( @@ -177,14 +177,17 @@ private fun SelfUserStatusContent( Column( modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(internalPadding) + .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing12x), + verticalArrangement = Arrangement.spacedBy(dimensions().spacing16x), ) { - AvailabilitySection( + AvailabilityCard( userStatus = state.availabilityStatus, onStatusClicked = onAvailabilityStatusClicked, ) if (state.isTeamMember) { - CustomStatusSection( + CustomStatusCard( emoji = resolveStatusEmoji(state.emoji, textState.text.toString()), textState = textState, messageLength = textState.text.length, @@ -238,7 +241,17 @@ fun SelectedAvailabilityDescription(status: UserAvailabilityStatus) { } @Composable -private fun AvailabilitySection( +private fun SectionLabel(text: String, modifier: Modifier = Modifier) { + Text( + text = text, + modifier = modifier.padding(bottom = dimensions().spacing4x), + style = MaterialTheme.wireTypography.label02, + color = MaterialTheme.wireColorScheme.secondaryText, + ) +} + +@Composable +private fun AvailabilityCard( userStatus: UserAvailabilityStatus, onStatusClicked: (UserAvailabilityStatus) -> Unit, ) { @@ -250,10 +263,14 @@ private fun AvailabilitySection( ) Column(modifier = Modifier.fillMaxWidth()) { - SectionHeader(stringResource(R.string.user_profile_status_availability)) - Surface( - color = MaterialTheme.wireColorScheme.surface, + SectionLabel(stringResource(R.string.user_profile_status_availability)) + ElevatedCard( modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(dimensions().spacing12x), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.wireColorScheme.surface, + ), ) { Column( modifier = Modifier @@ -285,136 +302,115 @@ private fun AvailabilitySection( } @Composable -private fun CustomStatusSection( +private fun CustomStatusCard( emoji: String?, textState: TextFieldState, messageLength: Int, onEmojiClick: () -> Unit, - onClearCustomStatus: () -> Unit + onClearCustomStatus: () -> Unit, ) { val hasStatus = emoji != null || textState.text.isNotBlank() Column(modifier = Modifier.fillMaxWidth()) { - SectionHeader(stringResource(R.string.user_profile_custom_status_header)) - Surface( - color = MaterialTheme.wireColorScheme.surface, + SectionLabel(stringResource(R.string.user_profile_custom_status_header)) + ElevatedCard( modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(dimensions().spacing12x), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.wireColorScheme.surface, + ), ) { Column(modifier = Modifier.fillMaxWidth()) { - Row( + WireTextField( + textState = textState, modifier = Modifier .fillMaxWidth() - .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing16x), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), - verticalAlignment = Alignment.CenterVertically, - ) { - WireSecondaryButton( - leadingIcon = { + .padding(horizontal = dimensions().spacing12x, vertical = dimensions().spacing12x), + placeholderText = stringResource(R.string.user_profile_custom_status_placeholder), + leadingIcon = { + IconButton(onClick = onEmojiClick) { if (emoji == null) { - Icon(painter = painterResource(id = R.drawable.ic_emoticon), contentDescription = "") + Icon( + painter = painterResource(id = R.drawable.ic_emoticon), + contentDescription = null, + ) } else { - Text(emoji) + Text( + text = emoji, + style = MaterialTheme.wireTypography.title02, + ) } - }, - onClick = onEmojiClick, - fillMaxWidth = false, - minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, - ) - - WireTextField( - textState = textState, - modifier = Modifier.weight(1f), - placeholderText = stringResource(R.string.user_profile_custom_status_placeholder), - leadingIcon = null, - state = WireTextFieldState.Default, - trailingIcon = { - Box( - modifier = Modifier - .width(dimensions().spacing64x) - .height(dimensions().spacing40x), - contentAlignment = Alignment.CenterEnd - ) { - androidx.compose.animation.AnimatedVisibility( - visible = hasStatus, - enter = fadeIn(), - exit = fadeOut() - ) { - IconButton( - modifier = Modifier.padding(start = dimensions().spacing12x), - onClick = onClearCustomStatus, - ) { - Icon( - painter = painterResource(id = UICommonR.drawable.ic_clear_search), - contentDescription = stringResource(UICommonR.string.content_description_clear_content) - ) - } - } + } + }, + state = WireTextFieldState.Default, + colors = wireTextFieldColors( + backgroundColor = MaterialTheme.wireColorScheme.surfaceContainerLow, + ), + trailingIcon = { + AnimatedVisibility( + visible = hasStatus, + enter = fadeIn(), + exit = fadeOut(), + ) { + IconButton(onClick = onClearCustomStatus) { + Icon( + painter = painterResource(id = UICommonR.drawable.ic_clear_search), + contentDescription = stringResource(UICommonR.string.content_description_clear_content), + ) } - }, - inputTransformation = InputTransformation.maxLength(MAX_STATUS_TEXT_LENGTH), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - imeAction = ImeAction.Done - ), + } + }, + inputTransformation = InputTransformation.maxLength(MAX_STATUS_TEXT_LENGTH), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done, + ), + ) + AnimatedVisibility(visible = messageLength > 0) { + Text( + text = stringResource(R.string.user_profile_custom_status_counter, messageLength, MAX_STATUS_TEXT_LENGTH), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x), + style = MaterialTheme.wireTypography.label04, + color = MaterialTheme.wireColorScheme.secondaryText, ) } - Text( - text = stringResource(R.string.user_profile_custom_status_counter, messageLength, MAX_STATUS_TEXT_LENGTH), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = dimensions().spacing16x, vertical = dimensions().spacing8x), - style = MaterialTheme.wireTypography.body02, - color = MaterialTheme.wireColorScheme.secondaryText, - ) } } } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun QuickStatusSection( presets: List, onQuickStatusSelected: (String, String) -> Unit, ) { Column(modifier = Modifier.fillMaxWidth()) { - SectionHeader(stringResource(R.string.user_profile_quick_status_header)) - Surface( - color = MaterialTheme.wireColorScheme.surface, + SectionLabel(stringResource(R.string.user_profile_quick_status_header)) + FlowRow( modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + verticalArrangement = Arrangement.spacedBy(dimensions().spacing4x), ) { - Column(modifier = Modifier.fillMaxWidth()) { - presets.forEachIndexed { index, preset -> - val presetLabel = stringResource(preset.labelResId) - RowItemTemplate( - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing12x), - ) { - Text( - text = preset.emoji, - style = MaterialTheme.wireTypography.title02, - ) - Text( - text = presetLabel, - style = MaterialTheme.wireTypography.body01, - color = MaterialTheme.wireColorScheme.onBackground, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - }, - titleStartPadding = dimensions().spacing16x, - contentTopPadding = dimensions().spacing16x, - contentBottomPadding = dimensions().spacing16x, - actionsEndPadding = dimensions().spacing12x, - divider = { WireDivider() }, - backgroundColor = MaterialTheme.wireColorScheme.surface, - clickable = Clickable(enabled = true) { - onQuickStatusSelected(preset.emoji, presetLabel) - } - ) - } + presets.forEach { preset -> + val presetLabel = stringResource(preset.labelResId) + AssistChip( + onClick = { onQuickStatusSelected(preset.emoji, presetLabel) }, + label = { + Text( + text = "${preset.emoji} $presetLabel", + style = MaterialTheme.wireTypography.body02, + ) + }, + colors = AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.wireColorScheme.surfaceContainerLow, + labelColor = MaterialTheme.wireColorScheme.onBackground, + ), + border = null, + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00616c7bdc1..c82b6e17634 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -714,7 +714,7 @@ Log out Your Other Accounts Availability - Status + Set a status Update Status Status updated Custom Status From f12ace90e7f6f79eb51d61a614d87d75ecef098d Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 15:38:29 +0200 Subject: [PATCH 15/18] feat: design adjstments --- .../userprofile/self/SelfUserProfileScreen.kt | 12 ++++++++++++ .../self/status/SelfUserStatusScreen.kt | 18 ++++++++++-------- .../self/status/SelfUserStatusState.kt | 2 ++ .../self/status/SelfUserStatusViewModel.kt | 15 ++++++++------- 4 files changed, 32 insertions(+), 15 deletions(-) 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 707ac144499..08400f01cf4 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 @@ -38,6 +38,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -116,11 +118,15 @@ fun SelfUserProfileScreen( navigator: Navigator, loginTypeSelector: LoginTypeSelector, avatarPickerResultRecipient: ResultRecipient, + statusUpdateResultRecipient: ResultRecipient, viewModelSelf: SelfUserProfileViewModel = selfUserProfileViewModel(), legalHoldRequestedViewModel: LegalHoldRequestedViewModel = legalHoldRequestedViewModel() ) { val legalHoldSubjectDialogState = rememberVisibilityState() val logoutAction = LocalSelfUserProfileLogoutAction.current + val snackbarHostState = LocalSnackbarHostState.current + val coroutineScope = rememberCoroutineScope() + val statusUpdatedMessage = stringResource(R.string.user_profile_status_updated) LaunchedEffect(Unit) { // Check if the user is able to migrate to a team account, every time the screen is shown @@ -189,6 +195,12 @@ fun SelfUserProfileScreen( } } + statusUpdateResultRecipient.onNavResult { result -> + if (result is NavResult.Value && result.value) { + coroutineScope.launch { snackbarHostState.showSnackbar(statusUpdatedMessage) } + } + } + if (legalHoldRequestedViewModel.state is LegalHoldRequestedState.Visible) { LegalHoldRequestedDialog( state = legalHoldRequestedViewModel.state as LegalHoldRequestedState.Visible, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt index 03732ab4931..fda986b07d4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusScreen.kt @@ -51,12 +51,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.R import com.wire.android.navigation.Navigator import com.wire.android.navigation.annotation.app.WireRootDestination @@ -65,10 +65,10 @@ import com.wire.android.ui.common.WireDropDown import com.wire.android.ui.common.avatar.UserStatusIndicator import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.bottomsheet.show +import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton 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.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.common.textfield.textAsFlow @@ -94,14 +94,13 @@ import com.wire.android.ui.common.R as UICommonR @SuppressLint("ComposeModifierMissing") fun SelfUserStatusScreen( navigator: Navigator, + resultNavigator: ResultBackNavigator, viewModel: SelfUserStatusViewModel = selfUserStatusViewModel(), ) { - val snackbarHostState = LocalSnackbarHostState.current - val context = LocalContext.current - LaunchedEffect(viewModel) { - viewModel.confirmationMessage.collect { messageResId -> - snackbarHostState.showSnackbar(context.getString(messageResId)) + viewModel.statusUpdated.collect { success -> + resultNavigator.setResult(success) + resultNavigator.navigateBack() } } @@ -134,6 +133,8 @@ private fun SelfUserStatusContent( ) { val textState = rememberTextFieldState(state.message) val emojiPickerState = rememberWireModalSheetState(skipPartiallyExpanded = false) + val hasUnsavedChanges = state.isTeamMember && + (textState.text.toString() != state.savedMessage || state.emoji != state.savedEmoji) LaunchedEffect(state.message) { if (textState.text.toString() != state.message) { @@ -162,13 +163,14 @@ private fun SelfUserStatusContent( bottomBar = { Surface( shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation, - color = MaterialTheme.wireColorScheme.surface, + color = MaterialTheme.wireColorScheme.background, modifier = Modifier.fillMaxWidth(), ) { WirePrimaryButton( modifier = Modifier.padding(dimensions().spacing16x), text = stringResource(R.string.user_profile_update_status), loading = state.isSaving, + state = if (hasUnsavedChanges) WireButtonState.Default else WireButtonState.Disabled, onClick = { onUpdateCustomStatus(textState.text.toString()) }, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt index 6049396b774..ff6d17ac0e5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusState.kt @@ -27,6 +27,8 @@ data class SelfUserStatusState( val availabilityStatus: UserAvailabilityStatus = UserAvailabilityStatus.NONE, val emoji: String? = null, val message: String = "", + val savedEmoji: String? = null, + val savedMessage: String = "", val isTeamMember: Boolean = false, val quickStatusPresets: List = defaultQuickStatusPresets(), val statusDialogData: StatusDialogData? = null, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt index 042f2ca1742..bd7fcba5181 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/status/SelfUserStatusViewModel.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.CurrentAccount -import com.wire.android.R import com.wire.android.ui.userprofile.self.dialog.StatusDialogData import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.common.functional.fold @@ -50,8 +49,8 @@ class SelfUserStatusViewModel @Inject constructor( private val dispatchers: DispatcherProvider, ) : ViewModel() { private val dataStore = userDataStoreProvider.getOrCreate(selfUserId) - private val _confirmationMessage = MutableSharedFlow(extraBufferCapacity = 1) - val confirmationMessage = _confirmationMessage.asSharedFlow() + private val _statusUpdated = MutableSharedFlow(extraBufferCapacity = 1) + val statusUpdated = _statusUpdated.asSharedFlow() var state by mutableStateOf(SelfUserStatusState()) private set @@ -103,8 +102,8 @@ class SelfUserStatusViewModel @Inject constructor( viewModelScope.launch { state = state.copy(isSaving = true) updateTextStatus(bundledStatus).fold( - fnL = { }, - fnR = { _confirmationMessage.tryEmit(R.string.user_profile_status_updated) } + fnL = { _statusUpdated.tryEmit(false) }, + fnR = { _statusUpdated.tryEmit(true) } ) state = state.copy(isSaving = false) } @@ -116,8 +115,8 @@ class SelfUserStatusViewModel @Inject constructor( viewModelScope.launch { state = state.copy(isSaving = true) updateTextStatus(" ").fold( - fnL = { }, - fnR = { _confirmationMessage.tryEmit(R.string.user_profile_status_updated) } + fnL = { _statusUpdated.tryEmit(false) }, + fnR = { _statusUpdated.tryEmit(true) } ) state = state.copy(isSaving = false) } @@ -133,6 +132,8 @@ class SelfUserStatusViewModel @Inject constructor( availabilityStatus = selfUser.availabilityStatus, emoji = parsedStatus.emoji, message = parsedStatus.message, + savedEmoji = parsedStatus.emoji, + savedMessage = parsedStatus.message, isTeamMember = team != null, ) } From 27dcdeb2deae1831132d06aab5084a3c1b222ffb Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 16:02:16 +0200 Subject: [PATCH 16/18] feat: design adjstments --- .../userprofile/self/SelfUserProfileScreen.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) 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 08400f01cf4..80473393e4b 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 @@ -257,6 +257,21 @@ private fun SelfUserProfileContent( } } ) + }, + bottomBar = { + val context = LocalContext.current + Column { + Divider(color = MaterialTheme.wireColorScheme.outline) + Column( + modifier = Modifier.padding(dimensions().spacing16x), + verticalArrangement = Arrangement.spacedBy(dimensions().spacing8x) + ) { + if (state.teamUrl != null) { + ManageTeamButton { uriHandler.openUri(state.teamUrl) } + } + NewTeamButton(onAddAccountClick, isUserInCall, context) + } + } } ) { internalPadding -> with(state) { @@ -353,18 +368,6 @@ private fun SelfUserProfileContent( } } } - - Divider(color = MaterialTheme.wireColorScheme.outline) - - Column( - modifier = Modifier.padding(dimensions().spacing16x), - verticalArrangement = Arrangement.spacedBy(dimensions().spacing8x) - ) { - if (teamUrl != null) { - ManageTeamButton { uriHandler.openUri(teamUrl) } - } - NewTeamButton(onAddAccountClick, isUserInCall, context) - } } LogoutOptionsDialog( dialogState = logoutOptionsDialogState, From d370dd7aa1ac7a8f4fbdb97f1748a75b38abcc50 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 17:03:44 +0200 Subject: [PATCH 17/18] feat: autocalling --- .../wire/android/GlobalObserversManager.kt | 3 + .../wire/android/datastore/UserDataStore.kt | 46 +++++ .../feature/calling/AutoCallStatusObserver.kt | 192 ++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 app/src/main/kotlin/com/wire/android/feature/calling/AutoCallStatusObserver.kt diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt index 0dfa1919d78..1f885594fc7 100644 --- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt +++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt @@ -20,6 +20,7 @@ package com.wire.android import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.calling.AutoCallStatusObserver import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager import com.wire.android.util.CurrentScreenManager @@ -61,12 +62,14 @@ class GlobalObserversManager @Inject constructor( private val notificationChannelsManager: NotificationChannelsManager, private val userDataStoreProvider: UserDataStoreProvider, private val currentScreenManager: CurrentScreenManager, + private val autoCallStatusObserver: AutoCallStatusObserver, ) { // TODO(tests): refactor so scope/dispatcher can be injected and properly stopped private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.io()) fun observe() { scope.launch { setUpNotifications() } + scope.launch { autoCallStatusObserver.observe() } scope.launch { coreLogic.getGlobalScope().observeValidAccounts().distinctUntilChanged().collectLatest { coroutineScope { diff --git a/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt index 357b3779b42..9782a0f32ee 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt @@ -29,6 +29,7 @@ import androidx.datastore.preferences.preferencesDataStore import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map @Suppress("TooManyFunctions") @@ -99,6 +100,43 @@ class UserDataStore(private val context: Context, userId: UserId) { context.dataStore.edit { it[IS_CREATE_TEAM_NOTICE_READ] = isRead } } + suspend fun saveAutoCallStatusSnapshot( + availabilityStatus: UserAvailabilityStatus, + textStatus: String? + ) { + context.dataStore.edit { preferences -> + preferences[AUTO_CALL_STATUS_ACTIVE] = true + preferences[AUTO_CALL_STATUS_PREVIOUS_AVAILABILITY] = availabilityStatus.name + if (textStatus == null) { + preferences.remove(AUTO_CALL_STATUS_PREVIOUS_TEXT_STATUS) + } else { + preferences[AUTO_CALL_STATUS_PREVIOUS_TEXT_STATUS] = textStatus + } + } + } + + suspend fun getAutoCallStatusSnapshot(): AutoCallStatusSnapshot? = + context.dataStore.data.map { preferences -> + if (preferences[AUTO_CALL_STATUS_ACTIVE] != true) { + null + } else { + AutoCallStatusSnapshot( + availabilityStatus = preferences[AUTO_CALL_STATUS_PREVIOUS_AVAILABILITY] + ?.let(UserAvailabilityStatus::valueOf) + ?: UserAvailabilityStatus.NONE, + textStatus = preferences[AUTO_CALL_STATUS_PREVIOUS_TEXT_STATUS] + ) + } + }.firstOrNull() + + suspend fun clearAutoCallStatusSnapshot() { + context.dataStore.edit { preferences -> + preferences.remove(AUTO_CALL_STATUS_ACTIVE) + preferences.remove(AUTO_CALL_STATUS_PREVIOUS_AVAILABILITY) + preferences.remove(AUTO_CALL_STATUS_PREVIOUS_TEXT_STATUS) + } + } + companion object { private const val PREFERENCES_NAME = "user_data" @@ -113,5 +151,13 @@ class UserDataStore(private val context: Context, userId: UserId) { private val ANONYMOUS_ANALYTICS = booleanPreferencesKey("anonymous_analytics") private val ANALYTICS_DIALOG_SEEN = booleanPreferencesKey("analytics_dialog_seen") private val IS_CREATE_TEAM_NOTICE_READ = booleanPreferencesKey("is_create_team_notice_read") + private val AUTO_CALL_STATUS_ACTIVE = booleanPreferencesKey("auto_call_status_active") + private val AUTO_CALL_STATUS_PREVIOUS_AVAILABILITY = stringPreferencesKey("auto_call_status_previous_availability") + private val AUTO_CALL_STATUS_PREVIOUS_TEXT_STATUS = stringPreferencesKey("auto_call_status_previous_text_status") } } + +data class AutoCallStatusSnapshot( + val availabilityStatus: UserAvailabilityStatus, + val textStatus: String? +) diff --git a/app/src/main/kotlin/com/wire/android/feature/calling/AutoCallStatusObserver.kt b/app/src/main/kotlin/com/wire/android/feature/calling/AutoCallStatusObserver.kt new file mode 100644 index 00000000000..506edcf068e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/feature/calling/AutoCallStatusObserver.kt @@ -0,0 +1,192 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.feature.calling + +import android.content.Context +import com.wire.android.R +import com.wire.android.appLogger +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.ApplicationContext +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.userprofile.self.status.buildTextStatus +import com.wire.kalium.common.functional.fold +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.call.Call +import com.wire.kalium.logic.data.user.SelfUser +import com.wire.kalium.logic.data.user.UserAvailabilityStatus +import com.wire.kalium.logic.data.user.UserId +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import java.util.concurrent.ConcurrentHashMap + +@SingleIn(AppScope::class) +class AutoCallStatusObserver @Inject constructor( + @ApplicationContext private val context: Context, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val userDataStoreProvider: UserDataStoreProvider, +) { + + private val userMutexes = ConcurrentHashMap() + + suspend fun observe() { + coreLogic.getGlobalScope().observeValidAccounts().collectLatest { validAccounts -> + kotlinx.coroutines.coroutineScope { + validAccounts.forEach { (selfUser, team) -> + launch { + observeAccountCallStatus(selfUser.id, team != null) + } + } + } + } + } + + private suspend fun observeAccountCallStatus(userId: UserId, isTeamMember: Boolean) { + val sessionScope = coreLogic.getSessionScope(userId) + combine( + sessionScope.calls.establishedCall(), + sessionScope.users.observeSelfUserWithTeam() + ) { establishedCalls, selfWithTeam -> + AccountCallStatusState( + isTeamMember = isTeamMember && selfWithTeam.second != null, + establishedCalls = establishedCalls, + selfUser = selfWithTeam.first, + ) + }.collectLatest { state -> + reconcile(userId, state) + } + } + + private suspend fun reconcile(userId: UserId, state: AccountCallStatusState) { + userMutex(userId).withLock { + val dataStore = userDataStoreProvider.getOrCreate(userId) + val snapshot = dataStore.getAutoCallStatusSnapshot() + val hasEstablishedCall = state.establishedCalls.isNotEmpty() + + if (!state.isTeamMember) { + if (snapshot != null) { + if (restoreSnapshot(userId, state.selfUser, snapshot.availabilityStatus, snapshot.textStatus)) { + dataStore.clearAutoCallStatusSnapshot() + } + } + return + } + + if (hasEstablishedCall) { + if (snapshot != null) { + if (!isAutoCallStatusApplied(state.selfUser)) { + applyMeetingStatus(userId, state.selfUser) + } + return + } + + dataStore.saveAutoCallStatusSnapshot( + availabilityStatus = state.selfUser.availabilityStatus, + textStatus = state.selfUser.textStatus + ) + applyMeetingStatus(userId, state.selfUser) + return + } + + if (snapshot != null) { + if (restoreSnapshot(userId, state.selfUser, snapshot.availabilityStatus, snapshot.textStatus)) { + dataStore.clearAutoCallStatusSnapshot() + } + } + } + } + + private suspend fun applyMeetingStatus(userId: UserId, selfUser: SelfUser): Boolean { + val statusText = buildTextStatus( + emoji = QUICK_STATUS_IN_MEETING_EMOJI, + message = context.getString(R.string.user_profile_quick_status_in_meeting) + ) + val userScope = coreLogic.getSessionScope(userId).users + var success = true + + if (selfUser.availabilityStatus != AUTO_CALL_AVAILABILITY) { + userScope.updateSelfAvailabilityStatus(AUTO_CALL_AVAILABILITY) + } + + if (selfUser.textStatus != statusText) { + userScope.updateSelfTextStatus(statusText) + .fold( + fnL = { + success = false + appLogger.e("Failed to update auto call text status for $userId: $it") + }, + fnR = {} + ) + } + + return success + } + + private suspend fun restoreSnapshot( + userId: UserId, + selfUser: SelfUser, + availabilityStatus: UserAvailabilityStatus, + textStatus: String? + ): Boolean { + val userScope = coreLogic.getSessionScope(userId).users + var success = true + + if (selfUser.availabilityStatus != availabilityStatus) { + userScope.updateSelfAvailabilityStatus(availabilityStatus) + } + + if (selfUser.textStatus != textStatus) { + userScope.updateSelfTextStatus(textStatus) + .fold( + fnL = { + success = false + appLogger.e("Failed to restore text status after call for $userId: $it") + }, + fnR = {} + ) + } + + return success + } + + private fun userMutex(userId: UserId): Mutex = userMutexes.getOrPut(userId) { Mutex() } + + private fun isAutoCallStatusApplied(selfUser: SelfUser): Boolean = + selfUser.availabilityStatus == AUTO_CALL_AVAILABILITY && + selfUser.textStatus == buildTextStatus( + emoji = QUICK_STATUS_IN_MEETING_EMOJI, + message = context.getString(R.string.user_profile_quick_status_in_meeting) + ) + + private data class AccountCallStatusState( + val isTeamMember: Boolean, + val establishedCalls: List, + val selfUser: SelfUser, + ) + + companion object { + private val AUTO_CALL_AVAILABILITY = UserAvailabilityStatus.BUSY + private const val QUICK_STATUS_IN_MEETING_EMOJI = "\uD83C\uDFA7" + } +} From e66602d43cfed71bea87fcd1905edbb480c0d7c3 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 18 Jun 2026 17:22:04 +0200 Subject: [PATCH 18/18] feat: adjs style availability --- .../com/wire/android/ui/userprofile/common/UserProfileInfo.kt | 3 +++ .../wire/android/ui/userprofile/self/SelfUserProfileScreen.kt | 1 + 2 files changed, 4 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index 970368a5868..50219039c64 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout @@ -121,6 +122,7 @@ fun UserProfileInfo( showQrCode: Boolean = true, textStatus: String? = null, availabilityStatus: UserAvailabilityStatus = UserAvailabilityStatus.NONE, + avatarStatusSize: Dp = dimensions().avatarStatusSize, ) { Column( horizontalAlignment = CenterHorizontally, @@ -159,6 +161,7 @@ fun UserProfileInfo( UserProfileAvatar( size = dimensions().avatarDefaultBigSize, temporaryUserBorderWidth = dimensions().avatarBigTemporaryUserBorderWidth, + statusSize = avatarStatusSize, avatarData = userAvatarData, clickable = remember(editableState) { Clickable( 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 80473393e4b..bef670f85b4 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 @@ -315,6 +315,7 @@ private fun SelfUserProfileContent( accentId = accentId, showQrCode = state.showQrCode, availabilityStatus = status, + avatarStatusSize = MaterialTheme.wireDimensions.avatarStatusSize + 2.dp, ) if (state.legalHoldStatus != LegalHoldUIState.None) { Box(