Hide the recovery key while we are entering it (#5147)

* Hide the recovery key while we are entering it (#5134)

This is the Element X Android part of
https://github.com/element-hq/element-meta/issues/2888

* Move the textfield contents being visible to the state so we can preview and test it

* Always use the password visual transformation for the recovery key field

* Update screenshots

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-08-12 14:46:00 +02:00 committed by GitHub
parent d39332f8cc
commit 00e72aae44
35 changed files with 138 additions and 42 deletions

View file

@ -9,6 +9,7 @@ package io.element.android.features.securebackup.impl.enter
sealed interface SecureBackupEnterRecoveryKeyEvents { sealed interface SecureBackupEnterRecoveryKeyEvents {
data class OnRecoveryKeyChange(val recoveryKey: String) : SecureBackupEnterRecoveryKeyEvents data class OnRecoveryKeyChange(val recoveryKey: String) : SecureBackupEnterRecoveryKeyEvents
data class ChangeRecoveryKeyFieldContentsVisibility(val visible: Boolean) : SecureBackupEnterRecoveryKeyEvents
data object Submit : SecureBackupEnterRecoveryKeyEvents data object Submit : SecureBackupEnterRecoveryKeyEvents
data object ClearDialog : SecureBackupEnterRecoveryKeyEvents data object ClearDialog : SecureBackupEnterRecoveryKeyEvents
} }

View file

@ -33,6 +33,9 @@ class SecureBackupEnterRecoveryKeyPresenter @Inject constructor(
@Composable @Composable
override fun present(): SecureBackupEnterRecoveryKeyState { override fun present(): SecureBackupEnterRecoveryKeyState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var displayRecoveryKeyFieldContents by rememberSaveable {
mutableStateOf(false)
}
var recoveryKey by rememberSaveable { var recoveryKey by rememberSaveable {
mutableStateOf("") mutableStateOf("")
} }
@ -59,6 +62,9 @@ class SecureBackupEnterRecoveryKeyPresenter @Inject constructor(
// No need to remove the spaces, the SDK will do it. // No need to remove the spaces, the SDK will do it.
coroutineScope.submitRecoveryKey(recoveryKey, submitAction) coroutineScope.submitRecoveryKey(recoveryKey, submitAction)
} }
is SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility -> {
displayRecoveryKeyFieldContents = event.visible
}
} }
} }
@ -66,6 +72,7 @@ class SecureBackupEnterRecoveryKeyPresenter @Inject constructor(
recoveryKeyViewState = RecoveryKeyViewState( recoveryKeyViewState = RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Enter, recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
formattedRecoveryKey = recoveryKey, formattedRecoveryKey = recoveryKey,
displayTextFieldContents = displayRecoveryKeyFieldContents,
inProgress = submitAction.value.isLoading(), inProgress = submitAction.value.isLoading(),
), ),
isSubmitEnabled = recoveryKey.isNotEmpty() && submitAction.value.isUninitialized(), isSubmitEnabled = recoveryKey.isNotEmpty() && submitAction.value.isUninitialized(),

View file

@ -20,18 +20,21 @@ open class SecureBackupEnterRecoveryKeyStateProvider : PreviewParameterProvider<
aSecureBackupEnterRecoveryKeyState(), aSecureBackupEnterRecoveryKeyState(),
aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Loading), aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Loading),
aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Failure(Exception("A Failure"))), aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Failure(Exception("A Failure"))),
aSecureBackupEnterRecoveryKeyState(displayTextFieldContents = false),
) )
} }
fun aSecureBackupEnterRecoveryKeyState( fun aSecureBackupEnterRecoveryKeyState(
recoveryKey: String = aFormattedRecoveryKey(), recoveryKey: String = aFormattedRecoveryKey(),
isSubmitEnabled: Boolean = recoveryKey.isNotEmpty(), isSubmitEnabled: Boolean = recoveryKey.isNotEmpty(),
displayTextFieldContents: Boolean = true,
submitAction: AsyncAction<Unit> = AsyncAction.Uninitialized, submitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (SecureBackupEnterRecoveryKeyEvents) -> Unit = {}, eventSink: (SecureBackupEnterRecoveryKeyEvents) -> Unit = {},
) = SecureBackupEnterRecoveryKeyState( ) = SecureBackupEnterRecoveryKeyState(
recoveryKeyViewState = RecoveryKeyViewState( recoveryKeyViewState = RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Enter, recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
formattedRecoveryKey = recoveryKey, formattedRecoveryKey = recoveryKey,
displayTextFieldContents = displayTextFieldContents,
inProgress = submitAction.isLoading(), inProgress = submitAction.isLoading(),
), ),
isSubmitEnabled = isSubmitEnabled, isSubmitEnabled = isSubmitEnabled,

View file

@ -102,6 +102,9 @@ private fun Content(
onSubmit = { onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit) state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
}, },
toggleRecoveryKeyVisibility = {
state.eventSink(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(it))
}
) )
} }

View file

@ -71,6 +71,7 @@ class SecureBackupSetupPresenter @AssistedInject constructor(
val recoveryKeyViewState = RecoveryKeyViewState( val recoveryKeyViewState = RecoveryKeyViewState(
recoveryKeyUserStory = if (isChangeRecoveryKeyUserStory) RecoveryKeyUserStory.Change else RecoveryKeyUserStory.Setup, recoveryKeyUserStory = if (isChangeRecoveryKeyUserStory) RecoveryKeyUserStory.Change else RecoveryKeyUserStory.Setup,
formattedRecoveryKey = setupState.recoveryKey(), formattedRecoveryKey = setupState.recoveryKey(),
displayTextFieldContents = true,
inProgress = setupState is SetupState.Creating, inProgress = setupState is SetupState.Creating,
) )

View file

@ -43,6 +43,7 @@ private fun SetupState.toRecoveryKeyViewState(): RecoveryKeyViewState {
return RecoveryKeyViewState( return RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Setup, recoveryKeyUserStory = RecoveryKeyUserStory.Setup,
formattedRecoveryKey = recoveryKey(), formattedRecoveryKey = recoveryKey(),
displayTextFieldContents = true,
inProgress = this is SetupState.Creating, inProgress = this is SetupState.Creating,
) )
} }

View file

@ -138,6 +138,7 @@ private fun Content(
onClick = clickLambda, onClick = clickLambda,
onChange = null, onChange = null,
onSubmit = null, onSubmit = null,
toggleRecoveryKeyVisibility = {},
) )
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.securebackup.impl.setup.views package io.element.android.features.securebackup.impl.setup.views
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -20,7 +21,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.autofill.ContentType
@ -32,6 +35,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -57,6 +61,7 @@ internal fun RecoveryKeyView(
onClick: (() -> Unit)?, onClick: (() -> Unit)?,
onChange: ((String) -> Unit)?, onChange: ((String) -> Unit)?,
onSubmit: (() -> Unit)?, onSubmit: (() -> Unit)?,
toggleRecoveryKeyVisibility: (Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -67,7 +72,7 @@ internal fun RecoveryKeyView(
text = stringResource(id = CommonStrings.common_recovery_key), text = stringResource(id = CommonStrings.common_recovery_key),
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
) )
RecoveryKeyContent(state, onClick, onChange, onSubmit) RecoveryKeyContent(state, onClick, onChange, onSubmit, toggleRecoveryKeyVisibility)
RecoveryKeyFooter(state) RecoveryKeyFooter(state)
} }
} }
@ -78,11 +83,17 @@ private fun RecoveryKeyContent(
onClick: (() -> Unit)?, onClick: (() -> Unit)?,
onChange: ((String) -> Unit)?, onChange: ((String) -> Unit)?,
onSubmit: (() -> Unit)?, onSubmit: (() -> Unit)?,
toggleRecoveryKeyVisibility: (Boolean) -> Unit,
) { ) {
when (state.recoveryKeyUserStory) { when (state.recoveryKeyUserStory) {
RecoveryKeyUserStory.Setup, RecoveryKeyUserStory.Setup,
RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick) RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick)
RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange, onSubmit) RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(
state = state,
toggleRecoveryKeyVisibility = toggleRecoveryKeyVisibility,
onChange = onChange,
onSubmit = onSubmit,
)
} }
} }
@ -171,15 +182,24 @@ private fun RecoveryKeyWithCopy(
@Composable @Composable
private fun RecoveryKeyFormContent( private fun RecoveryKeyFormContent(
state: RecoveryKeyViewState, state: RecoveryKeyViewState,
toggleRecoveryKeyVisibility: (Boolean) -> Unit,
onChange: ((String) -> Unit)?, onChange: ((String) -> Unit)?,
onSubmit: (() -> Unit)?, onSubmit: (() -> Unit)?,
) { ) {
onChange ?: error("onChange should not be null") onChange ?: error("onChange should not be null")
onSubmit ?: error("onSubmit should not be null") onSubmit ?: error("onSubmit should not be null")
if (state.inProgress) {
// Ensure recovery key is hidden when user submits the form
toggleRecoveryKeyVisibility(false)
}
val keyHasSpace = state.formattedRecoveryKey.orEmpty().contains(" ") val keyHasSpace = state.formattedRecoveryKey.orEmpty().contains(" ")
val recoveryKeyVisualTransformation = remember(keyHasSpace) { val recoveryKeyVisualTransformation = remember(keyHasSpace, state.displayTextFieldContents) {
// Do not apply a visual transformation if the key has spaces, to let user enter passphrase if (state.displayTextFieldContents) {
if (keyHasSpace) VisualTransformation.None else RecoveryKeyVisualTransformation() // Do not apply a visual transformation if the key has spaces, to let user enter passphrase
if (keyHasSpace) VisualTransformation.None else RecoveryKeyVisualTransformation()
} else {
PasswordVisualTransformation()
}
} }
TextField( TextField(
modifier = Modifier modifier = Modifier
@ -201,6 +221,18 @@ private fun RecoveryKeyFormContent(
onDone = { onSubmit() } onDone = { onSubmit() }
), ),
placeholder = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder), placeholder = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder),
trailingIcon = {
val image =
if (state.displayTextFieldContents) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff()
val description =
if (state.displayTextFieldContents) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
Box(Modifier.clickable { toggleRecoveryKeyVisibility(!state.displayTextFieldContents) }) {
Icon(
imageVector = image,
contentDescription = description,
)
}
},
) )
} }
@ -249,5 +281,6 @@ internal fun RecoveryKeyViewPreview(
onClick = {}, onClick = {},
onChange = {}, onChange = {},
onSubmit = {}, onSubmit = {},
toggleRecoveryKeyVisibility = {},
) )
} }

View file

@ -10,6 +10,7 @@ package io.element.android.features.securebackup.impl.setup.views
data class RecoveryKeyViewState( data class RecoveryKeyViewState(
val recoveryKeyUserStory: RecoveryKeyUserStory, val recoveryKeyUserStory: RecoveryKeyUserStory,
val formattedRecoveryKey: String?, val formattedRecoveryKey: String?,
val displayTextFieldContents: Boolean,
val inProgress: Boolean, val inProgress: Boolean,
) )

View file

@ -22,6 +22,11 @@ open class RecoveryKeyViewStateProvider : PreviewParameterProvider<RecoveryKeyVi
} + sequenceOf( } + sequenceOf(
aRecoveryKeyViewState(recoveryKeyUserStory = RecoveryKeyUserStory.Enter, formattedRecoveryKey = aFormattedRecoveryKey().replace(" ", "")), aRecoveryKeyViewState(recoveryKeyUserStory = RecoveryKeyUserStory.Enter, formattedRecoveryKey = aFormattedRecoveryKey().replace(" ", "")),
aRecoveryKeyViewState(recoveryKeyUserStory = RecoveryKeyUserStory.Enter, formattedRecoveryKey = "This is a passphrase with spaces"), aRecoveryKeyViewState(recoveryKeyUserStory = RecoveryKeyUserStory.Enter, formattedRecoveryKey = "This is a passphrase with spaces"),
aRecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
formattedRecoveryKey = aFormattedRecoveryKey().replace(" ", ""),
displayTextFieldContents = false
),
) )
} }
@ -29,9 +34,11 @@ fun aRecoveryKeyViewState(
recoveryKeyUserStory: RecoveryKeyUserStory = RecoveryKeyUserStory.Setup, recoveryKeyUserStory: RecoveryKeyUserStory = RecoveryKeyUserStory.Setup,
formattedRecoveryKey: String? = null, formattedRecoveryKey: String? = null,
inProgress: Boolean = false, inProgress: Boolean = false,
displayTextFieldContents: Boolean = true,
) = RecoveryKeyViewState( ) = RecoveryKeyViewState(
recoveryKeyUserStory = recoveryKeyUserStory, recoveryKeyUserStory = recoveryKeyUserStory,
formattedRecoveryKey = formattedRecoveryKey, formattedRecoveryKey = formattedRecoveryKey,
displayTextFieldContents = displayTextFieldContents,
inProgress = inProgress, inProgress = inProgress,
) )

View file

@ -40,6 +40,7 @@ class SecureBackupEnterRecoveryKeyPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Enter, recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
formattedRecoveryKey = "", formattedRecoveryKey = "",
displayTextFieldContents = false,
inProgress = false, inProgress = false,
) )
) )
@ -61,6 +62,7 @@ class SecureBackupEnterRecoveryKeyPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Enter, recoveryKeyUserStory = RecoveryKeyUserStory.Enter,
formattedRecoveryKey = "1234", formattedRecoveryKey = "1234",
displayTextFieldContents = false,
inProgress = false, inProgress = false,
) )
) )

View file

@ -10,11 +10,12 @@ package io.element.android.features.securebackup.impl.enter
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performImeAction
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.securebackup.impl.R
import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -81,6 +82,23 @@ class SecureBackupEnterRecoveryKeyViewTest {
) )
} }
@Test
@Config(qualifiers = "h1024dp")
fun `toggling the visibility of the textfield changes it`() {
val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>()
val keyValue = aFormattedRecoveryKey()
rule.setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder))
// Initially, the text field should be visible
rule.onNodeWithText(keyValue).assertExists()
rule.onNodeWithContentDescription(rule.activity.getString(CommonStrings.a11y_hide_password)).performClick()
rule.waitForIdle()
recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(false))
}
@Test @Test
fun `validating from keyboard emits the expected event`() { fun `validating from keyboard emits the expected event`() {
val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>() val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>()

View file

@ -40,6 +40,7 @@ class SecureBackupSetupPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Setup, recoveryKeyUserStory = RecoveryKeyUserStory.Setup,
formattedRecoveryKey = null, formattedRecoveryKey = null,
displayTextFieldContents = true,
inProgress = false, inProgress = false,
) )
) )
@ -63,6 +64,7 @@ class SecureBackupSetupPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Setup, recoveryKeyUserStory = RecoveryKeyUserStory.Setup,
formattedRecoveryKey = null, formattedRecoveryKey = null,
displayTextFieldContents = true,
inProgress = true, inProgress = true,
) )
) )
@ -73,6 +75,7 @@ class SecureBackupSetupPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Setup, recoveryKeyUserStory = RecoveryKeyUserStory.Setup,
formattedRecoveryKey = A_RECOVERY_KEY, formattedRecoveryKey = A_RECOVERY_KEY,
displayTextFieldContents = true,
inProgress = false, inProgress = false,
) )
) )
@ -103,6 +106,7 @@ class SecureBackupSetupPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Change, recoveryKeyUserStory = RecoveryKeyUserStory.Change,
formattedRecoveryKey = null, formattedRecoveryKey = null,
displayTextFieldContents = true,
inProgress = false, inProgress = false,
) )
) )
@ -155,6 +159,7 @@ class SecureBackupSetupPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Change, recoveryKeyUserStory = RecoveryKeyUserStory.Change,
formattedRecoveryKey = null, formattedRecoveryKey = null,
displayTextFieldContents = true,
inProgress = true, inProgress = true,
) )
) )
@ -164,6 +169,7 @@ class SecureBackupSetupPresenterTest {
RecoveryKeyViewState( RecoveryKeyViewState(
recoveryKeyUserStory = RecoveryKeyUserStory.Change, recoveryKeyUserStory = RecoveryKeyUserStory.Change,
formattedRecoveryKey = FakeEncryptionService.FAKE_RECOVERY_KEY, formattedRecoveryKey = FakeEncryptionService.FAKE_RECOVERY_KEY,
displayTextFieldContents = true,
inProgress = false, inProgress = false,
) )
) )

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a1f1460739ae84a1231044e042e427d61cd7c94027035c1300aebf6695aec60a oid sha256:e3518f4ff808d3e61047834161935aa3603e3f8161288be14b87a1ea48c5e25f
size 30986 size 31968

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d046c96d9948e4b3754de60cb5dc1017e21c4e2b509f9e736cc0ef1def375af7 oid sha256:3bdd192ae952875c6c6882ec6b02ef4855e4862431c78cf29842e25700dd3d0e
size 41756 size 42791

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:77c2e604b4c8281f284333a28608fb41f86adf56c3b9cf205d1eda0677c6c441 oid sha256:23dce3fae5178c45a70171f762333a3e0f16e26b9b6c585ba7943a519de6e0dc
size 41973 size 42771

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7e9ad9b6fe78b5028209f9f9683553fb6db2d2fc2e64f215c6abc02b19baecc
size 35633

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0a0ca7c2dbd511607e61abc3165f8c2c8c70a6849e02415deb919ef90c9d16be oid sha256:e5e0e27543c3247402719dfe0bb0bcb2152b0d3aef7dc006cc55b36746c2fd67
size 29969 size 30923

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5f4b8abd3133c3754af7686266ab81ad7cae8f549609f3ddd53e1639e9671de3 oid sha256:a081e06078ac0ef89e3c9f643766bc9c48474168227fbf011a92e31517344d8b
size 40484 size 41374

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d77618c41e5c06b9f350ece10bfe02180516a6553dd69b642b77663144ceb238 oid sha256:d2cb1156bd56a8602d17d6db4e7e7089e244e6afbaf129d9ea02ba625d8a944c
size 40427 size 41173

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69d2b8e9f30b06110a735161faf6e20b505ec9395c57349fb19144f07489a03c
size 34700

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:777216c815b4198de503101e3e52fe371c0e1916799ac209aef993739ca39004 oid sha256:ac0bb1a0a9d500f89d20bed5190f7d4e442fec7cb8bd6710f21657ef0c298fe2
size 25699 size 26672

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:47d063becbbeec9185ae3ea6dd9921d8a6620a084b8f153061fc05cf57040652 oid sha256:910264e17c8737b5ee4efdca0853450524f21db7e7dd5a45424bfb87d221347b
size 25038 size 25796

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:777216c815b4198de503101e3e52fe371c0e1916799ac209aef993739ca39004 oid sha256:ac0bb1a0a9d500f89d20bed5190f7d4e442fec7cb8bd6710f21657ef0c298fe2
size 25699 size 26672

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:46f7f3867698746018f5f6c12ca9aaccad4545a50f3b5db9193fe06a0e232601 oid sha256:1fc08c8fa05f681d48ff886883aba32d71a74aa3db52e92964ad69f7fb367fd5
size 20575 size 21717

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1347518095b2a8abdd67a0843b3f4d06bc685dd37ef113035864e3d74cc28740
size 18955

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:def874f1d40cd699de708cd3b4c2f55f7634de919587c2da25a2773ecdd39e4a oid sha256:5fd5183e7442a8e5945b8ffd8585204ede5b69b91ffb8944585678b4d88dcbeb
size 15493 size 16528

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2191feda845f1e255fa4387f73cb06f334b6687d08580e8c00fa94d6046b16f4 oid sha256:c2b2f513d4523c1e00f8508c5531f300a71b7a7f2396d1d4e40fc605e4075e6c
size 15237 size 16235

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5200e8e2a0619bed9a9a6aad6a676a4292fe64d6d3ac11b55161887e44fd11f9 oid sha256:3c908fe1b613b28a3d12d49f98f2363e361f2cab4b8c35c89482802525c5ef34
size 24702 size 25773

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ba1b220dd3866ed16eefacba1a390c2aafdf66e06db765743493177699c8c392 oid sha256:0fb8568e2cd07f9c856b1e3beac52a1e6979878fd8c6fdb6b57b4faecaa2c3e1
size 24173 size 24961

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5200e8e2a0619bed9a9a6aad6a676a4292fe64d6d3ac11b55161887e44fd11f9 oid sha256:3c908fe1b613b28a3d12d49f98f2363e361f2cab4b8c35c89482802525c5ef34
size 24702 size 25773

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f9dc5974fea6f46ad50b224a1aabbcde69fd87121578343df5d13eb9a5ec0cfd oid sha256:a3465fcc599b8fea97ad6347abd9525b983df5d2f4a28c0ce4a3b9743ef85c65
size 19488 size 20600

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba81a7ac256c647460e5661ec36f78a4abb592fa98f9a5e472590d7234154dd7
size 18101

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:696cead69a01bf5fce8b382a2b20b6823cdf66d47a30608cbbda4a3062bc6a05 oid sha256:963c535c88475dd6f659ce4f22494314f29401b70b2a407a689bdccfb3b062c1
size 14743 size 15723

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bea33b82255867ef88c363d502872ced6cc063d877a4ea3da389b4fc75cfafa3 oid sha256:985b5897ba1ae81ff8de6ac90daec70584f1586f0c5ff762ec154a9fd50e2cae
size 14737 size 15664