Respect "show contact names" setting in message details

This commit is contained in:
cketti 2023-02-08 13:46:30 +01:00
parent fba8e8aca3
commit 74f40dd0bd
6 changed files with 213 additions and 19 deletions

View file

@ -10,10 +10,13 @@ val messageDetailsUiModule = module {
messageRepository = get(),
contactSettingsProvider = get(),
contacts = get(),
clipboardManager = get()
clipboardManager = get(),
accountManager = get(),
participantFormatter = get()
)
}
factory { ContactSettingsProvider() }
factory { AddToContactsLauncher() }
factory { ShowContactLauncher() }
factory { createMessageDetailsParticipantFormatter(contactNameProvider = get()) }
}

View file

@ -0,0 +1,57 @@
package com.fsck.k9.ui.messagedetails
import android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import com.fsck.k9.Account
import com.fsck.k9.K9
import com.fsck.k9.helper.ContactNameProvider
import com.fsck.k9.mail.Address
/**
* Get the display name for a participant to be shown in the message details screen.
*/
internal interface MessageDetailsParticipantFormatter {
fun getDisplayName(address: Address, account: Account): CharSequence?
}
internal class RealMessageDetailsParticipantFormatter(
private val contactNameProvider: ContactNameProvider,
private val showContactNames: Boolean,
private val contactNameColor: Int?
) : MessageDetailsParticipantFormatter {
override fun getDisplayName(address: Address, account: Account): CharSequence? {
val identityDisplayName = account.findIdentity(address)?.name
if (identityDisplayName != null) {
return identityDisplayName
}
return if (showContactNames) {
getContactNameOrNull(address) ?: address.personal
} else {
address.personal
}
}
private fun getContactNameOrNull(address: Address): CharSequence? {
val contactName = contactNameProvider.getNameForAddress(address.address) ?: return null
return if (contactNameColor != null) {
SpannableString(contactName).apply {
setSpan(ForegroundColorSpan(contactNameColor), 0, contactName.length, SPAN_EXCLUSIVE_EXCLUSIVE)
}
} else {
contactName
}
}
}
internal fun createMessageDetailsParticipantFormatter(
contactNameProvider: ContactNameProvider
): MessageDetailsParticipantFormatter {
return RealMessageDetailsParticipantFormatter(
contactNameProvider = contactNameProvider,
showContactNames = K9.isShowContactName,
contactNameColor = if (K9.isChangeContactNameColor) K9.contactNameColor else null
)
}

View file

@ -21,9 +21,13 @@ data class CryptoDetails(
)
data class Participant(
val address: Address,
val displayName: CharSequence?,
val emailAddress: String,
val contactLookupUri: Uri?
) {
val isInContacts: Boolean
get() = contactLookupUri != null
val address: Address
get() = Address(emailAddress, displayName?.toString())
}

View file

@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fsck.k9.Account
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.helper.ClipboardManager
import com.fsck.k9.helper.Contacts
@ -11,6 +12,7 @@ import com.fsck.k9.mail.Address
import com.fsck.k9.mailstore.CryptoResultAnnotation
import com.fsck.k9.mailstore.MessageDate
import com.fsck.k9.mailstore.MessageRepository
import com.fsck.k9.preferences.AccountManager
import com.fsck.k9.ui.R
import com.fsck.k9.view.MessageCryptoDisplayStatus
import java.text.DateFormat
@ -27,7 +29,9 @@ internal class MessageDetailsViewModel(
private val messageRepository: MessageRepository,
private val contactSettingsProvider: ContactSettingsProvider,
private val contacts: Contacts,
private val clipboardManager: ClipboardManager
private val clipboardManager: ClipboardManager,
private val accountManager: AccountManager,
private val participantFormatter: MessageDetailsParticipantFormatter
) : ViewModel() {
private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.getDefault())
private val uiState = MutableStateFlow<MessageDetailsState>(MessageDetailsState.Loading)
@ -39,18 +43,19 @@ internal class MessageDetailsViewModel(
fun loadData(messageReference: MessageReference): StateFlow<MessageDetailsState> {
viewModelScope.launch(Dispatchers.IO) {
uiState.value = try {
val account = accountManager.getAccount(messageReference.accountUuid) ?: error("Account not found")
val messageDetails = messageRepository.getMessageDetails(messageReference)
val senderList = messageDetails.sender?.let { listOf(it) } ?: emptyList()
val messageDetailsUi = MessageDetailsUi(
date = buildDisplayDate(messageDetails.date),
cryptoDetails = cryptoResult?.toCryptoDetails(),
from = messageDetails.from.toParticipants(),
sender = senderList.toParticipants(),
replyTo = messageDetails.replyTo.toParticipants(),
to = messageDetails.to.toParticipants(),
cc = messageDetails.cc.toParticipants(),
bcc = messageDetails.bcc.toParticipants()
from = messageDetails.from.toParticipants(account),
sender = senderList.toParticipants(account),
replyTo = messageDetails.replyTo.toParticipants(account),
to = messageDetails.to.toParticipants(account),
cc = messageDetails.cc.toParticipants(account),
bcc = messageDetails.bcc.toParticipants(account)
)
MessageDetailsState.DataLoaded(
@ -82,11 +87,15 @@ internal class MessageDetailsViewModel(
)
}
private fun List<Address>.toParticipants(): List<Participant> {
private fun List<Address>.toParticipants(account: Account): List<Participant> {
return this.map { address ->
val displayName = participantFormatter.getDisplayName(address, account)
val emailAddress = address.address
Participant(
address = address,
contactLookupUri = contacts.getContactUri(address.address)
displayName = displayName,
emailAddress = emailAddress,
contactLookupUri = contacts.getContactUri(emailAddress)
)
}
}

View file

@ -38,20 +38,18 @@ internal class ParticipantItem(
override fun bindView(item: ParticipantItem, payloads: List<Any>) {
val participant = item.participant
val address = participant.address
val participantName = address.personal
if (participantName != null) {
name.text = participantName
email.text = address.address
if (participant.displayName != null) {
name.text = participant.displayName
email.text = participant.emailAddress
} else {
name.text = address.address
name.text = participant.emailAddress
email.isVisible = false
}
menuAddContact.isVisible = !participant.isInContacts
if (item.showContactsPicture) {
item.contactPictureLoader.setContactPicture(contactPicture, address)
item.contactPictureLoader.setContactPicture(contactPicture, participant.address)
} else {
contactPicture.isVisible = false
}

View file

@ -0,0 +1,123 @@
package com.fsck.k9.ui.messagedetails
import android.graphics.Color
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import androidx.core.text.getSpans
import com.fsck.k9.Account
import com.fsck.k9.Identity
import com.fsck.k9.RobolectricTest
import com.fsck.k9.helper.ContactNameProvider
import com.fsck.k9.mail.Address
import com.google.common.truth.Truth.assertThat
import org.junit.Test
private const val IDENTITY_NAME = "Alice"
private const val IDENTITY_ADDRESS = "me@domain.example"
class MessageDetailsParticipantFormatterTest : RobolectricTest() {
private val contactNameProvider = object : ContactNameProvider {
override fun getNameForAddress(address: String): String? {
return when (address) {
"user1@domain.example" -> "Contact One"
"spoof@domain.example" -> "contact@important.example"
else -> null
}
}
}
private val account = Account("uuid").apply {
identities += Identity(name = IDENTITY_NAME, email = IDENTITY_ADDRESS)
}
private val participantFormatter = createParticipantFormatter()
@Test
fun `identity address`() {
val displayName = participantFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant"), account)
assertThat(displayName).isEqualTo(IDENTITY_NAME)
}
@Test
fun `identity without a display name`() {
val account = Account("uuid").apply {
identities += Identity(name = null, email = IDENTITY_ADDRESS)
}
val displayName = participantFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "Bob"), account)
assertThat(displayName).isEqualTo("Bob")
}
@Test
fun `identity and address without a display name`() {
val account = Account("uuid").apply {
identities += Identity(name = null, email = IDENTITY_ADDRESS)
}
val displayName = participantFormatter.getDisplayName(Address(IDENTITY_ADDRESS), account)
assertThat(displayName).isNull()
}
@Test
fun `email address without display name`() {
val displayName = participantFormatter.getDisplayName(Address("alice@domain.example"), account)
assertThat(displayName).isNull()
}
@Test
fun `email address with display name`() {
val displayName = participantFormatter.getDisplayName(Address("alice@domain.example", "Alice"), account)
assertThat(displayName).isEqualTo("Alice")
}
@Test
fun `don't look up contact when showContactNames = false`() {
val participantFormatter = createParticipantFormatter(showContactNames = false)
val displayName = participantFormatter.getDisplayName(Address("user1@domain.example", "User 1"), account)
assertThat(displayName).isEqualTo("User 1")
}
@Test
fun `contact lookup`() {
val displayName = participantFormatter.getDisplayName(Address("user1@domain.example"), account)
assertThat(displayName).isEqualTo("Contact One")
}
@Test
fun `contact lookup despite display name`() {
val displayName = participantFormatter.getDisplayName(Address("user1@domain.example", "User 1"), account)
assertThat(displayName).isEqualTo("Contact One")
}
@Test
fun `colored contact name`() {
val participantFormatter = createParticipantFormatter(contactNameColor = Color.RED)
val displayName = participantFormatter.getDisplayName(Address("user1@domain.example"), account)
assertThat(displayName.toString()).isEqualTo("Contact One")
assertThat(displayName).isInstanceOf(Spannable::class.java)
val spans = (displayName as Spannable).getSpans<ForegroundColorSpan>(0, displayName.length)
assertThat(spans.map { it.foregroundColor }).containsExactly(Color.RED)
}
private fun createParticipantFormatter(
showContactNames: Boolean = true,
contactNameColor: Int? = null
): MessageDetailsParticipantFormatter {
return RealMessageDetailsParticipantFormatter(
contactNameProvider = contactNameProvider,
showContactNames = showContactNames,
contactNameColor = contactNameColor
)
}
}