mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Add username card to onboarding.
This commit is contained in:
committed by
Cody Henthorne
parent
56b4829b8b
commit
13652a74b6
@@ -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
|
||||
|
||||
+191
@@ -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>
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user