diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7911227fc4..05e5fc1a30 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1089,6 +1089,11 @@
android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/>
+
+
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 = emptyList()
- override val revokedPermissions: List = 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")
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt
index d8d852814c..1630ee32a8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt
@@ -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
+ )
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanRepository.kt
new file mode 100644
index 0000000000..a71a72f382
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanRepository.kt
@@ -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 {
+ 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 {
+ 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())
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt
index d5939441e9..16d2e4b1ce 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScanScreen.kt
@@ -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),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt
new file mode 100644
index 0000000000..ebcc11a95e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerActivity.kt
@@ -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() {
+ 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()
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerViewModel.kt
new file mode 100644
index 0000000000..26d62a6054
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameQrScannerViewModel.kt
@@ -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 = _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
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt
index 817f1601bd..36aa855b3b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt
@@ -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 = 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 = {}
)
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5184d7f8f0..3f3c26a620 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6621,6 +6621,8 @@
%1$s is not a Signal user. Would you like to invite this number?
Invite
+
+ Scan QR code