Add username card to onboarding.

This commit is contained in:
Greyson Parrelli
2026-09-10 17:10:15 -04:00
committed by Cody Henthorne
parent 56b4829b8b
commit 13652a74b6
6 changed files with 271 additions and 9 deletions
@@ -9,10 +9,11 @@ import java.util.List;
public final class OnboardingValues extends SignalStoreValues {
private static final String SHOW_NEW_GROUP = "onboarding.new_group";
private static final String SHOW_INVITE_FRIENDS = "onboarding.invite_friends";
private static final String SHOW_APPEARANCE = "onboarding.appearance";
private static final String SHOW_ADD_PHOTO = "onboarding.add_photo";
private static final String SHOW_NEW_GROUP = "onboarding.new_group";
private static final String SHOW_INVITE_FRIENDS = "onboarding.invite_friends";
private static final String SHOW_APPEARANCE = "onboarding.appearance";
private static final String SHOW_ADD_PHOTO = "onboarding.add_photo";
private static final String SHOW_SET_UP_USERNAME = "onboarding.set_up_username";
OnboardingValues(@NonNull KeyValueStore store) {
super(store);
@@ -24,6 +25,7 @@ public final class OnboardingValues extends SignalStoreValues {
putBoolean(SHOW_INVITE_FRIENDS, true);
putBoolean(SHOW_APPEARANCE, true);
putBoolean(SHOW_ADD_PHOTO, true);
putBoolean(SHOW_SET_UP_USERNAME, true);
}
@Override
@@ -36,13 +38,15 @@ public final class OnboardingValues extends SignalStoreValues {
setShowInviteFriends(false);
setShowAppearance(false);
setShowAddPhoto(false);
setShowSetUpUsername(false);
}
public boolean hasOnboarding(@NonNull Context context) {
return shouldShowNewGroup() ||
shouldShowInviteFriends() ||
shouldShowAppearance() ||
shouldShowAddPhoto();
return shouldShowNewGroup() ||
shouldShowInviteFriends() ||
shouldShowAppearance() ||
shouldShowAddPhoto() ||
shouldShowSetUpUsername();
}
public void setShowNewGroup(boolean value) {
@@ -76,4 +80,12 @@ public final class OnboardingValues extends SignalStoreValues {
public boolean shouldShowAddPhoto() {
return getBoolean(SHOW_ADD_PHOTO, false);
}
public void setShowSetUpUsername(boolean value) {
putBoolean(SHOW_SET_UP_USERNAME, value);
}
public boolean shouldShowSetUpUsername() {
return getBoolean(SHOW_SET_UP_USERNAME, false);
}
}
@@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.EmptyMegaphoneActionController
import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity
import org.thoughtcrime.securesms.profiles.username.ConnectWithUsernamesDialogFragment
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity
import org.signal.core.ui.R as CoreUiR
@@ -226,6 +227,11 @@ enum class OnboardingListItem(
@DrawableRes val icon: Int,
@ColorRes val cardColor: Int
) {
SET_UP_USERNAME(
title = R.string.Megaphones_set_up_username,
icon = CoreUiR.drawable.symbol_at_24,
cardColor = R.color.onboarding_background_5
),
GROUP(
title = R.string.Megaphones_new_group,
icon = R.drawable.symbol_group_24,
@@ -291,6 +297,7 @@ abstract class OnboardingState private constructor(
*/
private object Preview : OnboardingState(
initialState = DisplayState(
shouldShowSetUpUsername = true,
shouldShowNewGroup = true,
shouldShowInviteFriends = true,
shouldShowAddPhoto = true,
@@ -300,6 +307,7 @@ abstract class OnboardingState private constructor(
) {
override fun onItemCloseClick(onboardingListItem: OnboardingListItem) {
displayState = when (onboardingListItem) {
OnboardingListItem.SET_UP_USERNAME -> displayState.copy(shouldShowSetUpUsername = false)
OnboardingListItem.GROUP -> displayState.copy(shouldShowNewGroup = false)
OnboardingListItem.INVITE -> displayState.copy(shouldShowInviteFriends = false)
OnboardingListItem.ADD_PHOTO -> displayState.copy(shouldShowAddPhoto = false)
@@ -316,6 +324,7 @@ abstract class OnboardingState private constructor(
private class Real(megaphoneActionController: MegaphoneActionController) : OnboardingState(megaphoneActionController = megaphoneActionController) {
override fun onItemCloseClick(onboardingListItem: OnboardingListItem) {
when (onboardingListItem) {
OnboardingListItem.SET_UP_USERNAME -> SignalStore.onboarding.setShowSetUpUsername(false)
OnboardingListItem.GROUP -> SignalStore.onboarding.setShowNewGroup(false)
OnboardingListItem.INVITE -> SignalStore.onboarding.setShowInviteFriends(false)
OnboardingListItem.ADD_PHOTO -> SignalStore.onboarding.setShowAddPhoto(false)
@@ -331,6 +340,7 @@ abstract class OnboardingState private constructor(
override fun onItemActionClick(onboardingListItem: OnboardingListItem) {
when (onboardingListItem) {
OnboardingListItem.SET_UP_USERNAME -> megaphoneActionController.onMegaphoneDialogFragmentRequested(ConnectWithUsernamesDialogFragment())
OnboardingListItem.GROUP -> megaphoneActionController.onMegaphoneNavigationRequested(CreateGroupActivity.createIntent(megaphoneActionController.megaphoneActivity))
OnboardingListItem.INVITE -> megaphoneActionController.onMegaphoneNavigationRequested(AppSettingsActivity.invite(megaphoneActionController.megaphoneActivity))
OnboardingListItem.ADD_PHOTO -> {
@@ -355,15 +365,17 @@ abstract class OnboardingState private constructor(
* Simple display state, driven by [SignalStore] by default.
*/
data class DisplayState(
private val shouldShowSetUpUsername: Boolean = SignalStore.onboarding.shouldShowSetUpUsername() && SignalStore.account.isPhoneNumberless && SignalStore.account.username == null,
private val shouldShowNewGroup: Boolean = SignalStore.onboarding.shouldShowNewGroup(),
private val shouldShowInviteFriends: Boolean = SignalStore.onboarding.shouldShowInviteFriends(),
private val shouldShowAddPhoto: Boolean = SignalStore.onboarding.shouldShowAddPhoto() && !SignalStore.misc.hasEverHadAnAvatar,
private val shouldShowAppearance: Boolean = SignalStore.onboarding.shouldShowAppearance()
) {
fun hasNoVisibleContent(): Boolean = !(shouldShowNewGroup || shouldShowInviteFriends || shouldShowAddPhoto || shouldShowAppearance)
fun hasNoVisibleContent(): Boolean = !(shouldShowSetUpUsername || shouldShowNewGroup || shouldShowInviteFriends || shouldShowAddPhoto || shouldShowAppearance)
fun shouldDisplayListItem(onboardingListItem: OnboardingListItem): Boolean {
return when (onboardingListItem) {
OnboardingListItem.SET_UP_USERNAME -> shouldShowSetUpUsername
OnboardingListItem.GROUP -> shouldShowNewGroup
OnboardingListItem.INVITE -> shouldShowInviteFriends
OnboardingListItem.ADD_PHOTO -> shouldShowAddPhoto
@@ -0,0 +1,191 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.profiles.username
import android.app.Dialog
import android.os.Bundle
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ComposeDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.enableEdgeToEdge
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity
import org.signal.core.ui.R as CoreUiR
/**
* Explains usernames to accounts registered without a phone number and gives them the
* opportunity to set one up now.
*/
class ConnectWithUsernamesDialogFragment : ComposeDialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
window?.enableEdgeToEdge()
}
}
@Composable
override fun DialogContent() {
ConnectWithUsernamesDialogContent(
onSetUpUsernameClick = {
startActivity(EditProfileActivity.getIntentForUsernameEdit(requireContext()))
dismissAllowingStateLoss()
},
onNotNowClick = { dismissAllowingStateLoss() }
)
}
}
@Composable
private fun ConnectWithUsernamesDialogContent(
onSetUpUsernameClick: () -> Unit,
onNotNowClick: () -> Unit
) {
Scaffolds.Settings(
title = "",
onNavigationClick = onNotNowClick,
navigationIcon = SignalIcons.X.imageVector
) {
Column(modifier = Modifier.padding(it)) {
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
Text(
text = stringResource(R.string.ConnectWithUsernamesDialogFragment__connect_with_usernames),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
.padding(top = 4.dp, bottom = 36.dp)
)
ConnectWithUsernamesRowItem(
title = stringResource(R.string.ConnectWithUsernamesDialogFragment__usernames),
description = stringResource(R.string.ConnectWithUsernamesDialogFragment__people_can_message_you_using_your_optional_username),
image = painterResource(R.drawable.usernames_48_color)
)
ConnectWithUsernamesRowItem(
title = stringResource(R.string.ConnectWithUsernamesDialogFragment__qr_codes_and_links),
description = stringResource(R.string.ConnectWithUsernamesDialogFragment__usernames_have_a_unique_qr_code),
image = painterResource(R.drawable.qr_codes_48_color)
)
ConnectWithUsernamesRowItem(
title = stringResource(R.string.ConnectWithUsernamesDialogFragment__stay_discoverable),
description = stringResource(R.string.ConnectWithUsernamesDialogFragment__without_a_phone_number_usernames_and_links),
image = painterResource(R.drawable.discoverable_48_color)
)
}
Buttons.LargeTonal(
onClick = onSetUpUsernameClick,
modifier = Modifier
.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
.padding(top = 16.dp)
.defaultMinSize(minWidth = 221.dp)
.align(Alignment.CenterHorizontally)
) {
Text(text = stringResource(R.string.ConnectWithUsernamesDialogFragment__set_up_username))
}
TextButton(
onClick = onNotNowClick,
modifier = Modifier
.padding(
start = dimensionResource(CoreUiR.dimen.gutter),
end = dimensionResource(CoreUiR.dimen.gutter),
top = 8.dp,
bottom = 16.dp
)
.defaultMinSize(minWidth = 221.dp)
.align(Alignment.CenterHorizontally)
) {
Text(text = stringResource(R.string.ConnectWithUsernamesDialogFragment__not_now))
}
}
}
}
@Composable
private fun ConnectWithUsernamesRowItem(
title: String,
description: String,
image: Painter,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
.padding(bottom = 40.dp)
) {
Image(
painter = image,
contentDescription = null,
modifier = Modifier
.padding(start = 12.dp, top = 4.dp, end = 24.dp)
.size(48.dp)
)
Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp, end = 8.dp)
)
}
}
}
@DayNightPreviews
@Composable
private fun ConnectWithUsernamesDialogContentPreview() {
Previews.Preview {
ConnectWithUsernamesDialogContent(
onSetUpUsernameClick = {},
onNotNowClick = {}
)
}
}
@@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FFB6C5E3"
android:strokeColor="#FF61739A"
android:strokeWidth="1.5"
android:pathData="M46.11 42.33c1.04 1.04 1.04 2.73 0 3.78-1.04 1.04-2.73 1.04-3.78 0L29.73 33.51l3.78-3.78 12.6 12.6Z"/>
<path
android:fillColor="#FFC0CBE2"
android:strokeColor="#FF61739A"
android:strokeWidth="1.5"
android:pathData="M19.28 0.75a18.53 18.53 0 1 0 0 37.06 18.53 18.53 0 0 0 0-37.06Z"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19.5 4.25a15.25 15.25 0 1 0 0 30.5 15.25 15.25 0 0 0 0-30.5Z"/>
<path
android:fillColor="#FFD9E0EE"
android:strokeColor="#FF61739A"
android:strokeWidth="1.5"
android:pathData="M19.5 6.5a13 13 0 1 0 0 26 13 13 0 0 0 0-26Z"/>
</vector>
@@ -4,4 +4,5 @@
<color name="onboarding_background_2">#FFDEE5D6</color>
<color name="onboarding_background_3">#FFD6E5E5</color>
<color name="onboarding_background_4">#FFE5DBE7</color>
<color name="onboarding_background_5">#FFE5E8F8</color>
</resources>
+22
View File
@@ -1838,6 +1838,8 @@
<string name="Megaphones_invite_friends">Invite friends</string>
<string name="Megaphones_chat_colors">Chat colors</string>
<string name="Megaphones_add_a_profile_photo">Add a profile photo</string>
<!-- Text in a card view prompting the user to choose a username as part of the onboarding megaphone -->
<string name="Megaphones_set_up_username">Set up username</string>
<!-- Message body of megaphone telling users that a device was linked to their account. %1$s is the time it was linked -->
<string name="NewLinkedDeviceMegaphone__a_new_device_was_linked">A new device was linked to your account %1$s.</string>
@@ -3491,6 +3493,26 @@
<!-- Button label for continue -->
<string name="NewWaysToConnectDialogFragment__set_up_your_username">Set up username</string>
<!-- ConnectWithUsernamesDialogFragment -->
<!-- Title of the username education screen shown to accounts registered without a phone number -->
<string name="ConnectWithUsernamesDialogFragment__connect_with_usernames">Connect with usernames</string>
<!-- Title of the usernames section -->
<string name="ConnectWithUsernamesDialogFragment__usernames">Usernames</string>
<!-- Description of the usernames section -->
<string name="ConnectWithUsernamesDialogFragment__people_can_message_you_using_your_optional_username">People can message you using your optional username instead of a phone number. Usernames aren\'t visible on your profile.</string>
<!-- Title of the QR code section -->
<string name="ConnectWithUsernamesDialogFragment__qr_codes_and_links">QR codes and links</string>
<!-- Description of the QR code section -->
<string name="ConnectWithUsernamesDialogFragment__usernames_have_a_unique_qr_code">Usernames have a unique QR code and link you can share with friends to quickly start a chat with you.</string>
<!-- Title of the discoverability section -->
<string name="ConnectWithUsernamesDialogFragment__stay_discoverable">Stay discoverable</string>
<!-- Description of the discoverability section -->
<string name="ConnectWithUsernamesDialogFragment__without_a_phone_number_usernames_and_links">Without a phone number, usernames and links are the only way others can connect with you.</string>
<!-- Button label that takes the user to the username creation screen -->
<string name="ConnectWithUsernamesDialogFragment__set_up_username">Set up username</string>
<!-- Button label that dismisses the screen without creating a username -->
<string name="ConnectWithUsernamesDialogFragment__not_now">Not now</string>
<!-- ThumbnailView -->
<string name="ThumbnailView_Play_video_description">Play video</string>
<!-- Content description to describe an icon that indicates a video has captions -->