Add key transparency UI.

This commit is contained in:
Michelle Tang
2026-01-27 10:53:28 -05:00
committed by Greyson Parrelli
parent 279f9578cc
commit 69f4c89f84
10 changed files with 373 additions and 74 deletions

View File

@@ -28,6 +28,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.widget.ImageViewCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.button.MaterialButton
import org.signal.core.util.dp
import org.signal.libsignal.protocol.fingerprint.Fingerprint
import org.thoughtcrime.securesms.R
@@ -65,6 +66,7 @@ class SafetyNumberQrView : ConstraintLayout {
val qrCodeContainer: View
val shareButton: ImageView
val verifyButton: MaterialButton
private val loading: View
private val qrCode: ImageView
@@ -97,6 +99,7 @@ class SafetyNumberQrView : ConstraintLayout {
)
shareButton = findViewById(R.id.share)
verifyButton = findViewById(R.id.verify_button)
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {

View File

@@ -0,0 +1,135 @@
package org.thoughtcrime.securesms.verify
import android.os.Bundle
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.util.getSerializableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.BottomSheetUtil
/**
* Bottom sheet info explaining the results of automatic key verification
*/
class EncryptionVerifiedSheet : ComposeBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 0.67f
companion object {
private const val ARG_STATUS = "arg.status"
private const val ARG_NAME = "arg.name"
@JvmStatic
fun show(fragmentManager: FragmentManager, status: AutomaticVerificationStatus, name: String) {
EncryptionVerifiedSheet().apply {
arguments = Bundle().apply {
putSerializable(ARG_STATUS, status)
putString(ARG_NAME, name)
}
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
@Composable
override fun SheetContent() {
VerifiedSheet(
verifiedStatus = requireArguments().getSerializableCompat(ARG_STATUS, AutomaticVerificationStatus::class.java)!!,
name = requireArguments().getString(ARG_NAME, "")
) {
this.dismissAllowingStateLoss()
}
}
}
@Composable
fun VerifiedSheet(
verifiedStatus: AutomaticVerificationStatus = AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY,
name: String = "",
onClick: () -> Unit = {}
) {
val (icon, title, body) = when (verifiedStatus) {
AutomaticVerificationStatus.VERIFIED -> {
Triple(
ImageVector.vectorResource(R.drawable.symbol_check_48),
stringResource(R.string.EncryptionVerifiedSheet__title_success),
stringResource(R.string.EncryptionVerifiedSheet__body_success)
)
}
AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> {
Triple(
ImageVector.vectorResource(R.drawable.symbol_info_48),
stringResource(R.string.EncryptionVerifiedSheet__title_unavailable),
stringResource(R.string.EncryptionVerifiedSheet__body_unavailable)
)
}
else -> {
Triple(
ImageVector.vectorResource(R.drawable.symbol_info_48),
stringResource(R.string.EncryptionVerifiedSheet__title_no_longer_unavailable),
stringResource(R.string.EncryptionVerifiedSheet__body_no_longer_unavailable, name)
)
}
}
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.horizontalGutters()
) {
BottomSheets.Handle()
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.padding(top = 28.dp)
)
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.padding(vertical = 12.dp)
)
Text(
text = body,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Buttons.LargeTonal(
onClick = onClick,
modifier = Modifier.defaultMinSize(minWidth = 220.dp).padding(vertical = 40.dp)
) {
Text(stringResource(id = android.R.string.ok))
}
}
}
@DayNightPreviews
@Composable
fun FinishedSheetSheetPreview() {
Previews.BottomSheetContentPreview {
VerifiedSheet()
}
}

View File

@@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.text.TextUtils
import android.view.ContextMenu
@@ -12,9 +13,9 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnScrollChangedListener
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -32,13 +33,14 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.nio.charset.StandardCharsets
import java.util.Locale
/**
* Fragment to display a user's identity key.
*/
class VerifyDisplayFragment : Fragment(), OnScrollChangedListener {
class VerifyDisplayFragment : Fragment() {
private lateinit var viewModel: VerifySafetyNumberViewModel
private val binding by ViewBinderDelegate(VerifyDisplayFragmentBinding::bind)
@@ -71,11 +73,19 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener {
updateVerifyButton(requireArguments().getBoolean(VERIFIED_STATE, false), false)
binding.verifyButton.setOnClickListener { updateVerifyButton(!currentVerifiedState, true) }
binding.scrollView.viewTreeObserver?.addOnScrollChangedListener(this)
binding.automaticVerification.visible = RemoteConfig.keyTransparency
binding.safetyQrView.verifyButton.setOnClickListener { updateVerifyButton(!currentVerifiedState, true) }
binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
binding.toolbar.setTitle(R.string.AndroidManifest__verify_safety_number)
binding.caption.text = getString(R.string.verify_display_fragment__auto_verify_not_available)
binding.caption.setLink(getString(R.string.verify_display_fragment__link))
binding.caption.setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary))
viewModel.getAutomaticVerification().observe(viewLifecycleOwner) { status ->
updateStatus(status)
}
viewModel.recipient.observe(this) { recipient: Recipient -> setRecipientText(recipient) }
viewModel.getFingerprint().observe(viewLifecycleOwner) { fingerprint: SafetyNumberFingerprint? ->
if (fingerprint == null) {
@@ -99,6 +109,48 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener {
}
}
private fun updateStatus(status: AutomaticVerificationStatus) {
when (status) {
AutomaticVerificationStatus.NONE -> {
binding.autoVerifyText.text = getString(R.string.verify_display_fragment__verify_automatic)
binding.autoVerifyIcon.setImageResource(R.drawable.symbol_key_24)
binding.autoVerifyIcon.imageTintList = null
binding.autoVerifyMore.visible = false
}
AutomaticVerificationStatus.VERIFYING -> {
binding.autoVerifyText.text = getString(R.string.verify_display_fragment__verifying)
binding.autoVerifyMore.visible = false
}
AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> {
binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_unavailable)
binding.autoVerifyIcon.setImageResource(R.drawable.symbol_info_24)
ImageViewCompat.setImageTintList(binding.autoVerifyIcon, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant)))
binding.autoVerifyMore.visible = true
}
AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY -> {
binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_unavailable)
binding.autoVerifyIcon.setImageResource(R.drawable.symbol_info_24)
ImageViewCompat.setImageTintList(binding.autoVerifyIcon, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant)))
binding.autoVerifyMore.visible = true
}
AutomaticVerificationStatus.VERIFIED -> {
binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_verified)
binding.autoVerifyIcon.setImageResource(R.drawable.symbol_check_filled_circle_24)
binding.autoVerifyIcon.imageTintList = null
binding.autoVerifyMore.visible = true
}
}
if (status == AutomaticVerificationStatus.VERIFYING) {
binding.autoVerifySpinner.visible = true
binding.autoVerifyIcon.visible = false
} else {
binding.autoVerifySpinner.visible = false
binding.autoVerifyIcon.visible = true
}
binding.autoVerifyMore.setOnClickListener { EncryptionVerifiedSheet.show(parentFragmentManager, status, viewModel.recipient.resolve().getDisplayName(requireContext())) }
}
private fun initializeViewModel() {
val recipientId = requireArguments().requireParcelableCompat(RECIPIENT_ID, RecipientId::class.java)
val localIdentity = requireArguments().requireParcelableCompat(LOCAL_IDENTITY, IdentityKeyParcelable::class.java).get()!!
@@ -121,7 +173,6 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener {
animateFailure()
}
}
ThreadUtil.postToMain { onScrollChanged() }
}
override fun onCreateContextMenu(
@@ -252,34 +303,15 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener {
private fun updateVerifyButton(verified: Boolean, update: Boolean) {
currentVerifiedState = verified
if (verified) {
binding.verifyButton.setText(R.string.verify_display_fragment__clear_verification)
binding.safetyQrView.verifyButton.setText(R.string.verify_display_fragment__clear_verification)
} else {
binding.verifyButton.setText(R.string.verify_display_fragment__mark_as_verified)
binding.safetyQrView.verifyButton.setText(R.string.verify_display_fragment__mark_as_verified)
}
if (update) {
viewModel.updateSafetyNumberVerification(verified)
}
}
override fun onScrollChanged() {
if (binding.scrollView.canScrollVertically(-1) && currentFingerprint != null) {
if (binding.toolbarShadow.visibility != View.VISIBLE) {
ViewUtil.fadeIn(binding.toolbarShadow, 250)
}
} else {
if (binding.toolbarShadow.visibility != View.GONE) {
ViewUtil.fadeOut(binding.toolbarShadow, 250)
}
}
if (binding.scrollView.canScrollVertically(1)) {
if (binding.verifyIdentityBottomShadow.visibility != View.VISIBLE) {
ViewUtil.fadeIn(binding.verifyIdentityBottomShadow, 250)
}
} else {
ViewUtil.fadeOut(binding.verifyIdentityBottomShadow, 250)
}
}
internal interface Callback {
fun onQrCodeContainerClicked()
}

View File

@@ -39,6 +39,7 @@ class VerifySafetyNumberViewModel(
val recipient: LiveRecipient = Recipient.live(recipientId)
private val fingerprintLiveData = MutableLiveData<SafetyNumberFingerprint?>()
private val automaticVerificationLiveData = MutableLiveData(AutomaticVerificationStatus.NONE)
init {
initializeFingerprints()
@@ -69,6 +70,10 @@ class VerifySafetyNumberViewModel(
return fingerprintLiveData
}
fun getAutomaticVerification(): LiveData<AutomaticVerificationStatus> {
return automaticVerificationLiveData
}
fun updateSafetyNumberVerification(verified: Boolean) {
val recipientId: RecipientId = recipientId
val context: Context = AppDependencies.application
@@ -159,3 +164,11 @@ data class SafetyNumberFingerprint(
return result
}
}
enum class AutomaticVerificationStatus {
NONE,
VERIFYING,
UNAVAILABLE_PERMANENT,
UNAVAILABLE_TEMPORARY,
VERIFIED
}

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,24m-21.5,0a21.5,21.5 0,1 1,43 0a21.5,21.5 0,1 1,-43 0"
android:fillColor="#D2D8FE"/>
<path
android:pathData="M24,5.5C13.783,5.5 5.5,13.783 5.5,24C5.5,34.217 13.783,42.5 24,42.5C34.217,42.5 42.5,34.217 42.5,24C42.5,13.783 34.217,5.5 24,5.5ZM2.5,24C2.5,12.126 12.126,2.5 24,2.5C35.874,2.5 45.5,12.126 45.5,24C45.5,35.874 35.874,45.5 24,45.5C12.126,45.5 2.5,35.874 2.5,24Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M32.795,15.228C33.498,15.667 33.711,16.593 33.272,17.295L23.272,33.295C23.014,33.707 22.573,33.969 22.087,33.998C21.601,34.026 21.132,33.817 20.829,33.437L14.829,25.937C14.311,25.29 14.416,24.346 15.063,23.829C15.71,23.311 16.654,23.416 17.171,24.063L21.85,29.911L30.728,15.705C31.167,15.002 32.092,14.789 32.795,15.228Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,1.5C6.201,1.5 1.5,6.201 1.5,12C1.5,17.799 6.201,22.5 12,22.5C17.799,22.5 22.5,17.799 22.5,12C22.5,6.201 17.799,1.5 12,1.5ZM16.464,7.508C16.874,7.764 16.998,8.304 16.742,8.714L11.742,16.714C11.592,16.954 11.334,17.107 11.051,17.124C10.768,17.14 10.494,17.018 10.317,16.797L7.317,13.047C7.015,12.669 7.076,12.119 7.453,11.817C7.831,11.515 8.381,11.576 8.683,11.953L10.912,14.74L15.258,7.786C15.514,7.376 16.054,7.252 16.464,7.508Z"
android:fillColor="#4CAF50"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,24m-21.5,0a21.5,21.5 0,1 1,43 0a21.5,21.5 0,1 1,-43 0"
android:fillColor="#D2D8FE"/>
<path
android:pathData="M24.604,20.263C24.775,20.277 24.977,20.31 25.181,20.413C25.463,20.557 25.693,20.787 25.837,21.069C25.94,21.273 25.973,21.475 25.987,21.646C26,21.802 26,21.987 26,22.176L26,32.5H28.5C29.19,32.5 29.75,33.06 29.75,33.75C29.75,34.44 29.19,35 28.5,35H20.5C19.81,35 19.25,34.44 19.25,33.75C19.25,33.06 19.81,32.5 20.5,32.5H23V22.75H21C20.31,22.75 19.75,22.19 19.75,21.5C19.75,20.81 20.31,20.25 21,20.25L24.074,20.25C24.263,20.25 24.448,20.25 24.604,20.263Z"
android:fillColor="#3B45FD"/>
<path
android:pathData="M21.498,14.502C21.498,13.12 22.618,12 24,12C25.382,12 26.502,13.12 26.502,14.502C26.502,15.884 25.382,17.005 24,17.005C22.618,17.005 21.498,15.884 21.498,14.502Z"
android:fillColor="#3B45FD"/>
<path
android:pathData="M2.5,24C2.5,12.126 12.126,2.5 24,2.5C35.874,2.5 45.5,12.126 45.5,24C45.5,35.874 35.874,45.5 24,45.5C12.126,45.5 2.5,35.874 2.5,24ZM24,5.5C13.783,5.5 5.5,13.783 5.5,24C5.5,34.217 13.783,42.5 24,42.5C34.217,42.5 42.5,34.217 42.5,24C42.5,13.783 34.217,5.5 24,5.5Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
</vector>

View File

@@ -105,7 +105,6 @@
android:layout_marginBottom="49dp"
android:layout_marginHorizontal="24dp"
app:layout_constraintTop_toBottomOf="@id/qr_code_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:clickable="true"
@@ -229,4 +228,23 @@
android:layout_margin="24dp"
/>
<com.google.android.material.button.MaterialButton
android:id="@+id/verify_button"
style="@style/Signal.Widget.Button.Base.Tonal"
android:backgroundTint="@color/signal_colorSurface1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/verify_display_fragment__mark_as_verified"
app:layout_constraintTop_toBottomOf="@id/number_table"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Space
android:layout_width="match_parent"
android:layout_height="24dp"
app:layout_constraintTop_toBottomOf="@id/verify_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</merge>

View File

@@ -21,21 +21,13 @@
app:titleTextAppearance="@style/Signal.Text.TitleLarge"
tools:title="@string/AndroidManifest__verify_safety_number" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/header_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="toolbar"/>
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintTop_toBottomOf="@id/header_barrier"
app:layout_constraintBottom_toTopOf="@id/verify_button_container">
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="fill_parent"
@@ -65,44 +57,78 @@
android:text="@string/verify_display_fragment__pnp_verify_safety_numbers_explanation_with_s"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:textColorLink="@color/signal_colorPrimary" />
<LinearLayout
android:id="@+id/automatic_verification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Signal.Text.TitleSmall"
android:layout_marginStart="24dp"
android:layout_marginVertical="12dp"
android:text="@string/verify_display_fragment__automatic" />
<LinearLayout
android:id="@+id/auto_verify_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/rounded_rectangle_38"
android:backgroundTint="@color/signal_colorSurface1"
android:paddingVertical="16dp"
android:paddingHorizontal="20dp"
android:gravity="center_vertical"
android:layout_marginHorizontal="16dp"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/auto_verify_spinner"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminate="true"
android:indeterminateTint="@color/signal_colorOutline"
android:visibility="visible" />
<ImageView
android:id="@+id/auto_verify_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/symbol_key_24" />
<TextView
android:id="@+id/auto_verify_text"
style="@style/Signal.Text.BodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/verify_display_fragment__verify_automatic" />
<ImageView
android:id="@+id/auto_verify_more"
android:src="@drawable/symbol_chevron_right_compact_bold_16"
android:layout_marginStart="2dp"
app:tint="@color/signal_colorOutline"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="12dp"
android:paddingHorizontal="@dimen/gutter"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<View
android:id="@+id/toolbar_shadow"
android:layout_width="match_parent"
android:layout_height="5dp"
android:visibility="gone"
android:background="@drawable/toolbar_shadow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header_barrier"
tools:visibility="visible" />
<View
android:id="@+id/verify_identity_bottom_shadow"
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="@drawable/bottom_toolbar_shadow"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/verify_button_container" />
<FrameLayout
android:id="@+id/verify_button_container"
android:layout_width="match_parent"
android:layout_height="96dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/verify_button"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:layout_gravity="center_horizontal"
android:text="@string/verify_display_fragment__mark_as_verified"
android:textAllCaps="false" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -3822,6 +3822,32 @@
<string name="verify_display_fragment__loading">Loading…</string>
<string name="verify_display_fragment__mark_as_verified">Mark as verified</string>
<string name="verify_display_fragment__clear_verification">Clear verification</string>
<!-- Header title for automatic key verification -->
<string name="verify_display_fragment__automatic">Automatic Key Verification</string>
<!-- Button text to verify automatically -->
<string name="verify_display_fragment__verify_automatic">Verify automatically</string>
<!-- Loading text when verifying -->
<string name="verify_display_fragment__verifying">Verifying encryption…</string>
<!-- Success text shown when encryption is verified -->
<string name="verify_display_fragment__encryption_verified">Encryption verified</string>
<!-- Text shown when verification fails -->
<string name="verify_display_fragment__encryption_unavailable">Auto-verification unavailable</string>
<!-- Caption text explaining more about automatic verification -->
<string name="verify_display_fragment__auto_verify_not_available">Auto-verification is not available for all chats.</string>
<string name="verify_display_fragment__link" translatable="false">https://signal.org/redirect/safety-numbers</string>
<!-- Bottom sheet title when encryption is auto-verified -->
<string name="EncryptionVerifiedSheet__title_success">Encryption was auto-verified for this chat</string>
<!-- Bottom sheet body when encryption is auto-verified -->
<string name="EncryptionVerifiedSheet__body_success">For contacts youre connected to by phone number, Signal can automatically confirm whether the connection is secure using a process called key transparency. For added security, verify end-to-end encryption manually by comparing the numbers on the previous screen or scanning the code on their device.</string>
<!-- Bottom sheet title when encryption is no longer auto-verified -->
<string name="EncryptionVerifiedSheet__title_no_longer_unavailable">Auto-verification is no longer available for this chat</string>
<!-- Bottom sheet body when encryption is no longer auto-verified. Placeholder is the name of the person. -->
<string name="EncryptionVerifiedSheet__body_no_longer_unavailable">Signal can no longer automatically verify the encryption for this chat. This is likely because %1$s changed their phone number. Verify end-to-end encryption manually by comparing the numbers on the previous screen or scanning the code on their device.</string>
<!-- Bottom sheet title when encryption cannot be auto-verified -->
<string name="EncryptionVerifiedSheet__title_unavailable">Auto-verification not available for this chat</string>
<!-- Bottom sheet body when encryption cannot be auto-verified -->
<string name="EncryptionVerifiedSheet__body_unavailable">Signal can only automatically verify the encryption in chats where youre connected to someone via a phone number. If the chat was started with a username or a group in common, verify end-to-end encryption by comparing the numbers on the previous screen or scanning the code on their device.</string>
<!-- Title for auto verification education sheet -->
<string name="VerifyAutomaticallyEducationSheet__title">Signal now auto-verifies end-to-end encryption</string>