diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenConstraintLayout.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenConstraintLayout.kt
deleted file mode 100644
index b98cdb817f7..00000000000
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenConstraintLayout.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.fsck.k9.ui.compose
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.TextView
-import androidx.constraintlayout.widget.ConstraintLayout
-
-/**
- * Custom [ConstraintLayout] that returns an appropriate baseline value for our recipient token layout.
- */
-class RecipientTokenConstraintLayout @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
-) : ConstraintLayout(context, attrs, defStyleAttr) {
- private lateinit var textView: TextView
-
- override fun onFinishInflate() {
- super.onFinishInflate()
- textView = findViewById(android.R.id.text1)
- }
-
- override fun getBaseline(): Int {
- return textView.top + textView.baseline
- }
-}
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenLayout.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenLayout.kt
new file mode 100644
index 00000000000..668466197e9
--- /dev/null
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenLayout.kt
@@ -0,0 +1,72 @@
+package com.fsck.k9.ui.compose
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import com.fsck.k9.ui.R
+
+/**
+ * Custom layout for recipient tokens.
+ *
+ * Note: This layout is tightly coupled to recipient_token_item.xml
+ */
+class RecipientTokenLayout(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
+ private lateinit var background: View
+ private lateinit var contactPicture: View
+ private lateinit var recipientName: View
+ private lateinit var cryptoStatus: View
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ background = findViewById(R.id.background)
+ contactPicture = findViewById(R.id.contact_photo)
+ recipientName = findViewById(android.R.id.text1)
+ cryptoStatus = findViewById(R.id.crypto_status_container)
+ }
+
+ // Return an appropriate baseline so the view is properly aligned with user-entered text in RecipientSelectView
+ override fun getBaseline(): Int {
+ return recipientName.top + recipientName.baseline
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ recipientName.measure(widthMeasureSpec, heightMeasureSpec)
+ cryptoStatus.measure(widthMeasureSpec, heightMeasureSpec)
+
+ val height = recipientName.measuredHeight.coerceAtLeast(minimumHeight)
+
+ val contactPictureWidth = height
+ val fixedWidthComponent = contactPictureWidth + cryptoStatus.measuredWidth
+ val desiredWidth = fixedWidthComponent + recipientName.measuredWidth
+
+ if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+ setMeasuredDimension(desiredWidth, height)
+ } else {
+ // Re-measure recipient name view with final width constraint
+ val width = desiredWidth.coerceAtMost(MeasureSpec.getSize(widthMeasureSpec))
+ val recipientNameWidth = width - fixedWidthComponent
+ val recipientNameWidthMeasureSpec = MeasureSpec.makeMeasureSpec(recipientNameWidth, MeasureSpec.AT_MOST)
+ recipientName.measure(recipientNameWidthMeasureSpec, heightMeasureSpec)
+
+ setMeasuredDimension(width, height)
+ }
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ val contactPictureSize = height
+ background.layout(contactPictureSize / 2, 0, width, height)
+ contactPicture.layout(0, 0, contactPictureSize, contactPictureSize)
+
+ val recipientNameHeight = recipientName.measuredHeight
+ val recipientNameTop = (height - recipientNameHeight) / 2
+ recipientName.layout(
+ contactPictureSize,
+ recipientNameTop,
+ contactPictureSize + recipientName.measuredWidth,
+ recipientNameTop + recipientNameHeight,
+ )
+
+ cryptoStatus.layout(width - cryptoStatus.measuredWidth, 0, width, cryptoStatus.measuredHeight)
+ }
+}
diff --git a/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml b/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml
index 3ee1a30f30c..e9260afb43b 100644
--- a/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml
+++ b/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml
@@ -1,10 +1,11 @@
-
-
-
-
+ >
-
+
-
+
+
+
+
+
-
+
diff --git a/legacy/ui/legacy/src/main/res/values/themes.xml b/legacy/ui/legacy/src/main/res/values/themes.xml
index b8b20323ec6..7b1b21eea46 100644
--- a/legacy/ui/legacy/src/main/res/values/themes.xml
+++ b/legacy/ui/legacy/src/main/res/values/themes.xml
@@ -9,6 +9,14 @@
- ?android:attr/windowBackground
- ?attr/messageListRegularItemBackgroundColor
- ?attr/messageListRegularItemBackgroundColor
+ - #ccc
+ - #000
+ - #FF8800
+ - #CC0000
+ - #669900
+ - #336699
+ - #bbb
+ - #888
diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/compose/RecipientTokenLayoutTest.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/compose/RecipientTokenLayoutTest.kt
new file mode 100644
index 00000000000..61ccce1a5c5
--- /dev/null
+++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/compose/RecipientTokenLayoutTest.kt
@@ -0,0 +1,140 @@
+package com.fsck.k9.ui.compose
+
+import android.view.View
+import android.view.View.MeasureSpec
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import com.fsck.k9.RobolectricTest
+import com.fsck.k9.ui.R
+import com.google.android.material.textview.MaterialTextView
+import org.junit.Before
+import org.junit.Test
+import org.robolectric.Robolectric
+
+class RecipientTokenLayoutTest : RobolectricTest() {
+ private lateinit var activity: AppCompatActivity
+
+ private lateinit var recipientTokenLayout: RecipientTokenLayout
+
+ @Before
+ fun setUp() {
+ activity = Robolectric.buildActivity(AppCompatActivity::class.java).get()
+ activity.setTheme(R.style.Theme_Legacy_Test)
+
+ recipientTokenLayout =
+ activity.layoutInflater.inflate(R.layout.recipient_token_item, null, false) as RecipientTokenLayout
+ }
+
+ @Test
+ fun `measure with width constraint`() {
+ val maxWidth = 100
+ recipientTokenLayout.recipientNameView.text = "recipient@domain.example"
+
+ recipientTokenLayout.measure(
+ MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ )
+
+ assertThat(recipientTokenLayout.measuredWidth).isEqualTo(81)
+ assertThat(recipientTokenLayout.measuredHeight).isEqualTo(49)
+ }
+
+ @Test
+ fun `respect max width when measuring`() {
+ val maxWidth = 70
+ recipientTokenLayout.recipientNameView.text = "recipient@domain.example"
+
+ recipientTokenLayout.measure(
+ MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ )
+
+ assertThat(recipientTokenLayout.measuredWidth).isEqualTo(maxWidth)
+ }
+
+ @Test
+ fun `layout without reaching the maximum width`() {
+ val maxWidth = 100
+ recipientTokenLayout.recipientNameView.text = "recipient@domain.example"
+ recipientTokenLayout.measure(
+ MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ )
+
+ recipientTokenLayout.layout(0, 0, recipientTokenLayout.measuredWidth, recipientTokenLayout.measuredHeight)
+
+ assertThat(recipientTokenLayout.width).isEqualTo(81)
+ assertThat(recipientTokenLayout.height).isEqualTo(49)
+
+ assertThat(recipientTokenLayout.contactPictureView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.contactPictureView.bottom).isEqualTo(49)
+ assertThat(recipientTokenLayout.contactPictureView.left).isEqualTo(0)
+ assertThat(recipientTokenLayout.contactPictureView.right).isEqualTo(49)
+
+ assertThat(recipientTokenLayout.recipientNameView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.recipientNameView.bottom).isEqualTo(49)
+ assertThat(recipientTokenLayout.recipientNameView.left).isEqualTo(49)
+ assertThat(recipientTokenLayout.recipientNameView.right).isEqualTo(81)
+
+ assertThat(recipientTokenLayout.cryptoStatusView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.cryptoStatusView.bottom).isEqualTo(0)
+ assertThat(recipientTokenLayout.cryptoStatusView.left).isEqualTo(81)
+ assertThat(recipientTokenLayout.cryptoStatusView.right).isEqualTo(81)
+
+ assertThat(recipientTokenLayout.backgroundView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.backgroundView.bottom).isEqualTo(49)
+ assertThat(recipientTokenLayout.backgroundView.left).isEqualTo(24)
+ assertThat(recipientTokenLayout.backgroundView.right).isEqualTo(81)
+ }
+
+ @Test
+ fun `layout with ellipsized text and crypto status indicator`() {
+ val maxWidth = 70
+ recipientTokenLayout.recipientNameView.text = "recipient@domain.example"
+ recipientTokenLayout.cryptoStatusView.findViewById(R.id.contact_crypto_status_icon).isVisible = true
+ recipientTokenLayout.measure(
+ MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ )
+
+ recipientTokenLayout.layout(0, 0, recipientTokenLayout.measuredWidth, recipientTokenLayout.measuredHeight)
+
+ assertThat(recipientTokenLayout.width).isEqualTo(70)
+ assertThat(recipientTokenLayout.height).isEqualTo(49)
+
+ assertThat(recipientTokenLayout.contactPictureView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.contactPictureView.bottom).isEqualTo(49)
+ assertThat(recipientTokenLayout.contactPictureView.left).isEqualTo(0)
+ assertThat(recipientTokenLayout.contactPictureView.right).isEqualTo(49)
+
+ assertThat(recipientTokenLayout.recipientNameView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.recipientNameView.bottom).isEqualTo(49)
+ assertThat(recipientTokenLayout.recipientNameView.left).isEqualTo(49)
+ assertThat(recipientTokenLayout.recipientNameView.right).isEqualTo(58)
+
+ assertThat(recipientTokenLayout.cryptoStatusView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.cryptoStatusView.bottom).isEqualTo(12)
+ assertThat(recipientTokenLayout.cryptoStatusView.left).isEqualTo(58)
+ assertThat(recipientTokenLayout.cryptoStatusView.right).isEqualTo(70)
+
+ assertThat(recipientTokenLayout.backgroundView.top).isEqualTo(0)
+ assertThat(recipientTokenLayout.backgroundView.bottom).isEqualTo(49)
+ assertThat(recipientTokenLayout.backgroundView.left).isEqualTo(24)
+ assertThat(recipientTokenLayout.backgroundView.right).isEqualTo(70)
+ }
+}
+
+private val RecipientTokenLayout.backgroundView: View
+ get() = findViewById(R.id.background)
+
+private val RecipientTokenLayout.contactPictureView: View
+ get() = findViewById(R.id.contact_photo)
+
+private val RecipientTokenLayout.recipientNameView: MaterialTextView
+ get() = findViewById(android.R.id.text1)
+
+private val RecipientTokenLayout.cryptoStatusView: ViewGroup
+ get() = findViewById(R.id.crypto_status_container)