From 38d5d3ad1b79b1d95f963974836720dd0e9f2fca Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 25 Jan 2024 10:10:21 -0400 Subject: [PATCH] Add polish to usernames UX. --- .../settings/app/AppSettingsFragment.kt | 4 + .../UsernameQrCodeColorScheme.kt | 9 + .../main/UsernameLinkSettingsFragment.kt | 11 +- .../main/UsernameLinkSettingsViewModel.kt | 158 ++++++++++++------ .../main/UsernameLinkShareScreen.kt | 26 ++- .../profiles/manage/UsernameEditFragment.java | 2 + .../profiles/manage/UsernameEditViewModel.kt | 7 +- .../thoughtcrime/securesms/util/SpanUtil.java | 2 +- .../securesms/util/UsernameUtil.kt | 10 +- .../res/drawable/symbol_connections_24.xml | 20 +-- .../main/res/layout/bio_preference_item.xml | 63 ++++--- .../circular_progress_material_button.xml | 9 +- .../main/res/layout/edit_profile_fragment.xml | 4 +- .../res/layout/username_edit_fragment.xml | 10 +- app/src/main/res/values/strings.xml | 8 + .../main/java/org/signal/core/ui/Buttons.kt | 4 +- .../main/java/org/signal/core/ui/Snackbars.kt | 74 ++++++++ .../org/signal/core/ui/theme/SignalTheme.kt | 19 ++- .../signal/core/ui/theme/SnackbarColors.kt | 35 ++++ 19 files changed, 357 insertions(+), 118 deletions(-) create mode 100644 core-ui/src/main/java/org/signal/core/ui/Snackbars.kt create mode 100644 core-ui/src/main/java/org/signal/core/ui/theme/SnackbarColors.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index 71466609e8..aeddfecc21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.views.Stub +import org.thoughtcrime.securesms.util.visible class AppSettingsFragment : DSLSettingsFragment( titleId = R.string.text_secure_normal__menu_settings, @@ -340,6 +341,7 @@ class AppSettingsFragment : DSLSettingsFragment( private val aboutView: EmojiTextView = itemView.findViewById(R.id.about) private val badgeView: BadgeImageView = itemView.findViewById(R.id.badge) private val qrButton: View = itemView.findViewById(R.id.qr_button) + private val usernameView: TextView = itemView.findViewById(R.id.username) init { aboutView.setOverflowText(" ") @@ -352,6 +354,8 @@ class AppSettingsFragment : DSLSettingsFragment( titleView.text = model.recipient.profileName.toString() summaryView.text = PhoneNumberFormatter.prettyPrint(model.recipient.requireE164()) + usernameView.text = model.recipient.username.orElse("") + usernameView.visible = model.recipient.username.isPresent avatarView.setRecipient(Recipient.self()) badgeView.setBadgeFromRecipient(Recipient.self()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/UsernameQrCodeColorScheme.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/UsernameQrCodeColorScheme.kt index 41ac2988f6..aab55da528 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/UsernameQrCodeColorScheme.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/UsernameQrCodeColorScheme.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Color enum class UsernameQrCodeColorScheme( val borderColor: Color, val foregroundColor: Color, + val backgroundColor: Color, val textColor: Color = Color.White, val outlineColor: Color = Color.Transparent, private val key: String @@ -15,11 +16,13 @@ enum class UsernameQrCodeColorScheme( Blue( borderColor = Color(0xFF506ECD), foregroundColor = Color(0xFF2449C0), + backgroundColor = Color(0xFFEDF0FA), key = "blue" ), White( borderColor = Color(0xFFFFFFFF), foregroundColor = Color(0xFF000000), + backgroundColor = Color(0xFFF5F5F5), textColor = Color.Black, outlineColor = Color(0xFFE9E9E9), key = "white" @@ -27,31 +30,37 @@ enum class UsernameQrCodeColorScheme( Grey( borderColor = Color(0xFF6A6C74), foregroundColor = Color(0xFF464852), + backgroundColor = Color(0xFFF0F0F1), key = "grey" ), Tan( borderColor = Color(0xFFBBB29A), foregroundColor = Color(0xFF73694F), + backgroundColor = Color(0xFFF6F5F2), key = "tan" ), Green( borderColor = Color(0xFF97AA89), foregroundColor = Color(0xFF55733F), + backgroundColor = Color(0xFFF2F5F0), key = "green" ), Orange( borderColor = Color(0xFFDE7134), foregroundColor = Color(0xFFDA6C2E), + backgroundColor = Color(0xFFFCF1EB), key = "orange" ), Pink( borderColor = Color(0xFFEA7B9D), foregroundColor = Color(0xFFBB617B), + backgroundColor = Color(0xFFFCF1F5), key = "pink" ), Purple( borderColor = Color(0xFF9E7BE9), foregroundColor = Color(0xFF7651C5), + backgroundColor = Color(0xFFF5F3FA), key = "purple" ); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt index d843718e39..66bd4efcd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsFragment.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -56,6 +55,7 @@ import com.google.accompanist.permissions.rememberPermissionState import kotlinx.coroutines.CoroutineScope import org.signal.core.ui.Buttons import org.signal.core.ui.Dialogs +import org.signal.core.ui.Snackbars import org.signal.core.ui.theme.SignalTheme import org.signal.core.util.concurrent.LifecycleDisposable import org.thoughtcrime.securesms.R @@ -89,7 +89,9 @@ class UsernameLinkSettingsFragment : ComposeFragment() { val navController: NavController by remember { mutableStateOf(findNavController()) } var showResetDialog: Boolean by remember { mutableStateOf(false) } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - val cameraPermissionState: PermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA) + val cameraPermissionState: PermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA) { + viewModel.onTabSelected(ActiveTab.Scan) + } val linkCopiedEvent: UUID? by viewModel.linkCopiedEvent val linkCopiedString = stringResource(R.string.UsernameLinkSettings_link_copied_toast) @@ -101,7 +103,7 @@ class UsernameLinkSettingsFragment : ComposeFragment() { } Scaffold( - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + snackbarHost = { Snackbars.Host(snackbarHostState) }, topBar = { TopAppBarContent( activeTab = state.activeTab, @@ -123,6 +125,7 @@ class UsernameLinkSettingsFragment : ComposeFragment() { enter = slideInHorizontally(initialOffsetX = { fullWidth -> -fullWidth }), exit = slideOutHorizontally(targetOffsetX = { fullWidth -> -fullWidth }) ) { + val helpText = stringResource(id = R.string.UsernameLinkSettings_scan_this_qr_code) UsernameLinkShareScreen( state = state, snackbarHostState = snackbarHostState, @@ -130,7 +133,7 @@ class UsernameLinkSettingsFragment : ComposeFragment() { modifier = Modifier.padding(contentPadding), navController = navController, onShareBadge = { - shareQrBadge(viewModel.generateQrCodeImage()) + shareQrBadge(viewModel.generateQrCodeImage(helpText)) }, onResetClicked = { showResetDialog = true }, onLinkResultHandled = { viewModel.onUsernameLinkResetResultHandled() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt index 2dad371ae9..503fac70e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt @@ -10,6 +10,9 @@ import android.graphics.Rect import android.graphics.RectF import android.graphics.Typeface import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf @@ -17,8 +20,10 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.withSave import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import androidx.core.graphics.withTranslation import androidx.lifecycle.ViewModel import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single @@ -122,7 +127,9 @@ class UsernameLinkSettingsViewModel : ViewModel() { val components: Optional = when (result) { is UsernameLinkResetResult.Success -> Optional.of(result.components) is UsernameLinkResetResult.NetworkError -> Optional.empty() - else -> { usernameLink.value ?: Optional.empty() } + else -> { + usernameLink.value ?: Optional.empty() + } } _state.value = _state.value.copy( @@ -200,7 +207,7 @@ class UsernameLinkSettingsViewModel : ViewModel() { * * I hate this as much as you do. */ - fun generateQrCodeImage(): Bitmap? { + fun generateQrCodeImage(helpText: String): Bitmap? { val state: UsernameLinkSettingsState = _state.value if (state.qrCodeState !is QrCodeState.Present) { @@ -210,12 +217,16 @@ class UsernameLinkSettingsViewModel : ViewModel() { val qrCodeData: QrCodeData = state.qrCodeState.data - val width = 480 - val height = 525 - val qrSize = 300f - val qrPadding = 25f - val borderSizeX = 64f - val borderSizeY = 52f + val width = 424 + val height = 576 + val backgroundPadHorizontal = 64f + val backgroundPadVertical = 80f + val qrBorderWidth = width - (backgroundPadHorizontal * 2) + val qrBorderHeight = 324f + val qrSize = 184f + val qrPadding = 16f + val borderSizeX = 40f + val borderSizeY = 32f val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.TRANSPARENT) @@ -225,56 +236,70 @@ class UsernameLinkSettingsViewModel : ViewModel() { val composeCanvas = Canvas(androidCanvas) val canvasDrawScope = CanvasDrawScope() - // Draw the background - androidCanvas.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), 30f, 30f, Paint().apply { color = state.qrCodeColorScheme.borderColor.toArgb() }) - androidCanvas.drawRoundRect(borderSizeX, borderSizeY, borderSizeX + qrSize + qrPadding * 2, borderSizeY + qrSize + qrPadding * 2, 15f, 15f, Paint().apply { color = Color.WHITE }) - androidCanvas.drawRoundRect( - borderSizeX, - borderSizeY, - borderSizeX + qrSize + qrPadding * 2, - borderSizeY + qrSize + qrPadding * 2, - 15f, - 15f, - Paint().apply { - color = state.qrCodeColorScheme.outlineColor.toArgb() - style = Paint.Style.STROKE - strokeWidth = 4f - } - ) + // Background + androidCanvas.drawColor(state.qrCodeColorScheme.backgroundColor.toArgb()) - // Draw the QR code - composeCanvas.translate((width / 2) - (qrSize / 2), 80f) - canvasDrawScope.draw( - density = object : Density { - override val density: Float = 1f - override val fontScale: Float = 1f - }, - layoutDirection = LayoutDirection.Ltr, - canvas = composeCanvas, - size = Size(qrSize, qrSize) - ) { - drawQr( - data = qrCodeData, - foregroundColor = state.qrCodeColorScheme.foregroundColor, - backgroundColor = state.qrCodeColorScheme.borderColor, - deadzonePercent = 0.35f, - logo = null + // QR Border + androidCanvas.withTranslation(x = backgroundPadHorizontal, y = backgroundPadVertical) { + drawRoundRect(0f, 0f, qrBorderWidth, qrBorderHeight, 30f, 30f, Paint().apply { color = state.qrCodeColorScheme.borderColor.toArgb() }) + + drawRoundRect(borderSizeX, borderSizeY, borderSizeX + qrSize + qrPadding * 2, borderSizeY + qrSize + qrPadding * 2, 15f, 15f, Paint().apply { color = Color.WHITE }) + drawRoundRect( + borderSizeX, + borderSizeY, + borderSizeX + qrSize + qrPadding * 2, + borderSizeY + qrSize + qrPadding * 2, + 15f, + 15f, + Paint().apply { + color = state.qrCodeColorScheme.outlineColor.toArgb() + style = Paint.Style.STROKE + strokeWidth = 4f + } ) - } - composeCanvas.translate(-90f, -80f) - // Draw the signal logo -- unfortunately can't have the normal QR code drawing handle it because it requires a composable ImageBitmap - BitmapFactory.decodeResource(ApplicationDependencies.getApplication().resources, R.drawable.qrcode_logo).also { logoBitmap -> - val tintedPaint = Paint().apply { - colorFilter = PorterDuffColorFilter(state.qrCodeColorScheme.foregroundColor.toArgb(), PorterDuff.Mode.SRC_IN) + // Draw the QR code + composeCanvas.translate((qrBorderWidth / 2) - (qrSize / 2), borderSizeY + qrPadding) + + composeCanvas.withSave { + composeCanvas.scale(qrSize / 300f, qrSize / 300f) + canvasDrawScope.draw( + density = object : Density { + override val density: Float = 1f + override val fontScale: Float = 1f + }, + layoutDirection = LayoutDirection.Ltr, + canvas = composeCanvas, + size = Size(300f, 300f) + ) { + drawQr( + data = qrCodeData, + foregroundColor = state.qrCodeColorScheme.foregroundColor, + backgroundColor = state.qrCodeColorScheme.borderColor, + deadzonePercent = 0.35f, + logo = null + ) + } + } + + composeCanvas.translate(-90f, -80f) + + // Draw the signal logo -- unfortunately can't have the normal QR code drawing handle it because it requires a composable ImageBitmap + BitmapFactory.decodeResource(ApplicationDependencies.getApplication().resources, R.drawable.qrcode_logo).also { logoBitmap -> + val tintedPaint = Paint().apply { + colorFilter = PorterDuffColorFilter(state.qrCodeColorScheme.foregroundColor.toArgb(), PorterDuff.Mode.SRC_IN) + } + val sourceRect = Rect(0, 0, logoBitmap.width, logoBitmap.height) + + val destLeft = qrBorderWidth / 2f + qrPadding + val destTop = destLeft - 10f + val destRect = RectF(destLeft, destTop, destLeft + 36f, destTop + 36f) + drawBitmap(logoBitmap, sourceRect, destRect, tintedPaint) } - val sourceRect = Rect(0, 0, logoBitmap.width, logoBitmap.height) - val destRect = RectF(210f, 200f, 270f, 260f) - androidCanvas.drawBitmap(logoBitmap, sourceRect, destRect, tintedPaint) } - // Draw the text - val textPaint = Paint().apply { + // Draw the username + val usernamePaint = Paint().apply { color = state.qrCodeColorScheme.textColor.toArgb() textSize = 34f typeface = if (Build.VERSION.SDK_INT < 26) { @@ -286,10 +311,33 @@ class UsernameLinkSettingsViewModel : ViewModel() { .build() } } - val textBounds = Rect() - textPaint.getTextBounds(state.username, 0, state.username.length, textBounds) + val usernameBounds = Rect() + usernamePaint.getTextBounds(state.username, 0, state.username.length, usernameBounds) - androidCanvas.drawText(state.username, (width / 2f) - (textBounds.width() / 2f), 465f, textPaint) + androidCanvas.drawText(state.username, (width / 2f) - (usernameBounds.width() / 2f), 348f + usernameBounds.height(), usernamePaint) + + // Draw the help text + val helpTextPaint = TextPaint().apply { + isAntiAlias = true + color = 0xFF3C3C43.toInt() + textSize = 14f + typeface = if (Build.VERSION.SDK_INT < 26) { + Typeface.DEFAULT + } else { + Typeface.Builder("") + .setFallback("sans-serif") + .setWeight(400) + .build() + } + } + + val helpTextHorizontalPad = 72 + val maxWidth = width - helpTextHorizontalPad * 2 + val helpTextLayout = StaticLayout(helpText, helpTextPaint, maxWidth, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true) + + androidCanvas.withTranslation(x = helpTextHorizontalPad.toFloat(), y = 444f) { + helpTextLayout.draw(androidCanvas) + } return bitmap } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkShareScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkShareScreen.kt index 903b114239..e27bb57846 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkShareScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkShareScreen.kt @@ -93,21 +93,18 @@ fun UsernameLinkShareScreen( ButtonBar( onShareClicked = onShareBadge, - onColorClicked = { navController.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkQrColorPickerFragment()) } - ) - - LinkRow( - linkState = state.usernameLinkState, - onClick = { + onColorClicked = { navController.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkQrColorPickerFragment()) }, + onLinkClicked = { navController.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkShareBottomSheet()) - } + }, + linkState = state.usernameLinkState ) Text( text = stringResource(id = R.string.UsernameLinkSettings_qr_description), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 19.dp, start = 43.dp, end = 43.dp), + modifier = Modifier.padding(top = 42.dp, bottom = 19.dp, start = 43.dp, end = 43.dp), color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -127,11 +124,22 @@ fun UsernameLinkShareScreen( } @Composable -private fun ButtonBar(onShareClicked: () -> Unit, onColorClicked: () -> Unit) { +private fun ButtonBar( + linkState: UsernameLinkState, + onLinkClicked: () -> Unit, + onShareClicked: () -> Unit, + onColorClicked: () -> Unit +) { Row( horizontalArrangement = Arrangement.spacedBy(space = 32.dp, alignment = Alignment.CenterHorizontally), modifier = Modifier.fillMaxWidth() ) { + Buttons.ActionButton( + enabled = linkState is UsernameLinkState.Present, + onClick = onLinkClicked, + iconResId = R.drawable.symbol_link_24, + labelResId = R.string.UsernameLinkSettings_link_button_label + ) Buttons.ActionButton( onClick = onShareClicked, iconResId = R.drawable.symbol_share_android_24, diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java index 8e6e8b0654..0fab2e03b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java @@ -194,6 +194,8 @@ public class UsernameEditFragment extends LoggingFragment { case DISCRIMINATOR_HAS_INVALID_CHARACTERS, DISCRIMINATOR_NOT_AVAILABLE -> getString(R.string.UsernameEditFragment__this_username_is_not_available_try_another_number); case DISCRIMINATOR_TOO_LONG -> getString(R.string.UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits, UsernameUtil.MAX_DISCRIMINATOR_LENGTH); case DISCRIMINATOR_TOO_SHORT -> getString(R.string.UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits, UsernameUtil.MIN_DISCRIMINATOR_LENGTH); + case DISCRIMINATOR_CANNOT_BE_00 -> getString(R.string.UsernameEditFragment__this_number_cant_be_00); + case DISCRIMINATOR_CANNOT_START_WITH_00 -> getString(R.string.UsernameEditFragment__this_number_cant_start_with_00); }; int colorRes = error != null ? R.color.signal_colorError : R.color.signal_colorPrimary; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt index 88b0cabc2a..7af746d415 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt @@ -355,7 +355,9 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern DISCRIMINATOR_NOT_AVAILABLE, DISCRIMINATOR_TOO_SHORT, DISCRIMINATOR_TOO_LONG, - DISCRIMINATOR_HAS_INVALID_CHARACTERS + DISCRIMINATOR_HAS_INVALID_CHARACTERS, + DISCRIMINATOR_CANNOT_BE_00, + DISCRIMINATOR_CANNOT_START_WITH_00 } enum class ButtonState { @@ -383,6 +385,7 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG InvalidReason.STARTS_WITH_NUMBER -> UsernameStatus.CANNOT_START_WITH_NUMBER InvalidReason.INVALID_CHARACTERS -> UsernameStatus.INVALID_CHARACTERS + InvalidReason.INVALID_NUMBER, InvalidReason.INVALID_NUMBER_PREFIX -> error("Unexpected reason $invalidReason") } } @@ -391,6 +394,8 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern InvalidReason.TOO_SHORT -> UsernameStatus.DISCRIMINATOR_TOO_SHORT InvalidReason.TOO_LONG -> UsernameStatus.DISCRIMINATOR_TOO_LONG InvalidReason.INVALID_CHARACTERS -> UsernameStatus.DISCRIMINATOR_HAS_INVALID_CHARACTERS + InvalidReason.INVALID_NUMBER -> UsernameStatus.DISCRIMINATOR_CANNOT_BE_00 + InvalidReason.INVALID_NUMBER_PREFIX -> UsernameStatus.DISCRIMINATOR_CANNOT_START_WITH_00 else -> UsernameStatus.INVALID_GENERIC } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java index a62e3a3da8..d0c63c7798 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java @@ -149,7 +149,7 @@ public final class SpanUtil { public static void appendBottomImageSpan(@NonNull SpannableStringBuilder builder, @NonNull Drawable drawable, int width, int height) { drawable.setBounds(0, 0, ViewUtil.dpToPx(width), ViewUtil.dpToPx(height)); - builder.append(" ").append(SpanUtil.buildImageSpan(drawable, DynamicDrawableSpan.ALIGN_BOTTOM)); + builder.append(SpanUtil.buildImageSpan(drawable, DynamicDrawableSpan.ALIGN_BOTTOM)); } public static CharSequence learnMore(@NonNull Context context, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt index 0777040305..c391e5bca0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt @@ -64,6 +64,12 @@ object UsernameUtil { value == null -> { null } + value == "00" -> { + InvalidReason.INVALID_NUMBER + } + value.startsWith("00") -> { + InvalidReason.INVALID_NUMBER_PREFIX + } value.length < MIN_DISCRIMINATOR_LENGTH -> { InvalidReason.TOO_SHORT } @@ -83,6 +89,8 @@ object UsernameUtil { TOO_SHORT, TOO_LONG, INVALID_CHARACTERS, - STARTS_WITH_NUMBER + STARTS_WITH_NUMBER, + INVALID_NUMBER, + INVALID_NUMBER_PREFIX } } diff --git a/app/src/main/res/drawable/symbol_connections_24.xml b/app/src/main/res/drawable/symbol_connections_24.xml index fb4e987dd8..5103337dcc 100644 --- a/app/src/main/res/drawable/symbol_connections_24.xml +++ b/app/src/main/res/drawable/symbol_connections_24.xml @@ -1,24 +1,24 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M7.95 1.25C5.88 1.25 4.2 2.93 4.2 5c0 2.07 1.68 3.75 3.75 3.75 2.07 0 3.75-1.68 3.75-3.75 0-2.07-1.68-3.75-3.75-3.75ZM5.95 5c0-1.1 0.9-2 2-2s2 0.9 2 2-0.9 2-2 2-2-0.9-2-2Z"/> + android:pathData="M7.95 15.25c-2.07 0-3.75 1.68-3.75 3.75 0 2.07 1.68 3.75 3.75 3.75 2.07 0 3.75-1.68 3.75-3.75 0-2.07-1.68-3.75-3.75-3.75Zm-2 3.75c0-1.1 0.9-2 2-2s2 0.9 2 2-0.9 2-2 2-2-0.9-2-2Z"/> + android:pathData="M12.3 5c0-2.07 1.68-3.75 3.75-3.75 2.07 0 3.75 1.68 3.75 3.75 0 2.07-1.68 3.75-3.75 3.75-2.07 0-3.75-1.68-3.75-3.75Zm3.75-2c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2Z"/> + android:pathData="M16.05 15.25c-2.07 0-3.75 1.68-3.75 3.75 0 2.07 1.68 3.75 3.75 3.75 2.07 0 3.75-1.68 3.75-3.75 0-2.07-1.68-3.75-3.75-3.75Zm-2 3.75c0-1.1 0.9-2 2-2s2 0.9 2 2-0.9 2-2 2-2-0.9-2-2Z"/> + android:pathData="M16.38 12c0-2.07 1.67-3.75 3.75-3.75 2.07 0 3.75 1.68 3.75 3.75 0 2.07-1.68 3.75-3.75 3.75-2.08 0-3.75-1.68-3.75-3.75Zm3.75-2c-1.11 0-2 0.9-2 2s0.89 2 2 2c1.1 0 2-0.9 2-2s-0.9-2-2-2Z"/> + android:pathData="M3.88 8.25C1.8 8.25 0.13 9.93 0.13 12c0 2.07 1.67 3.75 3.75 3.75 2.07 0 3.75-1.68 3.75-3.75 0-2.07-1.68-3.75-3.75-3.75Zm-2 3.75c0-1.1 0.89-2 2-2 1.1 0 2 0.9 2 2s-0.9 2-2 2c-1.11 0-2-0.9-2-2Z"/> diff --git a/app/src/main/res/layout/bio_preference_item.xml b/app/src/main/res/layout/bio_preference_item.xml index ef3c36d31f..56a0a6c0d3 100644 --- a/app/src/main/res/layout/bio_preference_item.xml +++ b/app/src/main/res/layout/bio_preference_item.xml @@ -1,20 +1,20 @@ + android:minHeight="56dp" + tools:viewBindingIgnore="true"> - - + + + + + app:iconTint="@color/signal_colorOnSurface" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/circular_progress_material_button.xml b/app/src/main/res/layout/circular_progress_material_button.xml index 2886e7a34b..03964d71a7 100644 --- a/app/src/main/res/layout/circular_progress_material_button.xml +++ b/app/src/main/res/layout/circular_progress_material_button.xml @@ -1,16 +1,17 @@ + xmlns:tools="http://schemas.android.com/tools" + tools:parentTag="org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton" + tools:viewBindingIgnore="true"> + android:layout_gravity="center_horizontal" + app:indicatorInset="2dp" /> diff --git a/app/src/main/res/layout/username_edit_fragment.xml b/app/src/main/res/layout/username_edit_fragment.xml index bc4f5b0a04..cb179684ec 100644 --- a/app/src/main/res/layout/username_edit_fragment.xml +++ b/app/src/main/res/layout/username_edit_fragment.xml @@ -66,7 +66,7 @@ Invalid username, enter a minimum of %1$d digits. Invalid username, enter a maximum of %1$d digits. + + This number can\'t be 00. Enter a digit between 1–9 + + This number can\'t start with 00. Enter a digit between 1–9 Recovering your username will reset your existing QR code and link. Are you sure? @@ -6350,6 +6354,8 @@ This link will no longer work for anyone who as it. + + Link Share @@ -6394,6 +6400,8 @@ You do not have network access. Your link was not reset. Try again later. A network error occurred while trying to reset your link. Try again later. + + Scan this QR code with your phone to chat with me on Signal. Anyone with this link can view your username and start a chat with you. Only share it with people you trust. diff --git a/core-ui/src/main/java/org/signal/core/ui/Buttons.kt b/core-ui/src/main/java/org/signal/core/ui/Buttons.kt index 380e09d795..e101e0f0be 100644 --- a/core-ui/src/main/java/org/signal/core/ui/Buttons.kt +++ b/core-ui/src/main/java/org/signal/core/ui/Buttons.kt @@ -177,9 +177,11 @@ object Buttons { onClick: () -> Unit, @DrawableRes iconResId: Int, @StringRes labelResId: Int, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + enabled: Boolean = true ) { ActionButton( + enabled = enabled, onClick = onClick, label = stringResource(labelResId), modifier = modifier diff --git a/core-ui/src/main/java/org/signal/core/ui/Snackbars.kt b/core-ui/src/main/java/org/signal/core/ui/Snackbars.kt new file mode 100644 index 0000000000..ddc32dc21f --- /dev/null +++ b/core-ui/src/main/java/org/signal/core/ui/Snackbars.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.ui + +import android.content.res.Configuration +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import org.signal.core.ui.theme.LocalSnackbarColors +import org.signal.core.ui.theme.SignalTheme + +/** + * Properly themed Snackbars. Since these use internal color state, we need to + * use a local provider to pass the properly themed colors around. These composables + * allow for quick and easy access to the proper theming for snackbars. + */ +object Snackbars { + @Composable + fun Host(snackbarHostState: SnackbarHostState) { + SnackbarHost(hostState = snackbarHostState) { + Default(snackbarData = it) + } + } + + @Composable + internal fun Default(snackbarData: SnackbarData) { + val colors = LocalSnackbarColors.current + Snackbar( + snackbarData = snackbarData, + containerColor = colors.color, + contentColor = colors.contentColor, + actionColor = colors.actionColor, + actionContentColor = colors.actionContentColor, + dismissActionContentColor = colors.dismissActionContentColor + ) + } +} + +@Preview +@Composable +private fun SnackbarLightPreview() { + SignalTheme { + Snackbars.Default(snackbarData = SampleSnackbarData) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SnackbarDarkPreview() { + SignalTheme { + Snackbars.Default(snackbarData = SampleSnackbarData) + } +} + +private object SampleSnackbarData : SnackbarData { + override val visuals = object : SnackbarVisuals { + override val actionLabel: String = "Action Label" + override val duration: SnackbarDuration = SnackbarDuration.Short + override val message: String = "Message" + override val withDismissAction: Boolean = true + } + + override fun dismiss() = Unit + + override fun performAction() = Unit +} diff --git a/core-ui/src/main/java/org/signal/core/ui/theme/SignalTheme.kt b/core-ui/src/main/java/org/signal/core/ui/theme/SignalTheme.kt index 1d0340993a..bd3615c312 100644 --- a/core-ui/src/main/java/org/signal/core/ui/theme/SignalTheme.kt +++ b/core-ui/src/main/java/org/signal/core/ui/theme/SignalTheme.kt @@ -168,14 +168,31 @@ private val darkColorScheme = darkColorScheme( outline = Color(0xFF5C5E65) ) +private val lightSnackbarColors = SnackbarColors( + color = darkColorScheme.surface, + contentColor = darkColorScheme.onSurface, + actionColor = darkColorScheme.primary, + actionContentColor = darkColorScheme.primary, + dismissActionContentColor = darkColorScheme.onSurface +) + +private val darkSnackbarColors = SnackbarColors( + color = darkColorScheme.surfaceVariant, + contentColor = darkColorScheme.onSurfaceVariant, + actionColor = darkColorScheme.primary, + actionContentColor = darkColorScheme.primary, + dismissActionContentColor = darkColorScheme.onSurfaceVariant +) + @Composable fun SignalTheme( isDarkMode: Boolean = LocalContext.current.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES, content: @Composable () -> Unit ) { val extendedColors = if (isDarkMode) darkExtendedColors else lightExtendedColors + val snackbarColors = if (isDarkMode) darkSnackbarColors else lightSnackbarColors - CompositionLocalProvider(LocalExtendedColors provides extendedColors) { + CompositionLocalProvider(LocalExtendedColors provides extendedColors, LocalSnackbarColors provides snackbarColors) { MaterialTheme( colorScheme = if (isDarkMode) darkColorScheme else lightColorScheme, typography = typography, diff --git a/core-ui/src/main/java/org/signal/core/ui/theme/SnackbarColors.kt b/core-ui/src/main/java/org/signal/core/ui/theme/SnackbarColors.kt new file mode 100644 index 0000000000..f0e6087607 --- /dev/null +++ b/core-ui/src/main/java/org/signal/core/ui/theme/SnackbarColors.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +/** + * Borrowed from [androidx.compose.material3.Snackbar] + * + * Works in conjunction with [org.signal.core.ui.Snackbars] for properly + * themed snackbars in light and dark modes. + */ +@Immutable +data class SnackbarColors( + val color: Color, + val contentColor: Color, + val actionColor: Color, + val actionContentColor: Color, + val dismissActionContentColor: Color +) + +val LocalSnackbarColors = staticCompositionLocalOf { + SnackbarColors( + color = Color.Unspecified, + contentColor = Color.Unspecified, + actionColor = Color.Unspecified, + actionContentColor = Color.Unspecified, + dismissActionContentColor = Color.Unspecified + ) +}