[Link new device] Improve UI transition between QRCodes.

This commit is contained in:
Benoit Marty 2026-05-19 10:43:47 +02:00 committed by Benoit Marty
parent 1500c0fadd
commit 9d704fb751
4 changed files with 99 additions and 28 deletions

View file

@ -22,6 +22,9 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
import io.element.android.libraries.matrix.api.logs.LoggerTags import io.element.android.libraries.matrix.api.logs.LoggerTags
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
private val tag = LoggerTag("ShowQrCodePresenter", LoggerTags.linkNewDevice) private val tag = LoggerTag("ShowQrCodePresenter", LoggerTags.linkNewDevice)
@ -36,20 +39,54 @@ class ShowQrCodePresenter(
fun create(initialData: String): ShowQrCodePresenter fun create(initialData: String): ShowQrCodePresenter
} }
private var loadingJob: Job? = null
@Composable @Composable
override fun present(): ShowQrCodeState { override fun present(): ShowQrCodeState {
var qrCodeRotationCounter by remember { mutableIntStateOf(MAX_QR_CODE_ROTATION) } var qrCodeRotationCounter by remember { mutableIntStateOf(MAX_QR_CODE_ROTATION) }
val data by produceState<AsyncData<String>>(AsyncData.Success(initialData)) { val state by produceState(
initialValue = ShowQrCodeState(
data1 = AsyncData.Success(initialData),
data2 = AsyncData.Uninitialized,
dataToRender = 1,
)
) {
linkNewMobileHandler.stepFlow.collect { step -> linkNewMobileHandler.stepFlow.collect { step ->
val currentValue = value
when (step) { when (step) {
is LinkMobileStep.QrReady -> { is LinkMobileStep.QrReady -> {
value = AsyncData.Success(step.data) loadingJob?.cancel()
if (currentValue.dataToRender == 1) {
value = currentValue.copy(
data2 = AsyncData.Success(step.data),
dataToRender = 2,
)
} else {
value = currentValue.copy(
data1 = AsyncData.Success(step.data),
dataToRender = 1,
)
}
} }
is LinkMobileStep.QrRotating -> { is LinkMobileStep.QrRotating -> {
if (qrCodeRotationCounter-- > 0) { if (qrCodeRotationCounter-- > 0) {
Timber.tag(tag.value).d("Rotating QrCode") Timber.tag(tag.value).d("Rotating QrCode")
linkNewMobileHandler.rotateQrCode() linkNewMobileHandler.rotateQrCode()
value = AsyncData.Loading() // Ensure that outdated data is not rendered too long while rotating QR code
loadingJob = launch {
delay(1000)
if (currentValue.dataToRender == 1) {
value = currentValue.copy(
data2 = AsyncData.Loading(),
dataToRender = 2,
)
} else {
value = currentValue.copy(
data1 = AsyncData.Loading(),
dataToRender = 1,
)
}
}
} else { } else {
Timber.tag(tag.value).w("Max QR code rotation reached, not rotating anymore") Timber.tag(tag.value).w("Max QR code rotation reached, not rotating anymore")
linkNewMobileHandler.onTooManyRotation() linkNewMobileHandler.onTooManyRotation()
@ -60,9 +97,7 @@ class ShowQrCodePresenter(
} }
} }
return ShowQrCodeState( return state
data = data,
)
} }
companion object { companion object {

View file

@ -10,5 +10,7 @@ package io.element.android.features.linknewdevice.impl.screens.qrcode
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
data class ShowQrCodeState( data class ShowQrCodeState(
val data: AsyncData<String>, val data1: AsyncData<String>,
val data2: AsyncData<String>,
val dataToRender: Int,
) )

View file

@ -14,14 +14,23 @@ class ShowQrCodeStateProvider : PreviewParameterProvider<ShowQrCodeState> {
override val values: Sequence<ShowQrCodeState> override val values: Sequence<ShowQrCodeState>
get() = sequenceOf( get() = sequenceOf(
aShowQrCodeState(), aShowQrCodeState(),
ShowQrCodeState( aShowQrCodeState(
data = AsyncData.Loading(), data1 = AsyncData.Loading(),
),
aShowQrCodeState(
data1 = AsyncData.Success("DATA"),
data2 = AsyncData.Success("DATA2"),
dataToRender = 2,
), ),
) )
} }
private fun aShowQrCodeState( private fun aShowQrCodeState(
data: AsyncData.Success<String> = AsyncData.Success("DATA"), data1: AsyncData<String> = AsyncData.Success("DATA"),
data2: AsyncData<String> = AsyncData.Uninitialized,
dataToRender: Int = 1,
) = ShowQrCodeState( ) = ShowQrCodeState(
data = data, data1 = data1,
data2 = data2,
dataToRender = dataToRender,
) )

View file

@ -9,6 +9,9 @@
package io.element.android.features.linknewdevice.impl.screens.qrcode package io.element.android.features.linknewdevice.impl.screens.qrcode
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -58,23 +61,15 @@ fun ShowQrCodeView(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
when (val str = state.data.dataOrNull()) { Box {
null -> { QrCodeOrLoading(
Box( isVisible = state.dataToRender == 1,
modifier = Modifier data = state.data1.dataOrNull(),
.size(220.dp), )
contentAlignment = Alignment.Center, QrCodeOrLoading(
) { isVisible = state.dataToRender == 2,
CircularProgressIndicator() data = state.data2.dataOrNull(),
} )
}
else -> {
QrCodeImage(
data = str,
modifier = Modifier
.size(220.dp)
)
}
} }
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
NumberedListOrganism( NumberedListOrganism(
@ -95,6 +90,36 @@ fun ShowQrCodeView(
} }
} }
@Composable
private fun QrCodeOrLoading(
isVisible: Boolean,
data: String?,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
visible = isVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
if (data == null) {
Box(
modifier = Modifier
.size(220.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
} else {
QrCodeImage(
data = data,
modifier = Modifier
.size(220.dp)
)
}
}
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun ShowQrCodeViewPreview( internal fun ShowQrCodeViewPreview(