Add polish to usernames UX.

This commit is contained in:
Alex Hart
2024-01-25 10:10:21 -04:00
committed by Nicholas Tinsley
parent ec96b4e3aa
commit 38d5d3ad1b
19 changed files with 357 additions and 118 deletions

View File

@@ -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())

View File

@@ -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"
);

View File

@@ -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() }

View File

@@ -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<UsernameLinkComponents> = 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
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -1,24 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M13.5 2.5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6Zm-4 6c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4-4-1.8-4-4Z"/>
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"/>
<path
android:fillColor="#FF000000"
android:pathData="M13.5 25.5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6Zm-4 6c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4-4-1.8-4-4Z"/>
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"/>
<path
android:fillColor="#FF000000"
android:pathData="M20.5 8.5c0-3.31 2.69-6 6-6s6 2.69 6 6-2.69 6-6 6-6-2.69-6-6Zm6-4c-2.2 0-4 1.8-4 4s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z"/>
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"/>
<path
android:fillColor="#FF000000"
android:pathData="M26.5 25.5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6Zm-4 6c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4-4-1.8-4-4Z"/>
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"/>
<path
android:fillColor="#FF000000"
android:pathData="M27 20c0-3.31 2.69-6 6-6s6 2.69 6 6-2.69 6-6 6-6-2.69-6-6Zm6-4c-2.2 0-4 1.8-4 4s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z"/>
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"/>
<path
android:fillColor="#FF000000"
android:pathData="M7 14c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6Zm-4 6c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4-4-1.8-4-4Z"/>
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"/>
</vector>

View File

@@ -1,20 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:viewBindingIgnore="true"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/dsl_preference_item_background"
android:minHeight="56dp">
android:minHeight="56dp"
tools:viewBindingIgnore="true">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -24,8 +24,8 @@
android:id="@+id/badge"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="40dp"
android:layout_marginTop="40dp"
android:layout_marginStart="56dp"
android:layout_marginTop="56dp"
android:contentDescription="@string/ImageView__badge"
app:badge_size="medium"
app:layout_constraintStart_toStartOf="@id/icon"
@@ -35,7 +35,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="14dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/qr_button"
@@ -51,17 +53,6 @@
android:textAppearance="@style/Signal.Text.TitleLarge"
tools:text="Peter Parker" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/about"
style="@style/Signal.Text.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:textColor="@color/signal_colorOnSurfaceVariant"
tools:text="Crusin' the web" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/summary"
style="@style/Signal.Text.BodyMedium"
@@ -71,21 +62,43 @@
android:textColor="@color/signal_colorOnSurfaceVariant"
tools:text="+1 (999) 555-1234" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/username"
style="@style/Signal.Text.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:visibility="gone"
tools:text="miles.07" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/about"
style="@style/Signal.Text.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:textColor="@color/signal_colorOnSurfaceVariant"
tools:text="Crusin' the web" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/qr_button"
style="@style/Widget.Signal.Button.Icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:backgroundTint="@color/signal_colorSurface4"
app:icon="@drawable/symbol_qrcode_24"
app:iconSize="20dp"
app:iconTint="@color/core_black"
app:backgroundTint="@color/signal_light_colorSurface3"
style="@style/Widget.Signal.Button.Icon"/>
app:iconTint="@color/signal_colorOnSurface"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:parentTag="org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton">
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton"
tools:viewBindingIgnore="true">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_indicator"
style="?circularProgressIndicatorStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
android:layout_gravity="center_horizontal"
app:indicatorInset="2dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button"

View File

@@ -93,9 +93,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="14dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:text="@string/ManageProfileFragment__edit_photo"
android:textColor="@color/signal_colorOnSurface"
app:backgroundTint="@color/signal_colorSurface4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/manage_profile_avatar_background" />

View File

@@ -66,7 +66,7 @@
<EditText
android:id="@+id/username_text"
style="@style/Signal.Text.Body"
style="@style/Signal.Text.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/UsernameEditFragment_username"
@@ -83,7 +83,7 @@
<EditText
android:id="@+id/discriminator_text"
style="@style/Signal.Text.Body"
style="@style/Signal.Text.BodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
@@ -105,8 +105,8 @@
android:id="@+id/suffix_progress"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginBottom="10dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="10dp"
app:indicatorColor="@color/signal_colorOnSurfaceVariant"
app:indicatorSize="16dp"
app:layout_constraintBottom_toBottomOf="@id/username_text_wrapper"
@@ -134,11 +134,11 @@
<TextView
android:id="@+id/username_error"
style="@style/Signal.Text.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="4dp"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorError"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/username_description"
@@ -164,6 +164,7 @@
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/username_description"
style="@style/Signal.Text.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
@@ -171,7 +172,6 @@
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:layout_marginBottom="24dp"
android:text="@string/UsernameEditFragment__usernames_let_others_message"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toTopOf="@id/username_button_barrier"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -2243,6 +2243,10 @@
<string name="UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits">Invalid username, enter a minimum of %1$d digits.</string>
<!-- Displayed when the chosen discriminator is too long -->
<string name="UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits">Invalid username, enter a maximum of %1$d digits.</string>
<!-- Displayed when the chosen discriminator is 00 -->
<string name="UsernameEditFragment__this_number_cant_be_00">This number can\'t be 00. Enter a digit between 19</string>
<!-- Displayed when the chosen discriminator starts with 00 -->
<string name="UsernameEditFragment__this_number_cant_start_with_00">This number can\'t start with 00. Enter a digit between 19</string>
<!-- The body of an alert dialog asking the user to confirm that they want to recover their username -->
<string name="UsernameEditFragment_recovery_dialog_confirmation">Recovering your username will reset your existing QR code and link. Are you sure?</string>
@@ -6350,6 +6354,8 @@
<!-- Displayed as body in dialog when user attempts to delete the link -->
<string name="CallLinkDetailsFragment__this_link_will_no_longer_work">This link will no longer work for anyone who as it.</string>
<!-- Button label for the link button in the username link settings -->
<string name="UsernameLinkSettings_link_button_label">Link</string>
<!-- Button label for the share button in the username link settings -->
<string name="UsernameLinkSettings_share_button_label">Share</string>
<!-- Button label for the color selector button in the username link settings -->
@@ -6394,6 +6400,8 @@
<string name="UsernameLinkSettings_reset_link_result_network_unavailable">You do not have network access. Your link was not reset. Try again later.</string>
<!-- Body of a dialog that is displayed when we failed to reset your username link because of a transient network issue. -->
<string name="UsernameLinkSettings_reset_link_result_network_error">A network error occurred while trying to reset your link. Try again later.</string>
<!-- Shown on the generated username qr code image to explain how to use it. -->
<string name="UsernameLinkSettings_scan_this_qr_code">Scan this QR code with your phone to chat with me on Signal.</string>
<!-- Explanatory text at the top of a bottom sheet describing how username links work -->
<string name="UsernameLinkShareBottomSheet_title">Anyone with this link can view your username and start a chat with you. Only share it with people you trust.</string>

View File

@@ -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

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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
)
}