mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Update to the new username link spec.
This commit is contained in:
@@ -26,7 +26,7 @@ class UsernameOutOfSyncReminder : Reminder(R.string.UsernameOutOfSyncReminder__s
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun isEligible(): Boolean {
|
||||
return FeatureFlags.usernames() && SignalStore.phoneNumberPrivacy().isUsernameOutOfSync
|
||||
return FeatureFlags.usernames() && SignalStore.account().usernameOutOfSync
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||
},
|
||||
onQrButtonClicked = {
|
||||
if (Recipient.self().username.isPresent && Recipient.self().username.get().isNotEmpty()) {
|
||||
if (SignalStore.account().username != null) {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
|
||||
} else {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameEducationFragment)
|
||||
|
||||
@@ -60,7 +60,7 @@ private fun DrawScope.drawQr(
|
||||
deadzonePercent: Float,
|
||||
logo: ImageBitmap
|
||||
) {
|
||||
val deadzonePaddingPercent = 0.07f
|
||||
val deadzonePaddingPercent = 0.045f
|
||||
|
||||
// We want an even number of dots on either side of the deadzone
|
||||
val deadzoneRadius: Int = (data.height * (deadzonePercent + deadzonePaddingPercent)).toInt().let { candidateDeadzoneHeight ->
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -20,16 +29,22 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ScreenshotController
|
||||
import org.thoughtcrime.securesms.compose.getScreenshotBounds
|
||||
|
||||
@@ -37,19 +52,25 @@ import org.thoughtcrime.securesms.compose.getScreenshotBounds
|
||||
* Renders a QR code and username as a badge.
|
||||
*/
|
||||
@Composable
|
||||
fun QrCodeBadge(data: QrCodeData?, colorScheme: UsernameQrCodeColorScheme, username: String, modifier: Modifier = Modifier, screenshotController: ScreenshotController? = null) {
|
||||
val borderColor by animateColorAsState(targetValue = colorScheme.borderColor)
|
||||
val foregroundColor by animateColorAsState(targetValue = colorScheme.foregroundColor)
|
||||
val elevation by animateFloatAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) 10f else 0f)
|
||||
val textColor by animateColorAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) Color.Black else Color.White)
|
||||
fun QrCodeBadge(
|
||||
data: QrCodeState,
|
||||
colorScheme: UsernameQrCodeColorScheme,
|
||||
username: String,
|
||||
modifier: Modifier = Modifier,
|
||||
screenshotController: ScreenshotController? = null,
|
||||
usernameCopyable: Boolean = false,
|
||||
onClick: (() -> Unit) = {}
|
||||
) {
|
||||
val borderColor by animateColorAsState(targetValue = colorScheme.borderColor, label = "border")
|
||||
val foregroundColor by animateColorAsState(targetValue = colorScheme.foregroundColor, label = "foreground")
|
||||
val elevation by animateFloatAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) 10f else 0f, label = "elevation")
|
||||
val textColor by animateColorAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) Color.Black else Color.White, label = "textColor")
|
||||
var badgeBounds by remember {
|
||||
mutableStateOf<Rect?>(null)
|
||||
}
|
||||
screenshotController?.bind(LocalView.current, badgeBounds)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 59.dp, vertical = 24.dp)
|
||||
.onGloballyPositioned {
|
||||
badgeBounds = it.getScreenshotBounds()
|
||||
},
|
||||
@@ -57,24 +78,32 @@ fun QrCodeBadge(data: QrCodeData?, colorScheme: UsernameQrCodeColorScheme, usern
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
shadowElevation = elevation.dp
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.width(296.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = 32.dp,
|
||||
start = 40.dp,
|
||||
end = 40.dp,
|
||||
bottom = 16.dp
|
||||
end = 40.dp
|
||||
)
|
||||
.aspectRatio(1f)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White
|
||||
) {
|
||||
if (data != null) {
|
||||
if (data is QrCodeState.Present) {
|
||||
QrCode(
|
||||
data = data,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
data = data.data,
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = if (colorScheme == UsernameQrCodeColorScheme.White) 2.dp else 0.dp,
|
||||
color = Color(0xFFE9E9E9),
|
||||
shape = RoundedCornerShape(size = 12.dp)
|
||||
)
|
||||
.padding(16.dp),
|
||||
foregroundColor = foregroundColor,
|
||||
backgroundColor = Color.White
|
||||
)
|
||||
@@ -85,40 +114,169 @@ fun QrCodeBadge(data: QrCodeData?, colorScheme: UsernameQrCodeColorScheme, usern
|
||||
.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = colorScheme.borderColor,
|
||||
modifier = Modifier.size(56.dp)
|
||||
)
|
||||
if (data is QrCodeState.Loading) {
|
||||
CircularProgressIndicator(
|
||||
color = colorScheme.borderColor,
|
||||
modifier = Modifier.size(56.dp)
|
||||
)
|
||||
} else if (data is QrCodeState.NotSet) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.symbol_error_circle_24),
|
||||
contentDescription = stringResource(id = R.string.UsernameLinkSettings_link_not_set_label),
|
||||
colorFilter = ColorFilter.tint(colorResource(R.color.core_grey_25)),
|
||||
modifier = Modifier
|
||||
.width(28.dp)
|
||||
.height(28.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = username,
|
||||
color = textColor,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 26.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 40.dp,
|
||||
end = 40.dp,
|
||||
bottom = 32.dp
|
||||
start = 32.dp,
|
||||
end = 32.dp,
|
||||
top = 8.dp,
|
||||
bottom = 28.dp
|
||||
)
|
||||
)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(
|
||||
enabled = usernameCopyable,
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
if (usernameCopyable) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.symbol_copy_android_24),
|
||||
contentDescription = null,
|
||||
colorFilter = if (colorScheme == UsernameQrCodeColorScheme.White) {
|
||||
ColorFilter.tint(Color.Black)
|
||||
} else {
|
||||
ColorFilter.tint(Color.White)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = username,
|
||||
color = textColor,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 26.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(start = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(name = "Light Theme", group = "ShortName", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "ShortName", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewWithCode() {
|
||||
private fun PreviewWithCodeShort() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
Column {
|
||||
QrCodeBadge(
|
||||
data = QrCodeState.Present(QrCodeData.forData("https://signal.org", 64)),
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "parker.42",
|
||||
usernameCopyable = false
|
||||
)
|
||||
QrCodeBadge(
|
||||
data = QrCodeState.Present(QrCodeData.forData("https://signal.org", 64)),
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "parker.42",
|
||||
usernameCopyable = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = "LongName")
|
||||
@Composable
|
||||
private fun PreviewWithCodeLong() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
Column {
|
||||
QrCodeBadge(
|
||||
data = QrCodeState.Present(QrCodeData.forData("https://signal.org", 64)),
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "TheAmazingSpiderMan.42",
|
||||
usernameCopyable = false
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
QrCodeBadge(
|
||||
data = QrCodeState.Present(QrCodeData.forData("https://signal.org", 64)),
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "TheAmazingSpiderMan.42",
|
||||
usernameCopyable = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = "Colors", heightDp = 1500)
|
||||
@Composable
|
||||
private fun PreviewAllColorsP1() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
Column {
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.Blue)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.White)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.Green)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.Grey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = "Colors", heightDp = 1500)
|
||||
@Composable
|
||||
private fun PreviewAllColorsP2() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
Column {
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.Pink)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.Orange)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.Purple)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SampleCode(colorScheme = UsernameQrCodeColorScheme.Tan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SampleCode(colorScheme: UsernameQrCodeColorScheme) {
|
||||
QrCodeBadge(
|
||||
data = QrCodeState.Present(QrCodeData.forData("https://signal.me/#eu/asdfasdfasdfasdfasdfasdfasdfasdfasdf", 64)),
|
||||
colorScheme = colorScheme,
|
||||
username = "parker.42"
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "Loading", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "Loading", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewLoading() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
QrCodeBadge(
|
||||
data = QrCodeData.forData("https://signal.org", 64),
|
||||
data = QrCodeState.Loading,
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "parker.42"
|
||||
)
|
||||
@@ -126,13 +284,14 @@ private fun PreviewWithCode() {
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(name = "Light Theme", group = "NotSet", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "NotSet", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewWithoutCode() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
private fun PreviewNotSet() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
QrCodeBadge(
|
||||
data = null,
|
||||
data = QrCodeState.NotSet,
|
||||
colorScheme = UsernameQrCodeColorScheme.Blue,
|
||||
username = "parker.42"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ class QrCodeData(
|
||||
@WorkerThread
|
||||
fun forData(data: String, size: Int): QrCodeData {
|
||||
val qrCodeWriter = QRCodeWriter()
|
||||
val hints = mapOf(EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.H.toString())
|
||||
val hints = mapOf(EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.Q.toString())
|
||||
|
||||
val padded = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, size, size, hints)
|
||||
val dimens = padded.enclosingRectangle
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
sealed class QrCodeState {
|
||||
/** QR code data exists and is available. */
|
||||
data class Present(val data: QrCodeData) : QrCodeState()
|
||||
|
||||
/** QR code data does not exist. */
|
||||
object NotSet : QrCodeState()
|
||||
|
||||
/** QR code data is in an indeterminate loading state. */
|
||||
object Loading : QrCodeState()
|
||||
}
|
||||
@@ -17,7 +17,7 @@ enum class UsernameQrCodeColorScheme(
|
||||
),
|
||||
White(
|
||||
borderColor = Color(0xFFFFFFFF),
|
||||
foregroundColor = Color(0xFF464852),
|
||||
foregroundColor = Color(0xFF000000),
|
||||
key = "white"
|
||||
),
|
||||
Grey(
|
||||
|
||||
@@ -65,12 +65,14 @@ class UsernameLinkQrColorPickerFragment : ComposeFragment() {
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
QrCodeBadge(
|
||||
data = state.qrCodeData,
|
||||
colorScheme = state.selectedColorScheme,
|
||||
username = state.username
|
||||
username = state.username,
|
||||
modifier = Modifier.padding(horizontal = 58.dp, vertical = 24.dp)
|
||||
)
|
||||
|
||||
ColorPicker(
|
||||
@@ -160,7 +162,7 @@ class UsernameLinkQrColorPickerFragment : ComposeFragment() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ColorPickerItemPreview() {
|
||||
private fun PreviewColorPickerItem() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -173,7 +175,7 @@ class UsernameLinkQrColorPickerFragment : ComposeFragment() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ColorPickerPreview() {
|
||||
private fun PreviewColorPicker() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
ColorPicker(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.colorpicker
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
|
||||
data class UsernameLinkQrColorPickerState(
|
||||
val username: String,
|
||||
val qrCodeData: QrCodeData?,
|
||||
val qrCodeData: QrCodeState,
|
||||
val colorSchemes: ImmutableList<UsernameQrCodeColorScheme>,
|
||||
val selectedColorScheme: UsernameQrCodeColorScheme
|
||||
)
|
||||
|
||||
@@ -9,20 +9,22 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
|
||||
class UsernameLinkQrColorPickerViewModel : ViewModel() {
|
||||
|
||||
private val username: String = Recipient.self().username.get()
|
||||
|
||||
private val _state = mutableStateOf(
|
||||
UsernameLinkQrColorPickerState(
|
||||
username = username,
|
||||
qrCodeData = null,
|
||||
username = SignalStore.account().username!!,
|
||||
qrCodeData = QrCodeState.Loading,
|
||||
colorSchemes = UsernameQrCodeColorScheme.values().asList().toImmutableList(),
|
||||
selectedColorScheme = SignalStore.misc().usernameQrCodeColorScheme
|
||||
)
|
||||
@@ -33,15 +35,23 @@ class UsernameLinkQrColorPickerViewModel : ViewModel() {
|
||||
private val disposable: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
init {
|
||||
disposable += Single
|
||||
.fromCallable { QrCodeData.forData(UsernameUtil.generateLink(username), 64) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData ->
|
||||
_state.value = _state.value.copy(
|
||||
qrCodeData = qrData
|
||||
)
|
||||
}
|
||||
val usernameLink = SignalStore.account().usernameLink
|
||||
|
||||
if (usernameLink != null) {
|
||||
disposable += Single
|
||||
.fromCallable { QrCodeData.forData(UsernameUtil.generateLink(usernameLink), 64) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData ->
|
||||
_state.value = _state.value.copy(
|
||||
qrCodeData = QrCodeState.Present(qrData)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.value = _state.value.copy(
|
||||
qrCodeData = QrCodeState.NotSet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -50,6 +60,11 @@ class UsernameLinkQrColorPickerViewModel : ViewModel() {
|
||||
|
||||
fun onColorSelected(color: UsernameQrCodeColorScheme) {
|
||||
SignalStore.misc().usernameQrCodeColorScheme = color
|
||||
SignalExecutors.BOUNDED.run {
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
selectedColorScheme = color
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
sealed class QrScanResult {
|
||||
class Success(val recipient: Recipient) : QrScanResult()
|
||||
|
||||
class NotFound(val username: String) : QrScanResult()
|
||||
class NotFound(val username: String?) : QrScanResult()
|
||||
|
||||
object InvalidData : QrScanResult()
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
|
||||
|
||||
/**
|
||||
* Result of resetting the username link.
|
||||
*/
|
||||
sealed class UsernameLinkResetResult {
|
||||
/** Successfully reset the username link. */
|
||||
data class Success(val components: UsernameLinkComponents) : UsernameLinkResetResult()
|
||||
|
||||
/** Network failed when making the request. The username is still considered to be "reset". */
|
||||
object NetworkError : UsernameLinkResetResult()
|
||||
|
||||
/** We never made the request because we detected the user had no network. */
|
||||
object NetworkUnavailable : UsernameLinkResetResult()
|
||||
|
||||
/** We never made the request because we hit an unexpected error. */
|
||||
object UnexpectedError : UsernameLinkResetResult()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -17,7 +18,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -55,7 +56,6 @@ import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalPermissionsApi::class
|
||||
)
|
||||
class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
@@ -71,6 +71,7 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
||||
val scope: CoroutineScope = rememberCoroutineScope()
|
||||
val navController: NavController by remember { mutableStateOf(findNavController()) }
|
||||
var showResetDialog: Boolean by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
@@ -95,7 +96,9 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
onShareBadge = {
|
||||
shareQrBadge(it)
|
||||
},
|
||||
screenshotController = screenshotController
|
||||
screenshotController = screenshotController,
|
||||
onResetClicked = { showResetDialog = true },
|
||||
onLinkResultHandled = { viewModel.onUsernameLinkResetResultHandled() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -114,6 +117,16 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showResetDialog) {
|
||||
ResetDialog(
|
||||
onConfirm = {
|
||||
viewModel.onUsernameLinkReset()
|
||||
showResetDialog = false
|
||||
},
|
||||
onDismiss = { showResetDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -182,20 +195,43 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_title),
|
||||
body = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_body),
|
||||
confirm = stringResource(id = R.string.UsernameLinkSettings_reset_link_dialog_confirm_button),
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onConfirm = onConfirm,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppBarPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
private fun PreviewAppBar() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
TopAppBarContent(activeTab = ActiveTab.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewAll() {
|
||||
FragmentContent()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewAll() {
|
||||
FragmentContent()
|
||||
private fun PreviewResetDialog() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
ResetDialog(onConfirm = {}, onDismiss = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareQrBadge(badge: Bitmap) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
|
||||
/**
|
||||
@@ -9,10 +9,11 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.Username
|
||||
data class UsernameLinkSettingsState(
|
||||
val activeTab: ActiveTab,
|
||||
val username: String,
|
||||
val usernameLink: String,
|
||||
val qrCodeData: QrCodeData?,
|
||||
val usernameLinkState: UsernameLinkState,
|
||||
val qrCodeState: QrCodeState,
|
||||
val qrCodeColorScheme: UsernameQrCodeColorScheme,
|
||||
val qrScanResult: QrScanResult? = null,
|
||||
val usernameLinkResetResult: UsernameLinkResetResult? = null,
|
||||
val indeterminateProgress: Boolean = false
|
||||
) {
|
||||
enum class ActiveTab {
|
||||
|
||||
@@ -10,46 +10,46 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.usernames.BaseUsernameException
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import java.io.IOException
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
|
||||
import java.util.Optional
|
||||
|
||||
class UsernameLinkSettingsViewModel : ViewModel() {
|
||||
|
||||
private val TAG = Log.tag(UsernameLinkSettingsViewModel::class.java)
|
||||
|
||||
private val username: BehaviorSubject<String> = BehaviorSubject.createDefault(Recipient.self().username.get())
|
||||
|
||||
private val _state = mutableStateOf(
|
||||
UsernameLinkSettingsState(
|
||||
activeTab = ActiveTab.Code,
|
||||
username = username.value!!,
|
||||
usernameLink = UsernameUtil.generateLink(username.value!!),
|
||||
qrCodeData = null,
|
||||
username = SignalStore.account().username!!,
|
||||
usernameLinkState = SignalStore.account().usernameLink?.let { UsernameLinkState.Present(UsernameUtil.generateLink(it)) } ?: UsernameLinkState.NotSet,
|
||||
qrCodeState = QrCodeState.Loading,
|
||||
qrCodeColorScheme = SignalStore.misc().usernameQrCodeColorScheme
|
||||
)
|
||||
)
|
||||
|
||||
val state: State<UsernameLinkSettingsState> = _state
|
||||
|
||||
private val disposable: CompositeDisposable = CompositeDisposable()
|
||||
private val usernameLink: BehaviorSubject<Optional<UsernameLinkComponents>> = BehaviorSubject.createDefault(Optional.ofNullable(SignalStore.account().usernameLink))
|
||||
private val usernameRepo: UsernameRepository = UsernameRepository()
|
||||
|
||||
init {
|
||||
disposable += username
|
||||
disposable += usernameLink
|
||||
.observeOn(Schedulers.io())
|
||||
.map { UsernameUtil.generateLink(it) }
|
||||
.map { link -> link.map { UsernameUtil.generateLink(it) } }
|
||||
.flatMapSingle { generateQrCodeData(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData ->
|
||||
_state.value = _state.value.copy(
|
||||
qrCodeData = qrData
|
||||
qrCodeState = if (qrData.isPresent) QrCodeState.Present(qrData.get()) else QrCodeState.NotSet
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -70,37 +70,70 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
fun onUsernameLinkReset() {
|
||||
if (!NetworkUtil.isConnected(ApplicationDependencies.getApplication())) {
|
||||
_state.value = _state.value.copy(
|
||||
usernameLinkResetResult = UsernameLinkResetResult.NetworkUnavailable
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val currentValue = _state.value
|
||||
val previousQrValue: QrCodeData? = if (currentValue.qrCodeState is QrCodeState.Present) {
|
||||
currentValue.qrCodeState.data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
usernameLinkState = UsernameLinkState.Resetting,
|
||||
qrCodeState = QrCodeState.Loading
|
||||
)
|
||||
|
||||
disposable += usernameRepo.createOrResetUsernameLink()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
val components: Optional<UsernameLinkComponents> = when (result) {
|
||||
is UsernameLinkResetResult.Success -> Optional.of(result.components)
|
||||
is UsernameLinkResetResult.NetworkError -> Optional.empty()
|
||||
else -> { usernameLink.value ?: Optional.empty() }
|
||||
}
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
usernameLinkState = if (components.isPresent) {
|
||||
val link = UsernameUtil.generateLink(components.get())
|
||||
UsernameLinkState.Present(link)
|
||||
} else {
|
||||
UsernameLinkState.NotSet
|
||||
},
|
||||
usernameLinkResetResult = result,
|
||||
qrCodeState = if (components.isPresent && previousQrValue != null) {
|
||||
QrCodeState.Present(previousQrValue)
|
||||
} else {
|
||||
QrCodeState.NotSet
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onUsernameLinkResetResultHandled() {
|
||||
_state.value = _state.value.copy(
|
||||
usernameLinkResetResult = null
|
||||
)
|
||||
}
|
||||
|
||||
fun onQrCodeScanned(url: String) {
|
||||
_state.value = _state.value.copy(
|
||||
indeterminateProgress = true
|
||||
)
|
||||
|
||||
disposable += Single
|
||||
.fromCallable {
|
||||
val username: String? = UsernameUtil.parseLink(url)
|
||||
|
||||
if (username == null) {
|
||||
Log.w(TAG, "Failed to parse username from url")
|
||||
return@fromCallable QrScanResult.InvalidData
|
||||
}
|
||||
|
||||
return@fromCallable try {
|
||||
val hashed: String = UsernameUtil.hashUsernameToBase64(username)
|
||||
val aci: ACI = ApplicationDependencies.getSignalServiceAccountManager().getAciByUsernameHash(hashed)
|
||||
QrScanResult.Success(Recipient.externalUsername(aci, username))
|
||||
} catch (e: BaseUsernameException) {
|
||||
Log.w(TAG, "Invalid username", e)
|
||||
QrScanResult.InvalidData
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
Log.w(TAG, "Non-successful response during username resolution", e)
|
||||
if (e.code == 404) {
|
||||
QrScanResult.NotFound(username)
|
||||
} else {
|
||||
QrScanResult.NetworkError
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Network error during username resolution", e)
|
||||
QrScanResult.NetworkError
|
||||
disposable += usernameRepo.convertLinkToUsernameAndAci(url)
|
||||
.map { result ->
|
||||
when (result) {
|
||||
is UsernameRepository.UsernameLinkConversionResult.Success -> QrScanResult.Success(Recipient.externalUsername(result.aci, result.username.toString()))
|
||||
is UsernameRepository.UsernameLinkConversionResult.Invalid -> QrScanResult.InvalidData
|
||||
is UsernameRepository.UsernameLinkConversionResult.NotFound -> QrScanResult.NotFound(result.username?.toString())
|
||||
is UsernameRepository.UsernameLinkConversionResult.NetworkError -> QrScanResult.NetworkError
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -119,9 +152,9 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateQrCodeData(url: String): Single<QrCodeData> {
|
||||
private fun generateQrCodeData(url: Optional<String>): Single<Optional<QrCodeData>> {
|
||||
return Single.fromCallable {
|
||||
QrCodeData.forData(url, 64)
|
||||
url.map { QrCodeData.forData(it, 64) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -10,6 +12,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -19,6 +22,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -31,14 +35,15 @@ import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeBadge
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
|
||||
import org.thoughtcrime.securesms.compose.ScreenshotController
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -48,22 +53,43 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@Composable
|
||||
fun UsernameLinkShareScreen(
|
||||
state: UsernameLinkSettingsState,
|
||||
onLinkResultHandled: () -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope,
|
||||
navController: NavController,
|
||||
onShareBadge: (Bitmap) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
screenshotController: ScreenshotController? = null
|
||||
screenshotController: ScreenshotController? = null,
|
||||
onResetClicked: () -> Unit
|
||||
) {
|
||||
when (state.usernameLinkResetResult) {
|
||||
UsernameLinkResetResult.NetworkUnavailable -> {
|
||||
ResetLinkResultDialog(stringResource(R.string.UsernameLinkSettings_reset_link_result_network_unavailable), onDismiss = onLinkResultHandled)
|
||||
}
|
||||
UsernameLinkResetResult.NetworkError -> {
|
||||
ResetLinkResultDialog(stringResource(R.string.UsernameLinkSettings_reset_link_result_network_error), onDismiss = onLinkResultHandled)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val usernameCopiedString = stringResource(id = R.string.UsernameLinkSettings_username_copied_toast)
|
||||
QrCodeBadge(
|
||||
data = state.qrCodeData,
|
||||
data = state.qrCodeState,
|
||||
colorScheme = state.qrCodeColorScheme,
|
||||
username = state.username,
|
||||
screenshotController = screenshotController
|
||||
screenshotController = screenshotController,
|
||||
usernameCopyable = true,
|
||||
modifier = Modifier.padding(horizontal = 58.dp, vertical = 24.dp),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(usernameCopiedString)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ButtonBar(
|
||||
@@ -76,16 +102,8 @@ fun UsernameLinkShareScreen(
|
||||
onColorClicked = { navController.safeNavigate(R.id.action_usernameLinkSettingsFragment_to_usernameLinkQrColorPickerFragment) }
|
||||
)
|
||||
|
||||
CopyRow(
|
||||
displayText = state.username,
|
||||
copyMessage = stringResource(R.string.UsernameLinkSettings_username_copied_toast),
|
||||
snackbarHostState = snackbarHostState,
|
||||
scope = scope
|
||||
)
|
||||
|
||||
CopyRow(
|
||||
displayText = state.usernameLink,
|
||||
copyMessage = stringResource(R.string.UsernameLinkSettings_link_copied_toast),
|
||||
LinkRow(
|
||||
linkState = state.usernameLinkState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
scope = scope
|
||||
)
|
||||
@@ -94,7 +112,7 @@ fun UsernameLinkShareScreen(
|
||||
text = stringResource(id = R.string.UsernameLinkSettings_qr_description),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 36.dp, start = 43.dp, end = 43.dp),
|
||||
modifier = Modifier.padding(bottom = 19.dp, start = 43.dp, end = 43.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -104,7 +122,7 @@ fun UsernameLinkShareScreen(
|
||||
.padding(bottom = 24.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Buttons.Small(onClick = { /*TODO*/ }) {
|
||||
Buttons.Small(onClick = onResetClicked) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.UsernameLinkSettings_reset_button_label)
|
||||
)
|
||||
@@ -133,29 +151,46 @@ private fun ButtonBar(onShareClicked: () -> Unit, onColorClicked: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CopyRow(displayText: String, copyMessage: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
private fun LinkRow(linkState: UsernameLinkState, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
val context = LocalContext.current
|
||||
val copyMessage = stringResource(R.string.UsernameLinkSettings_link_copied_toast)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.clickable {
|
||||
Util.copyToClipboard(context, displayText)
|
||||
.padding(
|
||||
top = 32.dp,
|
||||
bottom = 24.dp,
|
||||
start = 24.dp,
|
||||
end = 24.dp
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.clickable(enabled = linkState is UsernameLinkState.Present) {
|
||||
Util.copyToClipboard(context, (linkState as UsernameLinkState.Present).link)
|
||||
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(copyMessage)
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 26.dp, vertical = 16.dp)
|
||||
.alpha(if (linkState is UsernameLinkState.Present) 1.0f else 0.6f)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.symbol_copy_android_24),
|
||||
painter = painterResource(id = R.drawable.symbol_link_24),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = displayText,
|
||||
text = when (linkState) {
|
||||
is UsernameLinkState.Present -> linkState.link
|
||||
is UsernameLinkState.NotSet -> stringResource(id = R.string.UsernameLinkSettings_link_not_set_label)
|
||||
is UsernameLinkState.Resetting -> stringResource(id = R.string.UsernameLinkSettings_resetting_link_label)
|
||||
},
|
||||
modifier = Modifier.padding(start = 26.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -163,45 +198,68 @@ private fun CopyRow(displayText: String, copyMessage: String, snackbarHostState:
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme")
|
||||
@Composable
|
||||
private fun ScreenPreviewLightTheme() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
private fun ResetLinkResultDialog(message: String, onDismiss: () -> Unit) {
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = message,
|
||||
dismiss = stringResource(id = android.R.string.ok),
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ScreenPreview() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
UsernameLinkShareScreen(
|
||||
state = previewState(),
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
scope = rememberCoroutineScope(),
|
||||
navController = NavController(LocalContext.current),
|
||||
onShareBadge = {}
|
||||
onShareBadge = {},
|
||||
onResetClicked = {},
|
||||
onLinkResultHandled = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Dark Theme")
|
||||
@Preview(name = "Light Theme", group = "LinkRow", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "LinkRow", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ScreenPreviewDarkTheme() {
|
||||
SignalTheme(isDarkMode = true) {
|
||||
private fun LinkRowPreview() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
UsernameLinkShareScreen(
|
||||
state = previewState(),
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
scope = rememberCoroutineScope(),
|
||||
navController = NavController(LocalContext.current),
|
||||
onShareBadge = {}
|
||||
)
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
LinkRow(
|
||||
linkState = UsernameLinkState.Present("https://signal.me/#eu/asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf"),
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
scope = rememberCoroutineScope()
|
||||
)
|
||||
LinkRow(
|
||||
linkState = UsernameLinkState.NotSet,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
scope = rememberCoroutineScope()
|
||||
)
|
||||
LinkRow(
|
||||
linkState = UsernameLinkState.Resetting,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
scope = rememberCoroutineScope()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewState(): UsernameLinkSettingsState {
|
||||
val link = UsernameUtil.generateLink("maya.45")
|
||||
val link = "https://signal.me/#eu/asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf"
|
||||
return UsernameLinkSettingsState(
|
||||
activeTab = ActiveTab.Code,
|
||||
username = "maya.45",
|
||||
usernameLink = link,
|
||||
qrCodeData = QrCodeData.forData(link, 64),
|
||||
username = "parker.42",
|
||||
usernameLinkState = UsernameLinkState.Present("https://signal.me/#eu/asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf"),
|
||||
qrCodeState = QrCodeState.Present(QrCodeData.forData(link, 64)),
|
||||
qrCodeColorScheme = UsernameQrCodeColorScheme.Blue
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
sealed class UsernameLinkState {
|
||||
|
||||
/** Link is set. */
|
||||
data class Present(val link: String) : UsernameLinkState()
|
||||
|
||||
/** Link has not been set yet or otherwise does not exist. */
|
||||
object NotSet : UsernameLinkState()
|
||||
|
||||
/** Link is in the process of being reset. */
|
||||
object Resetting : UsernameLinkState()
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.signal.qr.QrScannerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* A screen that allows you to scan a QR code to start a chat.
|
||||
@@ -53,7 +54,11 @@ fun UsernameQrScanScreen(
|
||||
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_network_error), onDismiss = onQrResultHandled)
|
||||
}
|
||||
is QrScanResult.NotFound -> {
|
||||
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_not_found, qrScanResult.username), onDismiss = onQrResultHandled)
|
||||
if (qrScanResult.username != null) {
|
||||
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_not_found, qrScanResult.username), onDismiss = onQrResultHandled)
|
||||
} else {
|
||||
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_not_found_no_username), onDismiss = onQrResultHandled)
|
||||
}
|
||||
}
|
||||
is QrScanResult.Success -> {
|
||||
CommunicationActions.startConversation(LocalContext.current, qrScanResult.recipient, null)
|
||||
@@ -70,7 +75,7 @@ fun UsernameQrScanScreen(
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val view = QrScannerView(context)
|
||||
disposables += view.qrData.distinctUntilChanged().subscribe { data ->
|
||||
disposables += view.qrData.throttleFirst(3000, TimeUnit.MILLISECONDS).subscribe { data ->
|
||||
onQrCodeScanned(data)
|
||||
}
|
||||
view
|
||||
|
||||
Reference in New Issue
Block a user