Migrate VerifyScanFragment to compose.

This commit is contained in:
Alex Hart
2026-04-21 12:21:49 -03:00
parent 552361dff4
commit 9fa587b7e4
4 changed files with 228 additions and 95 deletions
@@ -1,56 +1,35 @@
package org.thoughtcrime.securesms.verify
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.view.OneShotPreDrawListener
import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.qr.QrScannerView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import org.signal.camera.CameraScreenViewModel
import org.signal.core.ui.compose.ComposeFragment
import org.signal.qr.kitkat.ScanListener
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ShapeScrim
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.fragments.findListener
/**
* QR Scanner for identity verification
*/
class VerifyScanFragment : Fragment() {
private val lifecycleDisposable = LifecycleDisposable()
private lateinit var cameraView: QrScannerView
private lateinit var cameraScrim: ShapeScrim
private lateinit var cameraMarks: ImageView
override fun onCreateView(inflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
return ViewUtil.inflate(inflater, viewGroup!!, R.layout.verify_scan_fragment)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
cameraView = view.findViewById(R.id.scanner)
cameraScrim = view.findViewById(R.id.camera_scrim)
cameraMarks = view.findViewById(R.id.camera_marks)
OneShotPreDrawListener.add(cameraScrim) {
val width = cameraScrim.scrimWidth
val height = cameraScrim.scrimHeight
ViewUtil.updateLayoutParams(cameraMarks, width, height)
class VerifyScanFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
val viewModel = viewModel {
CameraScreenViewModel()
}
cameraView.start(viewLifecycleOwner, CameraXRemoteConfig.isBlocklisted())
val state by viewModel.state
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += cameraView
.qrData
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { qrData: String ->
findListener<ScanListener>()?.onQrDataFound(qrData)
LaunchedEffect(viewModel) {
viewModel.qrCodeDetected.collect {
findListener<ScanListener>()?.onQrDataFound(it)
}
}
VerifyScanScreen(
state = state,
emitter = viewModel::onEvent
)
}
}
@@ -0,0 +1,93 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.verify
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.camera.CameraCaptureMode
import org.signal.camera.CameraScreen
import org.signal.camera.CameraScreenEvents
import org.signal.camera.CameraScreenState
import org.signal.camera.CameraScreenViewModel
import org.signal.core.ui.compose.Cutout
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
/**
* Scanner screen for verifying user identities. This is meant to be utilized with an instance of
* [CameraScreenViewModel], which the parent component owns. There is a field on the ViewModel through
* which you can recieve via [qrCodeDetected]
*/
@Composable
fun VerifyScanScreen(
state: CameraScreenState,
emitter: (CameraScreenEvents) -> Unit
) {
Column {
CameraScreen(
state = state,
emitter = emitter,
roundCorners = false,
enableQrScanning = true,
captureMode = CameraCaptureMode.ImageOnly,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Cutout(
cutoutShape = RoundedCornerShape(18.dp),
cutoutPadding = PaddingValues(64.dp),
modifier = Modifier.fillMaxSize()
)
Image(
painter = painterResource(R.drawable.ic_camera_outline),
contentDescription = null,
modifier = Modifier.align(Alignment.Center)
)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.heightIn(min = 60.dp)
) {
Text(
text = stringResource(R.string.verify_scan_fragment__scan_the_qr_code_on_your_contact),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@DayNightPreviews
@Composable
private fun VerifyScanScreenPreview() {
Previews.Preview {
VerifyScanScreen(
state = CameraScreenState(),
emitter = {}
)
}
}