Allow scanning QR code from 'Find by username' screen.

This commit is contained in:
Greyson Parrelli
2024-02-29 13:59:39 -05:00
committed by Alex Hart
parent c6df4af53a
commit 56b482a26f
9 changed files with 370 additions and 75 deletions

View File

@@ -1089,6 +1089,11 @@
android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrScannerActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="false"/>
<activity android:name=".reactions.edit.EditReactionsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"

View File

@@ -42,11 +42,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.core.app.TaskStackBuilder
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
@@ -67,6 +69,7 @@ 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.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
@@ -75,6 +78,7 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.Use
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.CommunicationActions
import java.io.ByteArrayOutputStream
import java.util.UUID
@@ -130,14 +134,19 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
lifecycleOwner = viewLifecycleOwner,
disposables = disposables.disposables,
cameraPermissionState = cameraPermissionState,
galleryPermissionState = galleryPermissionState,
onCodeTabSelected = { viewModel.onTabSelected(ActiveTab.Code) },
onScanTabSelected = { viewModel.onTabSelected(ActiveTab.Scan) },
onUsernameLinkResetResultHandled = { viewModel.onUsernameLinkResetResultHandled() },
onShareBadge = { shareQrBadge(requireActivity(), viewModel.generateQrCodeImage(helpText)) },
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
onQrResultHandled = { viewModel.onQrResultHandled() },
onOpenGalleryClicked = { galleryLauncher.launch(Unit) },
onOpenGalleryClicked = {
if (galleryPermissionState.allPermissionsGranted) {
galleryLauncher.launch(Unit)
} else {
galleryPermissionState.launchMultiplePermissionRequest()
}
},
onLinkReset = { viewModel.onUsernameLinkReset() },
onBackNavigationPressed = { requireActivity().onBackPressed() },
linkCopiedEvent = linkCopiedEvent
@@ -162,7 +171,6 @@ private fun MainScreen(
lifecycleOwner: LifecycleOwner = previewLifecycleOwner,
disposables: CompositeDisposable = CompositeDisposable(),
cameraPermissionState: PermissionState = previewPermissionState(),
galleryPermissionState: MultiplePermissionsState = previewMultiplePermissionState(),
onCodeTabSelected: () -> Unit = {},
onScanTabSelected: () -> Unit = {},
onUsernameLinkResetResultHandled: () -> Unit = {},
@@ -174,6 +182,8 @@ private fun MainScreen(
onBackNavigationPressed: () -> Unit = {},
linkCopiedEvent: UUID? = null
) {
val context = LocalContext.current
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
val scope: CoroutineScope = rememberCoroutineScope()
var showResetDialog: Boolean by remember { mutableStateOf(false) }
@@ -231,12 +241,18 @@ private fun MainScreen(
UsernameQrScanScreen(
lifecycleOwner = lifecycleOwner,
disposables = disposables,
galleryPermissionState = galleryPermissionState,
qrScanResult = state.qrScanResult,
onQrCodeScanned = onQrCodeScanned,
onQrResultHandled = onQrResultHandled,
onOpenGalleryClicked = onOpenGalleryClicked,
modifier = Modifier.padding(contentPadding)
modifier = Modifier.padding(contentPadding),
onRecipientFound = { recipient ->
val taskStack = TaskStackBuilder
.create(context)
.addNextIntent(MainActivity.clearTop(context))
CommunicationActions.startConversation(context, recipient, null, taskStack)
}
)
}
}
@@ -387,16 +403,6 @@ private fun previewPermissionState(): PermissionState {
}
}
private fun previewMultiplePermissionState(): MultiplePermissionsState {
return object : MultiplePermissionsState {
override val allPermissionsGranted: Boolean = true
override val permissions: List<PermissionState> = emptyList()
override val revokedPermissions: List<PermissionState> = emptyList()
override val shouldShowRationale: Boolean = false
override fun launchMultiplePermissionRequest() = Unit
}
}
private val previewLifecycleOwner: LifecycleOwner = object : LifecycleOwner {
override val lifecycle: Lifecycle
get() = throw UnsupportedOperationException("Only for tests")

View File

@@ -27,8 +27,6 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.core.graphics.withTranslation
import androidx.lifecycle.ViewModel
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
@@ -37,8 +35,6 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.signal.core.util.logging.Log
import org.signal.core.util.toOptional
import org.signal.qr.QrProcessor
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
@@ -48,7 +44,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.toLink
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.NetworkUtil
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import java.util.Optional
@@ -171,16 +166,7 @@ class UsernameLinkSettingsViewModel : ViewModel() {
indeterminateProgress = true
)
disposable += UsernameRepository.fetchUsernameAndAciFromLink(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())
disposable += UsernameQrScanRepository.lookupUsernameUrl(url)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
_state.value = _state.value.copy(
@@ -201,25 +187,17 @@ class UsernameLinkSettingsViewModel : ViewModel() {
}
fun scanImage(context: Context, uri: Uri) {
val loadBitmap = Glide.with(context)
.asBitmap()
.format(DecodeFormat.PREFER_ARGB_8888)
.load(uri)
.submit()
_state.value = _state.value.copy(
indeterminateProgress = true
)
disposable += Single.fromFuture(loadBitmap)
.subscribeOn(Schedulers.io())
.map { QrProcessor().getScannedData(it).toOptional() }
disposable += UsernameQrScanRepository.scanImageUriForQrCode(context, uri)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
if (it.isPresent) {
onQrCodeScanned(it.get())
} else {
_state.value = _state.value.copy(
qrScanResult = QrScanResult.QrNotFound,
indeterminateProgress = false
)
}
.subscribeBy { result ->
_state.value = _state.value.copy(
qrScanResult = result,
indeterminateProgress = false
)
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.content.Context
import android.net.Uri
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.toOptional
import org.signal.qr.QrProcessor
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
import org.thoughtcrime.securesms.recipients.Recipient
/**
* A collection of functions to help with scanning QR codes for usernames.
*/
object UsernameQrScanRepository {
/**
* Given a URL, will attempt to lookup the username, coercing it to a standard set of [QrScanResult]s.
*/
fun lookupUsernameUrl(url: String): Single<QrScanResult> {
return UsernameRepository.fetchUsernameAndAciFromLink(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())
}
/**
* Given a URI pointing to an image that may contain a username QR code, this will attempt to lookup the username, coercing it to a standard set of [QrScanResult]s.
*/
fun scanImageUriForQrCode(context: Context, uri: Uri): Single<QrScanResult> {
val loadBitmap = Glide.with(context)
.asBitmap()
.format(DecodeFormat.PREFER_ARGB_8888)
.load(uri)
.submit()
return Single.fromFuture(loadBitmap)
.map { QrProcessor().getScannedData(it).toOptional() }
.flatMap {
if (it.isPresent) {
lookupUsernameUrl(it.get())
} else {
Single.just(QrScanResult.QrNotFound)
}
}
.subscribeOn(Schedulers.io())
}
}

View File

@@ -24,39 +24,33 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.TaskStackBuilder
import androidx.lifecycle.LifecycleOwner
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.signal.core.ui.Dialogs
import org.signal.core.ui.theme.SignalTheme
import org.signal.qr.QrScannerView
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.concurrent.TimeUnit
/**
* A screen that allows you to scan a QR code to start a chat.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun UsernameQrScanScreen(
lifecycleOwner: LifecycleOwner,
disposables: CompositeDisposable,
galleryPermissionState: MultiplePermissionsState,
qrScanResult: QrScanResult?,
onQrCodeScanned: (String) -> Unit,
onQrResultHandled: () -> Unit,
onOpenGalleryClicked: () -> Unit,
onRecipientFound: (Recipient) -> Unit,
modifier: Modifier = Modifier
) {
val path = remember { Path() }
@@ -87,12 +81,7 @@ fun UsernameQrScanScreen(
}
is QrScanResult.Success -> {
val taskStack = TaskStackBuilder
.create(LocalContext.current)
.addNextIntent(MainActivity.clearTop(LocalContext.current))
CommunicationActions.startConversation(LocalContext.current, qrScanResult.recipient, null, taskStack)
onQrResultHandled()
onRecipientFound(qrScanResult.recipient)
}
null -> {}
@@ -134,13 +123,7 @@ fun UsernameQrScanScreen(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp),
onClick = {
if (galleryPermissionState.allPermissionsGranted) {
onOpenGalleryClicked()
} else {
galleryPermissionState.launchMultiplePermissionRequest()
}
}
onClick = onOpenGalleryClicked
) {
Image(
painter = painterResource(id = R.drawable.symbol_album_24),

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@file:OptIn(ExperimentalPermissionsApi::class)
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.LifecycleOwner
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.signal.core.ui.Dialogs
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.DynamicTheme
/**
* Prompts the user to scan a username QR code. Uses the activity result to communicate the recipient that was found, or null if no valid usernames were scanned.
* See [Contract].
*/
class UsernameQrScannerActivity : AppCompatActivity() {
companion object {
private const val KEY_RECIPIENT_ID = "recipient_id"
}
private val viewModel: UsernameQrScannerViewModel by viewModels()
private val disposables = LifecycleDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
disposables.bindTo(this)
val galleryLauncher = registerForActivityResult(UsernameQrImageSelectionActivity.Contract()) { uri ->
if (uri != null) {
viewModel.onQrImageSelected(this, uri)
}
}
setContent {
val galleryPermissionState: MultiplePermissionsState = rememberMultiplePermissionsState(permissions = PermissionCompat.forImages().toList()) { grants ->
if (grants.values.all { it }) {
galleryLauncher.launch(Unit)
} else {
Toast.makeText(this, R.string.ChatWallpaperPreviewActivity__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT).show()
}
}
val state by viewModel.state
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
Content(
lifecycleOwner = this,
diposables = disposables.disposables,
state = state,
galleryPermissionsState = galleryPermissionState,
onQrScanned = { url -> viewModel.onQrScanned(url) },
onQrResultHandled = {
finish()
},
onOpenGalleryClicked = {
if (galleryPermissionState.allPermissionsGranted) {
galleryLauncher.launch(Unit)
} else {
galleryPermissionState.launchMultiplePermissionRequest()
}
},
onRecipientFound = { recipient ->
val intent = Intent().apply {
putExtra(KEY_RECIPIENT_ID, recipient.id)
}
setResult(RESULT_OK, intent)
finish()
},
onBackNavigationPressed = {
finish()
}
)
}
}
}
class Contract : ActivityResultContract<Unit, RecipientId?>() {
override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, UsernameQrScannerActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): RecipientId? {
return intent?.getParcelableExtraCompat(KEY_RECIPIENT_ID, RecipientId::class.java)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Content(
lifecycleOwner: LifecycleOwner,
diposables: CompositeDisposable,
state: UsernameQrScannerViewModel.ScannerState,
galleryPermissionsState: MultiplePermissionsState,
onQrScanned: (String) -> Unit,
onQrResultHandled: () -> Unit,
onOpenGalleryClicked: () -> Unit,
onRecipientFound: (Recipient) -> Unit,
onBackNavigationPressed: () -> Unit
) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {},
navigationIcon = {
IconButton(
onClick = onBackNavigationPressed
) {
Icon(
painter = painterResource(R.drawable.symbol_x_24),
contentDescription = stringResource(android.R.string.cancel)
)
}
}
)
}
) { contentPadding ->
UsernameQrScanScreen(
lifecycleOwner = lifecycleOwner,
disposables = diposables,
qrScanResult = state.qrScanResult,
onQrCodeScanned = onQrScanned,
onQrResultHandled = onQrResultHandled,
onOpenGalleryClicked = onOpenGalleryClicked,
onRecipientFound = onRecipientFound,
modifier = Modifier.padding(contentPadding)
)
if (state.indeterminateProgress) {
Dialogs.IndeterminateProgressDialog()
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
class UsernameQrScannerViewModel : ViewModel() {
private val _state = mutableStateOf(ScannerState(qrScanResult = null, indeterminateProgress = false))
val state: State<ScannerState> = _state
private val disposables = CompositeDisposable()
fun onQrScanned(url: String) {
_state.value = state.value.copy(indeterminateProgress = true)
disposables += UsernameQrScanRepository.lookupUsernameUrl(url)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
_state.value = _state.value.copy(
qrScanResult = result,
indeterminateProgress = false
)
}
}
fun onQrImageSelected(context: Context, uri: Uri) {
_state.value = state.value.copy(indeterminateProgress = true)
disposables += UsernameQrScanRepository.scanImageUriForQrCode(context, uri)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy { result ->
_state.value = _state.value.copy(
qrScanResult = result,
indeterminateProgress = false
)
}
}
override fun onCleared() {
disposables.clear()
}
data class ScannerState(
val qrScanResult: QrScanResult?,
val indeterminateProgress: Boolean
)
}

View File

@@ -10,6 +10,7 @@ import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
@@ -26,6 +27,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -77,6 +79,7 @@ import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameQrScannerActivity
import org.thoughtcrime.securesms.invites.InviteActions
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberVisualTransformation
import org.thoughtcrime.securesms.recipients.Recipient
@@ -101,6 +104,13 @@ class FindByActivity : PassphraseRequiredActivity() {
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
val qrScanLauncher: ActivityResultLauncher<Unit> = registerForActivityResult(UsernameQrScannerActivity.Contract()) { recipientId ->
if (recipientId != null) {
setResult(RESULT_OK, Intent().putExtra(RECIPIENT_ID, recipientId))
finishAfterTransition()
}
}
setContent {
val state by viewModel.state
@@ -145,6 +155,9 @@ class FindByActivity : PassphraseRequiredActivity() {
},
onSelectCountryPrefixClick = {
navController.navigate("select-country-prefix")
},
onQrCodeScanClicked = {
qrScanLauncher.launch(Unit)
}
)
}
@@ -273,7 +286,8 @@ private fun Content(
state: FindByState,
onUserEntryChanged: (String) -> Unit,
onNextClick: () -> Unit,
onSelectCountryPrefixClick: () -> Unit
onSelectCountryPrefixClick: () -> Unit,
onQrCodeScanClicked: () -> Unit
) {
val placeholderLabel = remember(state.mode) {
if (state.mode == FindByMode.PHONE_NUMBER) R.string.FindByActivity__phone_number else R.string.FindByActivity__username
@@ -364,6 +378,23 @@ private fun Content(
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(top = 8.dp)
)
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Buttons.Small(onClick = onQrCodeScanClicked) {
Icon(painter = painterResource(id = R.drawable.symbol_qrcode_24), contentDescription = stringResource(id = R.string.FindByActivity__qr_scan_button))
Spacer(modifier = Modifier.width(10.dp))
Text(
text = stringResource(id = R.string.FindByActivity__qr_scan_button),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
@@ -541,8 +572,8 @@ private fun CountryPrefixRowItem(
}
}
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(name = "Light Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewPhoneNumber() {
Previews.Preview {
@@ -554,13 +585,14 @@ private fun ContentPreviewPhoneNumber() {
),
onUserEntryChanged = {},
onNextClick = {},
onSelectCountryPrefixClick = {}
onSelectCountryPrefixClick = {},
onQrCodeScanClicked = {}
)
}
}
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(name = "Light Theme", group = "content - username", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content - username", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewUsername() {
Previews.Preview {
@@ -572,7 +604,8 @@ private fun ContentPreviewUsername() {
),
onUserEntryChanged = {},
onNextClick = {},
onSelectCountryPrefixClick = {}
onSelectCountryPrefixClick = {},
onQrCodeScanClicked = {}
)
}
}

View File

@@ -6621,6 +6621,8 @@
<string name="FindByActivity__s_is_not_a_signal_user_would">%1$s is not a Signal user. Would you like to invite this number?</string>
<!-- Dialog action to invite the phone number to Signal -->
<string name="FindByActivity__invite">Invite</string>
<!-- Button label for a button that will launch a camera to scan a username QR code -->
<string name="FindByActivity__qr_scan_button">Scan QR code</string>
<!-- EOF -->
</resources>