From c6ff93af982ced40683209cbf62e97b9290fe9a5 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 10 Sep 2026 15:27:28 -0400 Subject: [PATCH] Add username onboarding banner in new chat screen. --- .../conversation/NewConversationActivity.kt | 29 ++++- .../conversation/NewConversationViewModel.kt | 16 ++- .../securesms/keyvalue/UiHintValues.java | 9 ++ .../profiles/username/SetUpUsernameBanner.kt | 105 ++++++++++++++++++ .../recipients/ui/RecipientPicker.kt | 5 +- .../values/set_up_username_banner_colors.xml | 5 + app/src/main/res/values/strings.xml | 8 ++ 7 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/profiles/username/SetUpUsernameBanner.kt create mode 100644 app/src/main/res/values/set_up_username_banner_colors.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt index 28d0459bfa..d0970aa94a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt @@ -14,6 +14,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon @@ -49,6 +55,8 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity +import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity +import org.thoughtcrime.securesms.profiles.username.SetUpUsernameBanner import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.ui.RecipientLookupFailureMessage @@ -141,6 +149,8 @@ private fun NewConversationScreen( } override fun onInviteToSignal() = context.startActivity(AppSettingsActivity.invite(context)) + override fun onSetUpUsername() = context.startActivity(EditProfileActivity.getIntentForUsernameEdit(context)) + override fun onSetUpUsernameBannerDismissed() = viewModel.dismissSetUpUsernameBanner() override fun onRefresh() = viewModel.refresh() override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.clearUserMessage() override fun onContactsListReset() = viewModel.clearShouldResetContactsList() @@ -275,6 +285,8 @@ private interface NewConversationUiCallbacks : fun onRemoveConfirmed(recipient: Recipient) fun onBlockConfirmed(recipient: Recipient) fun onUserMessageDismissed(userMessage: UserMessage) + fun onSetUpUsername() + fun onSetUpUsernameBannerDismissed() fun onBackPressed() object Empty : NewConversationUiCallbacks { @@ -295,6 +307,8 @@ private interface NewConversationUiCallbacks : override fun onRefresh() = Unit override fun onContactsListReset() = Unit override fun onUserMessageDismissed(userMessage: UserMessage) = Unit + override fun onSetUpUsername() = Unit + override fun onSetUpUsernameBannerDismissed() = Unit override fun onBackPressed() = Unit } } @@ -320,7 +334,20 @@ private fun NewConversationRecipientPicker( findByPhoneNumber = callbacks ) }, - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), + belowSearchBar = { + AnimatedVisibility( + visible = uiState.showSetUpUsernameBanner, + enter = fadeIn(tween(150)) + expandVertically(tween(200)), + exit = fadeOut(tween(100)) + shrinkVertically(tween(200)) + ) { + SetUpUsernameBanner( + onSetUpClick = callbacks::onSetUpUsername, + onDismissClick = callbacks::onSetUpUsernameBannerDismissed, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt index fc40d4fc41..d8cf5476e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt @@ -34,7 +34,13 @@ class NewConversationViewModel : ViewModel() { private val TAG = Log.tag(NewConversationViewModel::class) } - private val internalUiState = MutableStateFlow(NewConversationUiState()) + private val internalUiState = MutableStateFlow( + NewConversationUiState( + showSetUpUsernameBanner = SignalStore.account.isPhoneNumberless && + SignalStore.account.username == null && + !SignalStore.uiHints.hasDismissedSetUpUsernameBanner() + ) + ) val uiState: StateFlow = internalUiState.asStateFlow() private val contactsManagementRepo = ContactsManagementRepository(AppDependencies.application) @@ -176,6 +182,11 @@ class NewConversationViewModel : ViewModel() { fun clearUserMessage() { internalUiState.update { it.copy(userMessage = null) } } + + fun dismissSetUpUsernameBanner() { + SignalStore.uiHints.markHasDismissedSetUpUsernameBanner() + internalUiState.update { it.copy(showSetUpUsernameBanner = false) } + } } data class NewConversationUiState( @@ -184,7 +195,8 @@ data class NewConversationUiState( val isRefreshingContacts: Boolean = false, val shouldResetContactsList: Boolean = false, val pendingDestination: RecipientId? = null, - val userMessage: UserMessage? = null + val userMessage: UserMessage? = null, + val showSetUpUsernameBanner: Boolean = false ) { sealed interface UserMessage { sealed interface Info : UserMessage { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java index 74ba5e14a4..1474959017 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java @@ -33,6 +33,7 @@ public class UiHintValues extends SignalStoreValues { private static final String HAS_SEEN_VERIFY_AUTO_SHEET = "uihints.has_seen_verify_auto_sheet"; private static final String HAS_DISMISSED_MEMBER_LABEL_ABOUT_OVERRIDE_WARNING = "uihints.has_dismissed_member_label_about_override_warning"; private static final String HAS_SEEN_ADMIN_DELETE_EDUCATION_DIALOG = "uihints.has_seen_admin_delete_education_dialog"; + private static final String DISMISSED_SET_UP_USERNAME_BANNER = "uihints.dismissed_set_up_username_banner"; UiHintValues(@NonNull KeyValueStore store) { super(store); @@ -72,6 +73,14 @@ public class UiHintValues extends SignalStoreValues { return getBoolean(HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE, false); } + public void markHasDismissedSetUpUsernameBanner() { + putBoolean(DISMISSED_SET_UP_USERNAME_BANNER, true); + } + + public boolean hasDismissedSetUpUsernameBanner() { + return getBoolean(DISMISSED_SET_UP_USERNAME_BANNER, false); + } + public boolean hasSetOrSkippedUsernameCreation() { return getBoolean(HAS_SET_OR_SKIPPED_USERNAME_CREATION, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/username/SetUpUsernameBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/username/SetUpUsernameBanner.kt new file mode 100644 index 0000000000..fed889b4eb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/username/SetUpUsernameBanner.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.profiles.username + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.Buttons +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.IconButtons +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.SignalIcons +import org.thoughtcrime.securesms.R +import org.signal.core.ui.R as CoreUiR + +/** + * Prompts a phone-numberless user without a username to create one. Dismissal is permanent. + */ +@Composable +fun SetUpUsernameBanner( + onSetUpClick: () -> Unit, + onDismissClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(36.dp)) + .background(colorResource(R.color.set_up_username_banner_background)) + .defaultMinSize(minHeight = 72.dp) + .padding(start = 20.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + ) { + Icon( + imageVector = ImageVector.vectorResource(CoreUiR.drawable.symbol_at_24), + contentDescription = null, + tint = colorResource(CoreUiR.color.signal_light_colorOnSurface), + modifier = Modifier.size(24.dp) + ) + + Text( + text = stringResource(R.string.SetUpUsernameBanner__choose_a_username_so_others_can_connect_with_you), + style = MaterialTheme.typography.bodyMedium, + color = colorResource(CoreUiR.color.signal_light_colorOnSurface), + modifier = Modifier + .weight(1f) + .padding(start = 12.dp) + ) + + Buttons.Small( + onClick = onSetUpClick, + colors = ButtonDefaults.buttonColors( + containerColor = colorResource(R.color.set_up_username_banner_button_background), + contentColor = colorResource(CoreUiR.color.signal_light_colorNeutralInverse) + ) + ) { + Text(text = stringResource(R.string.SetUpUsernameBanner__set_up)) + } + + IconButtons.IconButton( + onClick = onDismissClick, + size = 32.dp + ) { + Icon( + imageVector = SignalIcons.X.imageVector, + contentDescription = stringResource(R.string.SetUpUsernameBanner__dismiss), + tint = colorResource(CoreUiR.color.signal_light_colorOnSurface) + ) + } + } +} + +@DayNightPreviews +@Composable +private fun SetUpUsernameBannerPreview() { + Previews.Preview { + SetUpUsernameBanner( + onSetUpClick = {}, + onDismissClick = {}, + modifier = Modifier.padding(16.dp) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPicker.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPicker.kt index cff02b51b8..a47bf1df9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPicker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientPicker.kt @@ -83,7 +83,8 @@ fun RecipientPicker( listBottomPadding: Dp? = null, clipListToPadding: Boolean = ContactSelectionArguments.Defaults.RECYCLER_CHILD_CLIPPING, callbacks: RecipientPickerCallbacks, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + belowSearchBar: @Composable () -> Unit = {} ) { Column( modifier = modifier @@ -114,6 +115,8 @@ fun RecipientPicker( .padding(horizontal = 16.dp) ) + belowSearchBar() + RecipientSearchResultsList( displayModes = displayModes, selectionLimits = selectionLimits, diff --git a/app/src/main/res/values/set_up_username_banner_colors.xml b/app/src/main/res/values/set_up_username_banner_colors.xml new file mode 100644 index 0000000000..75de91ed9d --- /dev/null +++ b/app/src/main/res/values/set_up_username_banner_colors.xml @@ -0,0 +1,5 @@ + + + #FFE5E8F8 + #FFC2C8EE + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c01e95643..bc317a9740 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3513,6 +3513,14 @@ Not now + + + Choose a username so others can connect with you. + + Set up + + Dismiss + Play video