mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Add username onboarding banner in new chat screen.
This commit is contained in:
committed by
Cody Henthorne
parent
0a6e4fa152
commit
c6ff93af98
+28
-1
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+14
-2
@@ -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<NewConversationUiState> = 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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+105
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="set_up_username_banner_background">#FFE5E8F8</color>
|
||||
<color name="set_up_username_banner_button_background">#FFC2C8EE</color>
|
||||
</resources>
|
||||
@@ -3513,6 +3513,14 @@
|
||||
<!-- Button label that dismisses the screen without creating a username -->
|
||||
<string name="ConnectWithUsernamesDialogFragment__not_now">Not now</string>
|
||||
|
||||
<!-- SetUpUsernameBanner -->
|
||||
<!-- Body of a banner on the new message screen prompting the user to create a username -->
|
||||
<string name="SetUpUsernameBanner__choose_a_username_so_others_can_connect_with_you">Choose a username so others can connect with you.</string>
|
||||
<!-- Button label that takes the user to the username creation screen -->
|
||||
<string name="SetUpUsernameBanner__set_up">Set up</string>
|
||||
<!-- Accessibility label for the button that permanently dismisses the banner -->
|
||||
<string name="SetUpUsernameBanner__dismiss">Dismiss</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