Merge pull request #6618 from thundernest/message-view-redesign

Add the redesigned message view screen
This commit is contained in:
cketti 2023-02-01 18:51:05 +01:00 committed by GitHub
commit e826ed8db7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 3647 additions and 1137 deletions

View file

@ -25,6 +25,7 @@ dependencies {
implementation libs.moshi
implementation libs.timber
implementation libs.mime4j.core
implementation libs.mime4j.dom
testImplementation project(':mail:testing')
testImplementation project(":backend:imap")

View file

@ -16,10 +16,7 @@ public class FontSizes {
private static final String MESSAGE_LIST_DATE = "fontSizeMessageListDate";
private static final String MESSAGE_LIST_PREVIEW = "fontSizeMessageListPreview";
private static final String MESSAGE_VIEW_SENDER = "fontSizeMessageViewSender";
private static final String MESSAGE_VIEW_TO = "fontSizeMessageViewTo";
private static final String MESSAGE_VIEW_CC = "fontSizeMessageViewCC";
private static final String MESSAGE_VIEW_BCC = "fontSizeMessageViewBCC";
private static final String MESSAGE_VIEW_ADDITIONAL_HEADERS = "fontSizeMessageViewAdditionalHeaders";
private static final String MESSAGE_VIEW_RECIPIENTS = "fontSizeMessageViewTo";
private static final String MESSAGE_VIEW_SUBJECT = "fontSizeMessageViewSubject";
private static final String MESSAGE_VIEW_DATE = "fontSizeMessageViewDate";
private static final String MESSAGE_VIEW_CONTENT_PERCENT = "fontSizeMessageViewContentPercent";
@ -40,10 +37,7 @@ public class FontSizes {
private int messageListDate;
private int messageListPreview;
private int messageViewSender;
private int messageViewTo;
private int messageViewCC;
private int messageViewBCC;
private int messageViewAdditionalHeaders;
private int messageViewRecipients;
private int messageViewSubject;
private int messageViewDate;
private int messageViewContentPercent;
@ -57,10 +51,7 @@ public class FontSizes {
messageListPreview = FONT_DEFAULT;
messageViewSender = FONT_DEFAULT;
messageViewTo = FONT_DEFAULT;
messageViewCC = FONT_DEFAULT;
messageViewBCC = FONT_DEFAULT;
messageViewAdditionalHeaders = FONT_DEFAULT;
messageViewRecipients = FONT_DEFAULT;
messageViewSubject = FONT_DEFAULT;
messageViewDate = FONT_DEFAULT;
messageViewContentPercent = 100;
@ -75,10 +66,7 @@ public class FontSizes {
editor.putInt(MESSAGE_LIST_PREVIEW, messageListPreview);
editor.putInt(MESSAGE_VIEW_SENDER, messageViewSender);
editor.putInt(MESSAGE_VIEW_TO, messageViewTo);
editor.putInt(MESSAGE_VIEW_CC, messageViewCC);
editor.putInt(MESSAGE_VIEW_BCC, messageViewBCC);
editor.putInt(MESSAGE_VIEW_ADDITIONAL_HEADERS, messageViewAdditionalHeaders);
editor.putInt(MESSAGE_VIEW_RECIPIENTS, messageViewRecipients);
editor.putInt(MESSAGE_VIEW_SUBJECT, messageViewSubject);
editor.putInt(MESSAGE_VIEW_DATE, messageViewDate);
editor.putInt(MESSAGE_VIEW_CONTENT_PERCENT, getMessageViewContentAsPercent());
@ -93,10 +81,7 @@ public class FontSizes {
messageListPreview = storage.getInt(MESSAGE_LIST_PREVIEW, messageListPreview);
messageViewSender = storage.getInt(MESSAGE_VIEW_SENDER, messageViewSender);
messageViewTo = storage.getInt(MESSAGE_VIEW_TO, messageViewTo);
messageViewCC = storage.getInt(MESSAGE_VIEW_CC, messageViewCC);
messageViewBCC = storage.getInt(MESSAGE_VIEW_BCC, messageViewBCC);
messageViewAdditionalHeaders = storage.getInt(MESSAGE_VIEW_ADDITIONAL_HEADERS, messageViewAdditionalHeaders);
messageViewRecipients = storage.getInt(MESSAGE_VIEW_RECIPIENTS, messageViewRecipients);
messageViewSubject = storage.getInt(MESSAGE_VIEW_SUBJECT, messageViewSubject);
messageViewDate = storage.getInt(MESSAGE_VIEW_DATE, messageViewDate);
@ -149,36 +134,12 @@ public class FontSizes {
this.messageViewSender = messageViewSender;
}
public int getMessageViewTo() {
return messageViewTo;
public int getMessageViewRecipients() {
return messageViewRecipients;
}
public void setMessageViewTo(int messageViewTo) {
this.messageViewTo = messageViewTo;
}
public int getMessageViewCC() {
return messageViewCC;
}
public void setMessageViewCC(int messageViewCC) {
this.messageViewCC = messageViewCC;
}
public int getMessageViewBCC() {
return messageViewBCC;
}
public void setMessageViewBCC(int messageViewBCC) {
this.messageViewBCC = messageViewBCC;
}
public int getMessageViewAdditionalHeaders() {
return messageViewAdditionalHeaders;
}
public void setMessageViewAdditionalHeaders(int messageViewAdditionalHeaders) {
this.messageViewAdditionalHeaders = messageViewAdditionalHeaders;
public void setMessageViewRecipients(int messageViewRecipients) {
this.messageViewRecipients = messageViewRecipients;
}
public int getMessageViewSubject() {

View file

@ -0,0 +1,74 @@
package com.fsck.k9.helper
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.Identity
import com.fsck.k9.mail.Address
/**
* Get the display name for an email address.
*/
interface AddressFormatter {
fun getDisplayName(address: Address): CharSequence
}
class RealAddressFormatter(
private val contactNameProvider: ContactNameProvider,
private val account: Account,
private val showCorrespondentNames: Boolean,
private val showContactNames: Boolean,
private val contactNameColor: Int?,
private val meText: String
) : AddressFormatter {
override fun getDisplayName(address: Address): CharSequence {
val identity = account.findIdentity(address)
if (identity != null) {
return getIdentityName(identity)
}
return if (!showCorrespondentNames) {
address.address
} else if (showContactNames) {
getContactName(address)
} else {
buildDisplayName(address)
}
}
private fun getIdentityName(identity: Identity): String {
return if (account.identities.size == 1) {
meText
} else {
identity.description ?: identity.name ?: identity.email ?: meText
}
}
private fun getContactName(address: Address): CharSequence {
val contactName = contactNameProvider.getNameForAddress(address.address) ?: return buildDisplayName(address)
return if (contactNameColor != null) {
SpannableString(contactName).apply {
setSpan(ForegroundColorSpan(contactNameColor), 0, contactName.length, SPAN_EXCLUSIVE_EXCLUSIVE)
}
} else {
contactName
}
}
private fun buildDisplayName(address: Address): CharSequence {
return address.personal?.takeIf {
it.isNotBlank() && !it.equals(meText, ignoreCase = true) && !isSpoofAddress(it)
} ?: address.address
}
private fun isSpoofAddress(displayName: String): Boolean {
val atIndex = displayName.indexOf('@')
return if (atIndex > 0) {
displayName[atIndex - 1] != '('
} else {
false
}
}
}

View file

@ -0,0 +1,11 @@
package com.fsck.k9.helper
interface ContactNameProvider {
fun getNameForAddress(address: String): String?
}
class RealContactNameProvider(private val contacts: Contacts) : ContactNameProvider {
override fun getNameForAddress(address: String): String? {
return contacts.getNameForAddress(address)
}
}

View file

@ -1,6 +1,8 @@
package com.fsck.k9.helper;
import java.util.HashMap;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Context;
@ -9,13 +11,12 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import timber.log.Timber;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.fsck.k9.mail.Address;
import java.util.HashMap;
import timber.log.Timber;
/**
* Helper class to access the contacts stored on the device.
@ -37,7 +38,8 @@ public class Contacts {
ContactsContract.CommonDataKinds.Email._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Email.CONTACT_ID,
Photo.PHOTO_URI
Photo.PHOTO_URI,
ContactsContract.Contacts.LOOKUP_KEY
};
/**
@ -52,6 +54,8 @@ public class Contacts {
*/
protected static final int CONTACT_ID_INDEX = 2;
protected static final int LOOKUP_KEY_INDEX = 4;
/**
* Get instance of the Contacts class.
@ -169,6 +173,24 @@ public class Contacts {
return false;
}
@Nullable
public Uri getContactUri(String emailAddress) {
Cursor cursor = getContactByAddress(emailAddress);
if (cursor == null) {
return null;
}
try (cursor) {
if (!cursor.moveToFirst()) {
return null;
}
long contactId = cursor.getLong(CONTACT_ID_INDEX);
String lookupKey = cursor.getString(LOOKUP_KEY_INDEX);
return ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
}
}
/**
* Get the name of the contact an email address belongs to.
*

View file

@ -0,0 +1,22 @@
package com.fsck.k9.mailstore
import com.fsck.k9.mail.Address
import java.util.Date
data class MessageDetails(
val date: MessageDate,
val from: List<Address>,
val sender: Address?,
val replyTo: List<Address>,
val to: List<Address>,
val cc: List<Address>,
val bcc: List<Address>
)
sealed interface MessageDate {
data class ValidDate(val date: Date) : MessageDate
data class InvalidDate(val dateHeader: String) : MessageDate
object MissingDate : MessageDate
}

View file

@ -1,11 +1,69 @@
package com.fsck.k9.mailstore
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Header
import com.fsck.k9.mail.internet.MimeUtility
import org.apache.james.mime4j.dom.field.DateTimeField
import org.apache.james.mime4j.field.DefaultFieldParser
class MessageRepository(private val messageStoreManager: MessageStoreManager) {
fun getHeaders(messageReference: MessageReference): List<Header> {
val messageStore = messageStoreManager.getMessageStore(messageReference.accountUuid)
return messageStore.getHeaders(messageReference.folderId, messageReference.uid)
}
fun getMessageDetails(messageReference: MessageReference): MessageDetails {
val messageStore = messageStoreManager.getMessageStore(messageReference.accountUuid)
val headers = messageStore.getHeaders(messageReference.folderId, messageReference.uid, MESSAGE_DETAILS_HEADERS)
val messageDate = headers.parseDate("date")
val fromAddresses = headers.parseAddresses("from")
val senderAddresses = headers.parseAddresses("sender")
val replyToAddresses = headers.parseAddresses("reply-to")
val toAddresses = headers.parseAddresses("to")
val ccAddresses = headers.parseAddresses("cc")
val bccAddresses = headers.parseAddresses("bcc")
return MessageDetails(
date = messageDate,
from = fromAddresses,
sender = senderAddresses.firstOrNull(),
replyTo = replyToAddresses,
to = toAddresses,
cc = ccAddresses,
bcc = bccAddresses
)
}
private fun List<Header>.firstHeaderOrNull(name: String): String? {
return firstOrNull { it.name.equals(name, ignoreCase = true) }?.value
}
private fun List<Header>.parseAddresses(headerName: String): List<Address> {
return Address.parse(MimeUtility.unfold(firstHeaderOrNull(headerName))).toList()
}
private fun List<Header>.parseDate(headerName: String): MessageDate {
val dateHeader = firstHeaderOrNull(headerName) ?: return MessageDate.MissingDate
return try {
val dateTimeField = DefaultFieldParser.parse("Date: $dateHeader") as DateTimeField
return MessageDate.ValidDate(date = dateTimeField.date)
} catch (e: Exception) {
MessageDate.InvalidDate(dateHeader)
}
}
companion object {
private val MESSAGE_DETAILS_HEADERS = setOf(
"date",
"from",
"sender",
"reply-to",
"to",
"cc",
"bcc",
)
}
}

View file

@ -156,6 +156,11 @@ interface MessageStore {
*/
fun getHeaders(folderId: Long, messageServerId: String): List<Header>
/**
* Retrieve selected header fields of a message.
*/
fun getHeaders(folderId: Long, messageServerId: String, headerNames: Set<String>): List<Header>
/**
* Return the size of this message store in bytes.
*/

View file

@ -0,0 +1,36 @@
package com.fsck.k9.message
import com.fsck.k9.Account
import com.fsck.k9.helper.ReplyToParser
import com.fsck.k9.mail.Message
/**
* Figures out which reply actions are available to the user.
*/
class ReplyActionStrategy(private val replyRoParser: ReplyToParser) {
fun getReplyActions(account: Account, message: Message): ReplyActions {
val recipientsToReplyTo = replyRoParser.getRecipientsToReplyTo(message, account)
val recipientsToReplyAllTo = replyRoParser.getRecipientsToReplyAllTo(message, account)
val replyToAllCount = recipientsToReplyAllTo.to.size + recipientsToReplyAllTo.cc.size
return if (replyToAllCount <= 1) {
if (recipientsToReplyTo.to.isEmpty()) {
ReplyActions(defaultAction = null)
} else {
ReplyActions(defaultAction = ReplyAction.REPLY)
}
} else {
ReplyActions(defaultAction = ReplyAction.REPLY_ALL, additionalActions = listOf(ReplyAction.REPLY))
}
}
}
data class ReplyActions(
val defaultAction: ReplyAction?,
val additionalActions: List<ReplyAction> = emptyList()
)
enum class ReplyAction {
REPLY,
REPLY_ALL
}

View file

@ -0,0 +1,206 @@
package com.fsck.k9.helper
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.mail.Address
import com.google.common.truth.Truth.assertThat
import org.junit.Test
private const val IDENTITY_ADDRESS = "me@domain.example"
private const val ME_TEXT = "me"
class RealAddressFormatterTest : 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(email = IDENTITY_ADDRESS)
}
@Test
fun `single identity`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant"))
assertThat(displayName).isEqualTo(ME_TEXT)
}
@Test
fun `multiple identities`() {
val account = Account("uuid").apply {
identities += Identity(description = "My identity", email = IDENTITY_ADDRESS)
identities += Identity(email = "another.one@domain.example")
}
val addressFormatter = createAddressFormatter(account)
val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant"))
assertThat(displayName).isEqualTo("My identity")
}
@Test
fun `identity without a description`() {
val account = Account("uuid").apply {
identities += Identity(name = "My name", email = IDENTITY_ADDRESS)
identities += Identity(email = "another.one@domain.example")
}
val addressFormatter = createAddressFormatter(account)
val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant"))
assertThat(displayName).isEqualTo("My name")
}
@Test
fun `identity without a description and name`() {
val account = Account("uuid").apply {
identities += Identity(email = IDENTITY_ADDRESS)
identities += Identity(email = "another.one@domain.example")
}
val addressFormatter = createAddressFormatter(account)
val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant"))
assertThat(displayName).isEqualTo(IDENTITY_ADDRESS)
}
@Test
fun `email address without display name`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("alice@domain.example"))
assertThat(displayName).isEqualTo("alice@domain.example")
}
@Test
fun `email address with display name`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("alice@domain.example", "Alice"))
assertThat(displayName).isEqualTo("Alice")
}
@Test
fun `don't look up contact when showContactNames = false`() {
val addressFormatter = createAddressFormatter(showContactNames = false)
val displayName = addressFormatter.getDisplayName(Address("user1@domain.example", "User 1"))
assertThat(displayName).isEqualTo("User 1")
}
@Test
fun `contact lookup`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("user1@domain.example"))
assertThat(displayName).isEqualTo("Contact One")
}
@Test
fun `contact lookup despite display name`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("user1@domain.example", "User 1"))
assertThat(displayName).isEqualTo("Contact One")
}
@Test
fun `colored contact name`() {
val addressFormatter = createAddressFormatter(contactNameColor = Color.RED)
val displayName = addressFormatter.getDisplayName(Address("user1@domain.example"))
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)
}
@Test
fun `email address with display name but not showing correspondent names`() {
val addressFormatter = createAddressFormatter(showCorrespondentNames = false)
val displayName = addressFormatter.getDisplayName(Address("alice@domain.example", "Alice"))
assertThat(displayName).isEqualTo("alice@domain.example")
}
@Test
fun `do not show display name that looks like an email address`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("mallory@domain.example", "potus@whitehouse.gov"))
assertThat(displayName).isEqualTo("mallory@domain.example")
}
@Test
fun `do show display name that contains an @ preceded by an opening parenthesis`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("gitlab@gitlab.example", "username (@username)"))
assertThat(displayName).isEqualTo("username (@username)")
}
@Test
fun `do show display name that starts with an @`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("address@domain.example", "@username"))
assertThat(displayName).isEqualTo("@username")
}
@Test
fun `spoof prevention doesn't apply to contact names`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("spoof@domain.example", "contact@important.example"))
assertThat(displayName).isEqualTo("contact@important.example")
}
@Test
fun `display name matches me text`() {
val addressFormatter = createAddressFormatter()
val displayName = addressFormatter.getDisplayName(Address("someone_named_me@domain.example", "ME"))
assertThat(displayName).isEqualTo("someone_named_me@domain.example")
}
private fun createAddressFormatter(
account: Account = this.account,
showCorrespondentNames: Boolean = true,
showContactNames: Boolean = true,
contactNameColor: Int? = null
): RealAddressFormatter {
return RealAddressFormatter(
contactNameProvider = contactNameProvider,
account = account,
showCorrespondentNames = showCorrespondentNames,
showContactNames = showContactNames,
contactNameColor = contactNameColor,
meText = ME_TEXT
)
}
}

View file

@ -0,0 +1,111 @@
package com.fsck.k9.message
import com.fsck.k9.Account
import com.fsck.k9.Identity
import com.fsck.k9.helper.ReplyToParser
import com.fsck.k9.mail.buildMessage
import com.google.common.truth.Truth.assertThat
import org.junit.Test
private const val IDENTITY_EMAIL_ADDRESS = "myself@domain.example"
class ReplyActionStrategyTest {
private val account = createAccount()
private val replyActionStrategy = ReplyActionStrategy(ReplyToParser())
@Test
fun `message sent to only our identity`() {
val message = buildMessage {
header("From", "sender@domain.example")
header("To", IDENTITY_EMAIL_ADDRESS)
}
val replyActions = replyActionStrategy.getReplyActions(account, message)
assertThat(replyActions.defaultAction).isEqualTo(ReplyAction.REPLY)
assertThat(replyActions.additionalActions).isEmpty()
}
@Test
fun `message sent to our identity and others`() {
val message = buildMessage {
header("From", "sender@domain.example")
header("To", "$IDENTITY_EMAIL_ADDRESS, other@domain.example")
}
val replyActions = replyActionStrategy.getReplyActions(account, message)
assertThat(replyActions.defaultAction).isEqualTo(ReplyAction.REPLY_ALL)
assertThat(replyActions.additionalActions).containsExactly(ReplyAction.REPLY)
}
@Test
fun `message sent to our identity and others (CC)`() {
val message = buildMessage {
header("From", "sender@domain.example")
header("Cc", "$IDENTITY_EMAIL_ADDRESS, other@domain.example")
}
val replyActions = replyActionStrategy.getReplyActions(account, message)
assertThat(replyActions.defaultAction).isEqualTo(ReplyAction.REPLY_ALL)
assertThat(replyActions.additionalActions).containsExactly(ReplyAction.REPLY)
}
@Test
fun `message sent to our identity and others (To+CC)`() {
val message = buildMessage {
header("From", "sender@domain.example")
header("To", IDENTITY_EMAIL_ADDRESS)
header("Cc", "other@domain.example")
}
val replyActions = replyActionStrategy.getReplyActions(account, message)
assertThat(replyActions.defaultAction).isEqualTo(ReplyAction.REPLY_ALL)
assertThat(replyActions.additionalActions).containsExactly(ReplyAction.REPLY)
}
@Test
fun `message sent to our identity and others (CC+To)`() {
val message = buildMessage {
header("From", "sender@domain.example")
header("To", "other@domain.example")
header("Cc", IDENTITY_EMAIL_ADDRESS)
}
val replyActions = replyActionStrategy.getReplyActions(account, message)
assertThat(replyActions.defaultAction).isEqualTo(ReplyAction.REPLY_ALL)
assertThat(replyActions.additionalActions).containsExactly(ReplyAction.REPLY)
}
@Test
fun `message where neither sender nor recipient addresses belong to account`() {
val message = buildMessage {
header("From", "sender@domain.example")
header("To", "recipient@domain.example")
}
val replyActions = replyActionStrategy.getReplyActions(account, message)
assertThat(replyActions.defaultAction).isEqualTo(ReplyAction.REPLY_ALL)
assertThat(replyActions.additionalActions).containsExactly(ReplyAction.REPLY)
}
@Test
fun `message without any sender or recipient headers`() {
val message = buildMessage {}
val replyActions = replyActionStrategy.getReplyActions(account, message)
assertThat(replyActions.defaultAction).isNull()
assertThat(replyActions.additionalActions).isEmpty()
}
private fun createAccount(): Account {
return Account("00000000-0000-4000-0000-000000000000").apply {
identities += Identity(name = "Myself", email = IDENTITY_EMAIL_ADDRESS)
}
}
}

View file

@ -29,6 +29,10 @@
-dontnote com.fsck.k9.ui.messageview.**
-dontnote com.fsck.k9.view.**
-assumevalues class * extends android.view.View {
boolean isInEditMode() return false;
}
-keep public class org.openintents.openpgp.**
-keepclassmembers class * extends androidx.appcompat.widget.SearchView {

View file

@ -133,6 +133,10 @@ class K9MessageStore(
return retrieveMessageOperations.getHeaders(folderId, messageServerId)
}
override fun getHeaders(folderId: Long, messageServerId: String, headerNames: Set<String>): List<Header> {
return retrieveMessageOperations.getHeaders(folderId, messageServerId, headerNames)
}
override fun destroyMessages(folderId: Long, messageServerIds: Collection<String>) {
deleteMessageOperations.destroyMessages(folderId, messageServerIds)
}

View file

@ -2,6 +2,7 @@ package com.fsck.k9.storage.messages
import androidx.core.database.getLongOrNull
import com.fsck.k9.K9
import com.fsck.k9.helper.mapToSet
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Header
import com.fsck.k9.mail.internet.MimeHeader
@ -167,7 +168,7 @@ internal class RetrieveMessageOperations(private val lockableDatabase: LockableD
}
}
fun getHeaders(folderId: Long, messageServerId: String): List<Header> {
fun getHeaders(folderId: Long, messageServerId: String, headerNames: Set<String>? = null): List<Header> {
return lockableDatabase.execute(false) { database ->
database.rawQuery(
"SELECT message_parts.header FROM messages" +
@ -178,10 +179,13 @@ internal class RetrieveMessageOperations(private val lockableDatabase: LockableD
if (!cursor.moveToFirst()) throw MessageNotFoundException(folderId, messageServerId)
val headerBytes = cursor.getBlob(0)
val lowercaseHeaderNames = headerNames?.mapToSet(headerNames.size) { it.lowercase() }
val header = MimeHeader()
MessageHeaderParser.parse(headerBytes.inputStream()) { name, value ->
header.addRawHeader(name, value)
if (lowercaseHeaderNames == null || name.lowercase() in lowercaseHeaderNames) {
header.addRawHeader(name, value)
}
}
header.headers

View file

@ -170,6 +170,34 @@ class RetrieveMessageOperationsTest : RobolectricTest() {
)
}
@Test
fun `get some headers`() {
val messagePartId = sqliteDatabase.createMessagePart(
header = """
From: <alice@domain.example>
To: Bob <bob@domain.example>
Date: Thu, 01 Apr 2021 01:23:45 +0200
Subject: Test
Message-Id: <20210401012345.123456789A@domain.example>
""".trimIndent().crlf()
)
sqliteDatabase.createMessage(folderId = 1, uid = "uid1", messagePartId = messagePartId)
val headers = retrieveMessageOperations.getHeaders(
folderId = 1,
messageServerId = "uid1",
headerNames = setOf("from", "to", "message-id")
)
assertThat(headers).isEqualTo(
listOf(
Header("From", "<alice@domain.example>"),
Header("To", "Bob <bob@domain.example>"),
Header("Message-Id", "<20210401012345.123456789A@domain.example>")
)
)
}
@Test
fun `get oldest message date`() {
sqliteDatabase.createMessage(folderId = 1, date = 42)

View file

@ -11,6 +11,7 @@ dependencies {
implementation project(":app:autodiscovery:api")
implementation project(":app:autodiscovery:providersxml")
implementation project(":mail:common")
implementation project(":ui-utils:ToolbarBottomSheet")
//TODO: Remove AccountSetupIncoming's dependency on these
compileOnly project(":mail:protocols:imap")

View file

@ -11,6 +11,7 @@ import com.fsck.k9.ui.choosefolder.chooseFolderUiModule
import com.fsck.k9.ui.endtoend.endToEndUiModule
import com.fsck.k9.ui.folders.foldersUiModule
import com.fsck.k9.ui.managefolders.manageFoldersUiModule
import com.fsck.k9.ui.messagedetails.messageDetailsUiModule
import com.fsck.k9.ui.messagelist.messageListUiModule
import com.fsck.k9.ui.messagesource.messageSourceModule
import com.fsck.k9.ui.settings.settingsUiModule
@ -33,5 +34,6 @@ val uiModules = listOf(
viewModule,
changelogUiModule,
messageSourceModule,
accountUiModule
accountUiModule,
messageDetailsUiModule
)

View file

@ -129,6 +129,9 @@ open class MessageList :
private var messageListWasDisplayed = false
private var viewSwitcher: ViewSwitcher? = null
private val isShowAccountChip: Boolean
get() = messageListFragment?.isShowAccountChip ?: true
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -956,7 +959,7 @@ open class MessageList :
displayMode = DisplayMode.MESSAGE_LIST
MessageActions.actionEditDraft(this, messageReference)
} else {
val fragment = MessageViewContainerFragment.newInstance(messageReference)
val fragment = MessageViewContainerFragment.newInstance(messageReference, isShowAccountChip)
supportFragmentManager.commitNow {
replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER)
}

View file

@ -27,10 +27,7 @@ data class MessageListActivityConfig(
val fontSizeMessageListDate: Int,
val fontSizeMessageListPreview: Int,
val fontSizeMessageViewSender: Int,
val fontSizeMessageViewTo: Int,
val fontSizeMessageViewCC: Int,
val fontSizeMessageViewBCC: Int,
val fontSizeMessageViewAdditionalHeaders: Int,
val fontSizeMessageViewRecipients: Int,
val fontSizeMessageViewSubject: Int,
val fontSizeMessageViewDate: Int,
val fontSizeMessageViewContentAsPercent: Int,
@ -62,10 +59,7 @@ data class MessageListActivityConfig(
fontSizeMessageListDate = K9.fontSizes.messageListDate,
fontSizeMessageListPreview = K9.fontSizes.messageListPreview,
fontSizeMessageViewSender = K9.fontSizes.messageViewSender,
fontSizeMessageViewTo = K9.fontSizes.messageViewTo,
fontSizeMessageViewCC = K9.fontSizes.messageViewCC,
fontSizeMessageViewBCC = K9.fontSizes.messageViewBCC,
fontSizeMessageViewAdditionalHeaders = K9.fontSizes.messageViewAdditionalHeaders,
fontSizeMessageViewRecipients = K9.fontSizes.messageViewRecipients,
fontSizeMessageViewSubject = K9.fontSizes.messageViewSubject,
fontSizeMessageViewDate = K9.fontSizes.messageViewDate,
fontSizeMessageViewContentAsPercent = K9.fontSizes.messageViewContentAsPercent,

View file

@ -0,0 +1,21 @@
package com.fsck.k9.ui.helper
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
/**
* Return the baseline of the last line of text, instead of TextView's default of the first line.
*/
// Source: https://stackoverflow.com/a/62419876
class BottomBaselineTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : AppCompatTextView(context, attrs) {
override fun getBaseline(): Int {
val layout = layout ?: return super.getBaseline()
val baselineOffset = super.getBaseline() - layout.getLineBaseline(0)
return baselineOffset + layout.getLineBaseline(layout.lineCount - 1)
}
}

View file

@ -0,0 +1,23 @@
package com.fsck.k9.ui.messagedetails
import android.content.Context
import android.content.Intent
import android.provider.ContactsContract
internal class AddToContactsLauncher {
fun launch(context: Context, name: String?, email: String) {
val intent = Intent(Intent.ACTION_INSERT).apply {
type = ContactsContract.Contacts.CONTENT_TYPE
putExtra(ContactsContract.Intents.Insert.EMAIL, email)
if (name != null) {
putExtra(ContactsContract.Intents.Insert.NAME, name)
}
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
}
context.startActivity(intent)
}
}

View file

@ -0,0 +1,8 @@
package com.fsck.k9.ui.messagedetails
import com.fsck.k9.K9
class ContactSettingsProvider {
val isShowContactPicture: Boolean
get() = K9.isShowContactPicture
}

View file

@ -0,0 +1,52 @@
package com.fsck.k9.ui.messagedetails
import android.content.res.ColorStateList
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.fsck.k9.ui.R
import com.fsck.k9.ui.resolveColorAttribute
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
internal class CryptoStatusItem(val cryptoDetails: CryptoDetails) : AbstractItem<CryptoStatusItem.ViewHolder>() {
override val type = R.id.message_details_crypto_status
override val layoutRes = R.layout.message_details_crypto_status_item
override fun getViewHolder(v: View) = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<CryptoStatusItem>(view) {
private val titleTextView = view.findViewById<TextView>(R.id.crypto_status_title)
private val descriptionTextView = view.findViewById<TextView>(R.id.crypto_status_description)
private val imageView = view.findViewById<ImageView>(R.id.crypto_status_icon)
private val originalBackground = view.background
override fun bindView(item: CryptoStatusItem, payloads: List<Any>) {
val context = itemView.context
val cryptoDetails = item.cryptoDetails
val cryptoStatus = cryptoDetails.cryptoStatus
imageView.setImageResource(cryptoStatus.statusIconRes)
val tintColor = context.theme.resolveColorAttribute(cryptoStatus.colorAttr)
imageView.imageTintList = ColorStateList.valueOf(tintColor)
cryptoStatus.titleTextRes?.let { stringResId ->
titleTextView.text = context.getString(stringResId)
}
cryptoStatus.descriptionTextRes?.let { stringResId ->
descriptionTextView.text = context.getString(stringResId)
}
if (!cryptoDetails.isClickable) {
itemView.background = null
}
}
override fun unbindView(item: CryptoStatusItem) {
imageView.setImageDrawable(null)
titleTextView.text = null
descriptionTextView.text = null
itemView.background = originalBackground
}
}
}

View file

@ -0,0 +1,19 @@
package com.fsck.k9.ui.messagedetails
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val messageDetailsUiModule = module {
viewModel {
MessageDetailsViewModel(
resources = get(),
messageRepository = get(),
contactSettingsProvider = get(),
contacts = get(),
clipboardManager = get()
)
}
factory { ContactSettingsProvider() }
factory { AddToContactsLauncher() }
factory { ShowContactLauncher() }
}

View file

@ -0,0 +1,26 @@
package com.fsck.k9.ui.messagedetails
import android.view.View
import android.widget.TextView
import com.fsck.k9.ui.R
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
internal class MessageDateItem(private val date: String) : AbstractItem<MessageDateItem.ViewHolder>() {
override val type: Int = R.id.message_details_date
override val layoutRes = R.layout.message_details_date_item
override fun getViewHolder(v: View) = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<MessageDateItem>(view) {
private val textView = view.findViewById<TextView>(R.id.date)
override fun bindView(item: MessageDateItem, payloads: List<Any>) {
textView.text = item.date
}
override fun unbindView(item: MessageDateItem) {
textView.text = null
}
}
}

View file

@ -0,0 +1,19 @@
package com.fsck.k9.ui.messagedetails
import android.view.View
import com.fsck.k9.ui.R
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
internal class MessageDetailsDividerItem : AbstractItem<MessageDetailsDividerItem.ViewHolder>() {
override val type: Int = R.id.message_details_divider
override val layoutRes = R.layout.message_details_divider_item
override fun getViewHolder(v: View) = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<MessageDetailsDividerItem>(view) {
override fun bindView(item: MessageDetailsDividerItem, payloads: List<Any>) = Unit
override fun unbindView(item: MessageDetailsDividerItem) = Unit
}
}

View file

@ -0,0 +1,313 @@
package com.fsck.k9.ui.messagedetails
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import androidx.annotation.StringRes
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResult
import androidx.recyclerview.widget.RecyclerView
import app.k9mail.ui.utils.bottomsheet.ToolbarBottomSheetDialogFragment
import com.fsck.k9.activity.MessageCompose
import com.fsck.k9.contacts.ContactPictureLoader
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.mail.Address
import com.fsck.k9.mailstore.CryptoResultAnnotation
import com.fsck.k9.ui.R
import com.fsck.k9.ui.observe
import com.fsck.k9.ui.withArguments
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.GenericItem
import com.mikepenz.fastadapter.adapters.ItemAdapter
import com.mikepenz.fastadapter.listeners.ClickEventHook
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
private val viewModel: MessageDetailsViewModel by viewModel()
private val addToContactsLauncher: AddToContactsLauncher by inject()
private val showContactLauncher: ShowContactLauncher by inject()
private val contactPictureLoader: ContactPictureLoader by inject()
private lateinit var messageReference: MessageReference
// FIXME: Replace this with a mechanism that survives process death
var cryptoResult: CryptoResultAnnotation? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE))
?: error("Missing argument $ARG_REFERENCE")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.message_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
cryptoResult?.let {
viewModel.cryptoResult = it
}
val dialog = checkNotNull(dialog)
dialog.isDismissWithAnimation = true
val toolbar = checkNotNull(toolbar)
toolbar.apply {
title = getString(R.string.message_details_toolbar_title)
navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_close)
setNavigationOnClickListener {
dismiss()
}
}
val progressBar = view.findViewById<ProgressBar>(R.id.message_details_progress)
val errorView = view.findViewById<View>(R.id.message_details_error)
val recyclerView = view.findViewById<RecyclerView>(R.id.message_details_list)
viewModel.uiEvents.observe(this) { event ->
when (event) {
is MessageDetailEvent.ShowCryptoKeys -> showCryptoKeys(event.pendingIntent)
MessageDetailEvent.SearchCryptoKeys -> searchCryptoKeys()
MessageDetailEvent.ShowCryptoWarning -> showCryptoWarning()
}
}
viewModel.loadData(messageReference).observe(this) { state ->
when (state) {
MessageDetailsState.Loading -> {
progressBar.isVisible = true
errorView.isVisible = false
recyclerView.isVisible = false
}
MessageDetailsState.Error -> {
progressBar.isVisible = false
errorView.isVisible = true
recyclerView.isVisible = false
}
is MessageDetailsState.DataLoaded -> {
progressBar.isVisible = false
errorView.isVisible = false
recyclerView.isVisible = true
setMessageDetails(recyclerView, state.details, state.showContactPicture)
}
}
}
}
private fun setMessageDetails(recyclerView: RecyclerView, details: MessageDetailsUi, showContactPicture: Boolean) {
val itemAdapter = ItemAdapter<GenericItem>().apply {
add(MessageDateItem(details.date ?: getString(R.string.message_details_missing_date)))
if (details.cryptoDetails != null) {
add(CryptoStatusItem(details.cryptoDetails))
}
addParticipants(details.from, R.string.message_details_from_section_title, showContactPicture)
addParticipants(details.sender, R.string.message_details_sender_section_title, showContactPicture)
addParticipants(details.replyTo, R.string.message_details_replyto_section_title, showContactPicture)
add(MessageDetailsDividerItem())
addParticipants(details.to, R.string.message_details_to_section_title, showContactPicture)
addParticipants(details.cc, R.string.message_details_cc_section_title, showContactPicture)
addParticipants(details.bcc, R.string.message_details_bcc_section_title, showContactPicture)
}
val adapter = FastAdapter.with(itemAdapter).apply {
addEventHook(cryptoStatusClickEventHook)
addEventHook(participantClickEventHook)
addEventHook(addToContactsClickEventHook)
addEventHook(composeClickEventHook)
addEventHook(overflowClickEventHook)
}
recyclerView.adapter = adapter
}
private fun ItemAdapter<GenericItem>.addParticipants(
participants: List<Participant>,
@StringRes title: Int,
showContactPicture: Boolean
) {
if (participants.isNotEmpty()) {
val extraText = if (participants.size > 1) participants.size.toString() else null
add(SectionHeaderItem(title = getString(title), extra = extraText))
for (participant in participants) {
add(ParticipantItem(contactPictureLoader, showContactPicture, participant))
}
}
}
private val cryptoStatusClickEventHook = object : ClickEventHook<CryptoStatusItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
return if (viewHolder is CryptoStatusItem.ViewHolder) {
viewHolder.itemView
} else {
null
}
}
override fun onClick(
v: View,
position: Int,
fastAdapter: FastAdapter<CryptoStatusItem>,
item: CryptoStatusItem
) {
if (item.cryptoDetails.isClickable) {
viewModel.onCryptoStatusClicked()
}
}
}
private val participantClickEventHook = object : ClickEventHook<ParticipantItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
return if (viewHolder is ParticipantItem.ViewHolder) {
viewHolder.itemView
} else {
null
}
}
override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
val contactLookupUri = item.participant.contactLookupUri ?: return
showContact(contactLookupUri)
}
}
private fun showContact(contactLookupUri: Uri) {
showContactLauncher.launch(requireContext(), contactLookupUri)
}
private val addToContactsClickEventHook = object : ClickEventHook<ParticipantItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
return if (viewHolder is ParticipantItem.ViewHolder) {
viewHolder.menuAddContact
} else {
null
}
}
override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
val address = item.participant.address
addToContacts(address)
}
}
private fun addToContacts(address: Address) {
addToContactsLauncher.launch(context = requireContext(), name = address.personal, email = address.address)
}
private val composeClickEventHook = object : ClickEventHook<ParticipantItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
return if (viewHolder is ParticipantItem.ViewHolder) {
viewHolder.menuCompose
} else {
null
}
}
override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
val address = item.participant.address
composeMessageToAddress(address)
}
}
private fun composeMessageToAddress(address: Address) {
// TODO: Use the identity this message was sent to as sender identity
val intent = Intent(context, MessageCompose::class.java).apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_EMAIL, arrayOf(address.toString()))
putExtra(MessageCompose.EXTRA_ACCOUNT, messageReference.accountUuid)
}
dismiss()
requireContext().startActivity(intent)
}
private val overflowClickEventHook = object : ClickEventHook<ParticipantItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
return if (viewHolder is ParticipantItem.ViewHolder) {
viewHolder.menuOverflow
} else {
null
}
}
override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
showOverflowMenu(v, item.participant)
}
}
private fun showOverflowMenu(view: View, participant: Participant) {
val popupMenu = PopupMenu(requireContext(), view).apply {
inflate(R.menu.participant_overflow_menu)
}
if (participant.address.personal == null) {
popupMenu.menu.findItem(R.id.copy_name_and_email_address).isVisible = false
}
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
onOverflowMenuItemClick(item.itemId, participant)
true
}
popupMenu.show()
}
private fun onOverflowMenuItemClick(itemId: Int, participant: Participant) {
when (itemId) {
R.id.copy_email_address -> viewModel.onCopyEmailAddressToClipboard(participant)
R.id.copy_name_and_email_address -> viewModel.onCopyNameAndEmailAddressToClipboard(participant)
}
}
private fun showCryptoKeys(pendingIntent: PendingIntent) {
requireActivity().startIntentSender(pendingIntent.intentSender, null, 0, 0, 0)
}
private fun searchCryptoKeys() {
setFragmentResult(FRAGMENT_RESULT_KEY, bundleOf(RESULT_ACTION to ACTION_SEARCH_KEYS))
dismiss()
}
private fun showCryptoWarning() {
setFragmentResult(FRAGMENT_RESULT_KEY, bundleOf(RESULT_ACTION to ACTION_SHOW_WARNING))
dismiss()
}
companion object {
private const val ARG_REFERENCE = "reference"
const val FRAGMENT_RESULT_KEY = "messageDetailsResult"
const val RESULT_ACTION = "action"
const val ACTION_SEARCH_KEYS = "search_keys"
const val ACTION_SHOW_WARNING = "show_warning"
fun create(messageReference: MessageReference): MessageDetailsFragment {
return MessageDetailsFragment().withArguments(
ARG_REFERENCE to messageReference.toIdentityString()
)
}
}
}

View file

@ -0,0 +1,29 @@
package com.fsck.k9.ui.messagedetails
import android.net.Uri
import com.fsck.k9.mail.Address
import com.fsck.k9.view.MessageCryptoDisplayStatus
data class MessageDetailsUi(
val date: String?,
val cryptoDetails: CryptoDetails?,
val from: List<Participant>,
val sender: List<Participant>,
val replyTo: List<Participant>,
val to: List<Participant>,
val cc: List<Participant>,
val bcc: List<Participant>
)
data class CryptoDetails(
val cryptoStatus: MessageCryptoDisplayStatus,
val isClickable: Boolean
)
data class Participant(
val address: Address,
val contactLookupUri: Uri?
) {
val isInContacts: Boolean
get() = contactLookupUri != null
}

View file

@ -0,0 +1,142 @@
package com.fsck.k9.ui.messagedetails
import android.app.PendingIntent
import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.helper.ClipboardManager
import com.fsck.k9.helper.Contacts
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.ui.R
import com.fsck.k9.view.MessageCryptoDisplayStatus
import java.text.DateFormat
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
internal class MessageDetailsViewModel(
private val resources: Resources,
private val messageRepository: MessageRepository,
private val contactSettingsProvider: ContactSettingsProvider,
private val contacts: Contacts,
private val clipboardManager: ClipboardManager
) : ViewModel() {
private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.getDefault())
private val uiState = MutableStateFlow<MessageDetailsState>(MessageDetailsState.Loading)
private val eventChannel = Channel<MessageDetailEvent>()
val uiEvents = eventChannel.receiveAsFlow()
var cryptoResult: CryptoResultAnnotation? = null
fun loadData(messageReference: MessageReference): StateFlow<MessageDetailsState> {
viewModelScope.launch(Dispatchers.IO) {
uiState.value = try {
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()
)
MessageDetailsState.DataLoaded(
showContactPicture = contactSettingsProvider.isShowContactPicture,
details = messageDetailsUi
)
} catch (e: Exception) {
MessageDetailsState.Error
}
}
return uiState
}
private fun buildDisplayDate(messageDate: MessageDate): String? {
return when (messageDate) {
is MessageDate.InvalidDate -> messageDate.dateHeader
MessageDate.MissingDate -> null
is MessageDate.ValidDate -> dateFormat.format(messageDate.date)
}
}
private fun CryptoResultAnnotation.toCryptoDetails(): CryptoDetails {
val messageCryptoDisplayStatus = MessageCryptoDisplayStatus.fromResultAnnotation(this)
return CryptoDetails(
cryptoStatus = messageCryptoDisplayStatus,
isClickable = messageCryptoDisplayStatus.hasAssociatedKey() || messageCryptoDisplayStatus.isUnknownKey ||
hasOpenPgpInsecureWarningPendingIntent()
)
}
private fun List<Address>.toParticipants(): List<Participant> {
return this.map { address ->
Participant(
address = address,
contactLookupUri = contacts.getContactUri(address.address)
)
}
}
fun onCryptoStatusClicked() {
val cryptoResult = cryptoResult ?: return
val cryptoStatus = MessageCryptoDisplayStatus.fromResultAnnotation(cryptoResult)
if (cryptoStatus.hasAssociatedKey()) {
val pendingIntent = cryptoResult.openPgpSigningKeyIntentIfAny
if (pendingIntent != null) {
viewModelScope.launch {
eventChannel.send(MessageDetailEvent.ShowCryptoKeys(pendingIntent))
}
}
} else if (cryptoStatus.isUnknownKey) {
viewModelScope.launch {
eventChannel.send(MessageDetailEvent.SearchCryptoKeys)
}
} else if (cryptoResult.hasOpenPgpInsecureWarningPendingIntent()) {
viewModelScope.launch {
eventChannel.send(MessageDetailEvent.ShowCryptoWarning)
}
}
}
fun onCopyEmailAddressToClipboard(participant: Participant) {
val label = resources.getString(R.string.clipboard_label_email_address)
val emailAddress = participant.address.address
clipboardManager.setText(label, emailAddress)
}
fun onCopyNameAndEmailAddressToClipboard(participant: Participant) {
val label = resources.getString(R.string.clipboard_label_name_and_email_address)
val nameAndEmailAddress = participant.address.toString()
clipboardManager.setText(label, nameAndEmailAddress)
}
}
sealed interface MessageDetailsState {
object Loading : MessageDetailsState
object Error : MessageDetailsState
data class DataLoaded(
val showContactPicture: Boolean,
val details: MessageDetailsUi
) : MessageDetailsState
}
sealed interface MessageDetailEvent {
data class ShowCryptoKeys(val pendingIntent: PendingIntent) : MessageDetailEvent
object SearchCryptoKeys : MessageDetailEvent
object ShowCryptoWarning : MessageDetailEvent
}

View file

@ -0,0 +1,74 @@
package com.fsck.k9.ui.messagedetails
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.isVisible
import com.fsck.k9.contacts.ContactPictureLoader
import com.fsck.k9.ui.R
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
internal class ParticipantItem(
private val contactPictureLoader: ContactPictureLoader,
private val showContactsPicture: Boolean,
val participant: Participant
) : AbstractItem<ParticipantItem.ViewHolder>() {
override val type: Int = R.id.message_details_participant
override val layoutRes = R.layout.message_details_participant_item
override fun getViewHolder(v: View) = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<ParticipantItem>(view) {
val menuAddContact: View = view.findViewById(R.id.menu_add_contact)
val menuCompose: View = view.findViewById(R.id.menu_compose)
val menuOverflow: View = view.findViewById(R.id.menu_overflow)
private val contactPicture: ImageView = view.findViewById(R.id.contact_picture)
private val name = view.findViewById<TextView>(R.id.name)
private val email = view.findViewById<TextView>(R.id.email)
private val originalBackground = view.background
init {
TooltipCompat.setTooltipText(menuAddContact, menuAddContact.contentDescription)
TooltipCompat.setTooltipText(menuCompose, menuCompose.contentDescription)
TooltipCompat.setTooltipText(menuOverflow, menuOverflow.contentDescription)
}
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
} else {
name.text = address.address
email.isVisible = false
}
menuAddContact.isVisible = !participant.isInContacts
if (item.showContactsPicture) {
item.contactPictureLoader.setContactPicture(contactPicture, address)
} else {
contactPicture.isVisible = false
}
if (!item.participant.isInContacts) {
itemView.isClickable = false
itemView.background = null
}
}
override fun unbindView(item: ParticipantItem) {
name.text = null
email.text = null
email.isVisible = true
contactPicture.isVisible = true
itemView.background = originalBackground
itemView.isClickable = true
}
}
}

View file

@ -0,0 +1,32 @@
package com.fsck.k9.ui.messagedetails
import android.view.View
import android.widget.TextView
import com.fsck.k9.ui.R
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
internal class SectionHeaderItem(
private val title: String,
private val extra: String?
) : AbstractItem<SectionHeaderItem.ViewHolder>() {
override val type: Int = R.id.message_details_section_header
override val layoutRes = R.layout.message_details_section_header_item
override fun getViewHolder(v: View) = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<SectionHeaderItem>(view) {
private val textView = view.findViewById<TextView>(R.id.title)
private val extraTextView = view.findViewById<TextView>(R.id.extra)
override fun bindView(item: SectionHeaderItem, payloads: List<Any>) {
textView.text = item.title
extraTextView.text = item.extra
}
override fun unbindView(item: SectionHeaderItem) {
textView.text = null
extraTextView.text = null
}
}
}

View file

@ -0,0 +1,16 @@
package com.fsck.k9.ui.messagedetails
import android.content.Context
import android.content.Intent
import android.net.Uri
internal class ShowContactLauncher {
fun launch(context: Context, contactLookupUri: Uri) {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = contactLookupUri
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
}
context.startActivity(intent)
}
}

View file

@ -157,6 +157,9 @@ class MessageListFragment :
maybeHideFloatingActionButton()
}
val isShowAccountChip: Boolean
get() = !isSingleAccountMode
override fun onAttach(context: Context) {
super.onAttach(context)
@ -571,7 +574,7 @@ class MessageListFragment :
showContactPicture = K9.isShowContactPicture,
showingThreadedList = showingThreadedList,
backGroundAsReadIndicator = K9.isUseBackgroundAsUnreadIndicator,
showAccountChip = !isSingleAccountMode
showAccountChip = isShowAccountChip
)
private fun getFolderInfoHolder(folderId: Long, account: Account): FolderInfoHolder {

View file

@ -1,140 +0,0 @@
package com.fsck.k9.ui.messageview;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.fsck.k9.ui.R;
import com.fsck.k9.view.MessageCryptoDisplayStatus;
import com.fsck.k9.view.ThemeUtils;
public class CryptoInfoDialog extends DialogFragment {
public static final String ARG_DISPLAY_STATUS = "display_status";
public static final String ARG_HAS_SECURITY_WARNING = "has_security_warning";
private ImageView statusIcon;
private TextView titleText;
private TextView descriptionText;
public static CryptoInfoDialog newInstance(MessageCryptoDisplayStatus displayStatus, boolean hasSecurityWarning) {
CryptoInfoDialog frag = new CryptoInfoDialog();
Bundle args = new Bundle();
args.putString(ARG_DISPLAY_STATUS, displayStatus.toString());
args.putBoolean(ARG_HAS_SECURITY_WARNING, hasSecurityWarning);
frag.setArguments(args);
return frag;
}
@SuppressLint("InflateParams") // inflating without root element is fine for creating a dialog
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Builder b = new AlertDialog.Builder(getActivity());
View dialogView = LayoutInflater.from(getActivity()).inflate(R.layout.message_crypto_info_dialog, null);
statusIcon = dialogView.findViewById(R.id.crypto_info_top_icon_1);
titleText = dialogView.findViewById(R.id.crypto_info_title);
descriptionText = dialogView.findViewById(R.id.crypto_info_text);
MessageCryptoDisplayStatus displayStatus =
MessageCryptoDisplayStatus.valueOf(getArguments().getString(ARG_DISPLAY_STATUS));
setMessageForDisplayStatus(displayStatus);
b.setView(dialogView);
b.setPositiveButton(R.string.crypto_info_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dismiss();
}
});
boolean hasSecurityWarning = getArguments().getBoolean(ARG_HAS_SECURITY_WARNING);
if (hasSecurityWarning) {
b.setNeutralButton(R.string.crypto_info_view_security_warning, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Fragment frag = getTargetFragment();
if (!(frag instanceof OnClickShowCryptoKeyListener)) {
throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!");
}
((OnClickShowCryptoKeyListener) frag).onClickShowSecurityWarning();
}
});
} else if (displayStatus.isUnknownKey()) {
b.setNeutralButton(R.string.crypto_info_search_key, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Fragment frag = getTargetFragment();
if (! (frag instanceof OnClickShowCryptoKeyListener)) {
throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!");
}
((OnClickShowCryptoKeyListener) frag).onClickSearchKey();
}
});
} else if (displayStatus.hasAssociatedKey()) {
int buttonLabel = displayStatus.isUnencryptedSigned() ?
R.string.crypto_info_view_signer : R.string.crypto_info_view_sender;
b.setNeutralButton(buttonLabel, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Fragment frag = getTargetFragment();
if (! (frag instanceof OnClickShowCryptoKeyListener)) {
throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!");
}
((OnClickShowCryptoKeyListener) frag).onClickShowCryptoKey();
}
});
}
return b.create();
}
private void setMessageForDisplayStatus(MessageCryptoDisplayStatus displayStatus) {
if (displayStatus.getTitleTextRes() == null) {
throw new AssertionError("Crypto info dialog can only be displayed for items with text!");
}
setMessageSingleLine(displayStatus.getColorAttr(), displayStatus.getTitleTextRes(),
displayStatus.getDescriptionTextRes(), displayStatus.getStatusIconRes());
}
private void setMessageSingleLine(@AttrRes int colorAttr, @StringRes int titleTextRes,
@StringRes Integer descTextRes, @DrawableRes int statusIconRes) {
@ColorInt int color = ThemeUtils.getStyledColor(getActivity(), colorAttr);
statusIcon.setImageResource(statusIconRes);
statusIcon.setColorFilter(color);
titleText.setText(titleTextRes);
if (descTextRes != null) {
descriptionText.setText(descTextRes);
descriptionText.setVisibility(View.VISIBLE);
} else {
descriptionText.setVisibility(View.GONE);
}
}
public interface OnClickShowCryptoKeyListener {
void onClickShowCryptoKey();
void onClickShowSecurityWarning();
void onClickSearchKey();
}
}

View file

@ -0,0 +1,59 @@
package com.fsck.k9.ui.messageview
import com.fsck.k9.Account
import com.fsck.k9.helper.AddressFormatter
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Message
/**
* Extract recipient names from a message to display them in the message view.
*
* This class extracts up to [maxNumberOfDisplayRecipients] recipients from the message and converts them to their
* display name using an [AddressFormatter].
*/
internal class DisplayRecipientsExtractor(
private val addressFormatter: AddressFormatter,
private val maxNumberOfDisplayRecipients: Int
) {
fun extractDisplayRecipients(message: Message, account: Account): DisplayRecipients {
val toRecipients = message.getRecipients(Message.RecipientType.TO)
val ccRecipients = message.getRecipients(Message.RecipientType.CC)
val bccRecipients = message.getRecipients(Message.RecipientType.BCC)
val numberOfRecipients = toRecipients.size + ccRecipients.size + bccRecipients.size
val identity = sequenceOf(toRecipients, ccRecipients, bccRecipients)
.flatMap { addressArray -> addressArray.asSequence() }
.mapNotNull { address -> account.findIdentity(address) }
.firstOrNull()
val identityEmail = identity?.email
val maxAdditionalRecipients = if (identity != null) {
maxNumberOfDisplayRecipients - 1
} else {
maxNumberOfDisplayRecipients
}
val recipientNames = sequenceOf(toRecipients, ccRecipients, bccRecipients)
.flatMap { addressArray -> addressArray.asSequence() }
.filter { address -> address.address != identityEmail }
.map { address -> addressFormatter.getDisplayName(address) }
.take(maxAdditionalRecipients)
.toList()
return if (identity != null) {
val identityAddress = Address(identity.email)
val meName = addressFormatter.getDisplayName(identityAddress)
val recipients = listOf(meName) + recipientNames
DisplayRecipients(recipients, numberOfRecipients)
} else {
DisplayRecipients(recipientNames, numberOfRecipients)
}
}
}
internal data class DisplayRecipients(
val recipientNames: List<CharSequence>,
val numberOfRecipients: Int
)

View file

@ -20,7 +20,7 @@ import timber.log.Timber;
@SuppressWarnings("WeakerAccess")
public class MessageCryptoPresenter implements OnCryptoClickListener {
public class MessageCryptoPresenter {
public static final int REQUEST_CODE_UNKNOWN_KEY = 123;
public static final int REQUEST_CODE_SECURITY_WARNING = 124;
@ -38,6 +38,10 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
this.messageCryptoMvpView = messageCryptoMvpView;
}
public CryptoResultAnnotation getCryptoResultAnnotation() {
return cryptoResultAnnotation;
}
public void onResume() {
if (reloadOnResumeWithoutRecreateFlag) {
reloadOnResumeWithoutRecreateFlag = false;
@ -96,23 +100,6 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
return true;
}
@Override
public void onCryptoClick() {
if (cryptoResultAnnotation == null) {
return;
}
MessageCryptoDisplayStatus displayStatus =
MessageCryptoDisplayStatus.fromResultAnnotation(cryptoResultAnnotation);
switch (displayStatus) {
case LOADING:
// no need to do anything, there is a progress bar...
break;
default:
displayCryptoInfoDialog(displayStatus);
break;
}
}
@SuppressWarnings("UnusedParameters") // for consistency with Activity.onActivityResult
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_UNKNOWN_KEY) {
@ -128,11 +115,6 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
}
}
private void displayCryptoInfoDialog(MessageCryptoDisplayStatus displayStatus) {
messageCryptoMvpView.showCryptoInfoDialog(
displayStatus, cryptoResultAnnotation.hasOpenPgpInsecureWarningPendingIntent());
}
void onClickSearchKey() {
try {
PendingIntent pendingIntent = cryptoResultAnnotation.getOpenPgpSigningKeyIntentIfAny();
@ -145,18 +127,6 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
}
}
public void onClickShowCryptoKey() {
try {
PendingIntent pendingIntent = cryptoResultAnnotation.getOpenPgpSigningKeyIntentIfAny();
if (pendingIntent != null) {
messageCryptoMvpView.startPendingIntentForCryptoPresenter(
pendingIntent.getIntentSender(), null, null, 0, 0, 0);
}
} catch (IntentSender.SendIntentException e) {
Timber.e(e, "SendIntentException");
}
}
public void onClickRetryCryptoOperation() {
messageCryptoMvpView.restartMessageCryptoProcessing();
}
@ -204,7 +174,6 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
void startPendingIntentForCryptoPresenter(IntentSender si, Integer requestCode, Intent fillIntent,
int flagsMask, int flagValues, int extraFlags) throws IntentSender.SendIntentException;
void showCryptoInfoDialog(MessageCryptoDisplayStatus displayStatus, boolean hasSecurityWarning);
void showCryptoConfigDialog();
}
}

View file

@ -0,0 +1,6 @@
package com.fsck.k9.ui.messageview
interface MessageHeaderClickListener {
fun onParticipantsContainerClick()
fun onMenuItemClick(itemId: Int)
}

View file

@ -9,7 +9,7 @@ import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@ -51,10 +51,13 @@ public class MessageTopView extends LinearLayout {
private ViewGroup containerView;
private Button mDownloadRemainder;
private AttachmentViewCallback attachmentCallback;
private View extraHeaderContainer;
private Button showPicturesButton;
private boolean isShowingProgress;
private boolean showPicturesButtonClicked;
private boolean showAccountChip;
private MessageCryptoPresenter messageCryptoPresenter;
@ -76,6 +79,7 @@ public class MessageTopView extends LinearLayout {
mDownloadRemainder = findViewById(R.id.download_remainder);
mDownloadRemainder.setVisibility(View.GONE);
extraHeaderContainer = findViewById(R.id.extra_header_container);
showPicturesButton = findViewById(R.id.show_pictures);
setShowPicturesButtonListener();
@ -84,6 +88,10 @@ public class MessageTopView extends LinearLayout {
hideHeaderView();
}
public void setShowAccountChip(boolean showAccountChip) {
this.showAccountChip = showAccountChip;
}
private void setShowPicturesButtonListener() {
showPicturesButton.setOnClickListener(new OnClickListener() {
@Override
@ -208,7 +216,7 @@ public class MessageTopView extends LinearLayout {
}
public void setHeaders(Message message, Account account, boolean showStar) {
mHeaderContainer.populate(message, account, showStar);
mHeaderContainer.populate(message, account, showStar, showAccountChip);
mHeaderContainer.setVisibility(View.VISIBLE);
}
@ -220,8 +228,8 @@ public class MessageTopView extends LinearLayout {
mHeaderContainer.setOnFlagListener(listener);
}
public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
mHeaderContainer.setOnMenuItemClickListener(listener);
public void setMessageHeaderClickListener(MessageHeaderClickListener listener) {
mHeaderContainer.setMessageHeaderClickListener(listener);
}
private void hideHeaderView() {
@ -238,7 +246,6 @@ public class MessageTopView extends LinearLayout {
public void setMessageCryptoPresenter(MessageCryptoPresenter messageCryptoPresenter) {
this.messageCryptoPresenter = messageCryptoPresenter;
mHeaderContainer.setOnCryptoClickListener(messageCryptoPresenter);
}
public void enableDownloadButton() {
@ -259,11 +266,11 @@ public class MessageTopView extends LinearLayout {
}
private void showShowPicturesButton() {
showPicturesButton.setVisibility(View.VISIBLE);
extraHeaderContainer.setVisibility(View.VISIBLE);
}
private void hideShowPicturesButton() {
showPicturesButton.setVisibility(View.GONE);
extraHeaderContainer.setVisibility(View.GONE);
}
private boolean shouldAutomaticallyLoadPictures(ShowPictures showPicturesSetting, Message message) {

View file

@ -29,6 +29,8 @@ class MessageViewContainerFragment : Fragment() {
setMenuVisibility(value)
}
private var showAccountChip: Boolean = true
lateinit var messageReference: MessageReference
private set
@ -72,7 +74,9 @@ class MessageViewContainerFragment : Fragment() {
lastDirection = savedInstanceState.getSerializable(STATE_LAST_DIRECTION) as Direction?
}
adapter = MessageViewContainerAdapter(this)
showAccountChip = arguments?.getBoolean(ARG_SHOW_ACCOUNT_CHIP) ?: showAccountChip
adapter = MessageViewContainerAdapter(this, showAccountChip)
}
override fun onAttach(context: Context) {
@ -229,7 +233,11 @@ class MessageViewContainerFragment : Fragment() {
findMessageViewFragment().onPendingIntentResult(requestCode, resultCode, data)
}
private class MessageViewContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
private class MessageViewContainerAdapter(
fragment: Fragment,
private val showAccountChip: Boolean
) : FragmentStateAdapter(fragment) {
var messageList: List<MessageListItem> = emptyList()
set(value) {
val diffResult = DiffUtil.calculateDiff(
@ -257,7 +265,7 @@ class MessageViewContainerFragment : Fragment() {
check(position in messageList.indices)
val messageReference = messageList[position].messageReference
return MessageViewFragment.newInstance(messageReference)
return MessageViewFragment.newInstance(messageReference, showAccountChip)
}
fun getMessageReference(position: Int): MessageReference? {
@ -298,13 +306,15 @@ class MessageViewContainerFragment : Fragment() {
companion object {
private const val ARG_REFERENCE = "reference"
private const val ARG_SHOW_ACCOUNT_CHIP = "showAccountChip"
private const val STATE_MESSAGE_REFERENCE = "messageReference"
private const val STATE_LAST_DIRECTION = "lastDirection"
fun newInstance(reference: MessageReference): MessageViewContainerFragment {
fun newInstance(reference: MessageReference, showAccountChip: Boolean): MessageViewContainerFragment {
return MessageViewContainerFragment().withArguments(
ARG_REFERENCE to reference.toIdentityString()
ARG_REFERENCE to reference.toIdentityString(),
ARG_SHOW_ACCOUNT_CHIP to showAccountChip
)
}
}

View file

@ -21,6 +21,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.withStyledAttributes
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import com.fsck.k9.Account
import com.fsck.k9.K9
import com.fsck.k9.activity.MessageCompose
@ -45,13 +46,12 @@ import com.fsck.k9.ui.R
import com.fsck.k9.ui.base.Theme
import com.fsck.k9.ui.base.ThemeManager
import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
import com.fsck.k9.ui.messagedetails.MessageDetailsFragment
import com.fsck.k9.ui.messagesource.MessageSourceActivity
import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener
import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView
import com.fsck.k9.ui.settings.account.AccountSettingsActivity
import com.fsck.k9.ui.share.ShareIntentBuilder
import com.fsck.k9.ui.withArguments
import com.fsck.k9.view.MessageCryptoDisplayStatus
import java.util.Locale
import org.koin.android.ext.android.inject
import timber.log.Timber
@ -59,8 +59,7 @@ import timber.log.Timber
class MessageViewFragment :
Fragment(),
ConfirmationDialogFragmentListener,
AttachmentViewCallback,
OnClickShowCryptoKeyListener {
AttachmentViewCallback {
private val themeManager: ThemeManager by inject()
private val messageLoaderHelperFactory: MessageLoaderHelperFactory by inject()
@ -86,6 +85,7 @@ class MessageViewFragment :
private lateinit var account: Account
lateinit var messageReference: MessageReference
private var showAccountChip: Boolean = true
private var currentAttachmentViewInfo: AttachmentViewInfo? = null
private var isDeleteMenuItemDisabled: Boolean = false
@ -118,6 +118,9 @@ class MessageViewFragment :
messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE))
?: error("Invalid argument '$ARG_REFERENCE'")
showAccountChip = arguments?.getBoolean(ARG_SHOW_ACCOUNT_CHIP)
?: error("Missing argument: '$ARG_SHOW_ACCOUNT_CHIP'")
if (savedInstanceState != null) {
wasMessageMarkedAsOpened = savedInstanceState.getBoolean(STATE_WAS_MESSAGE_MARKED_AS_OPENED)
}
@ -129,6 +132,8 @@ class MessageViewFragment :
fragmentManager = parentFragmentManager,
callback = messageLoaderCallbacks
)
setFragmentResultListener(MessageDetailsFragment.FRAGMENT_RESULT_KEY, ::onMessageDetailsResult)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -145,6 +150,8 @@ class MessageViewFragment :
}
private fun initializeMessageTopView(messageTopView: MessageTopView) {
messageTopView.setShowAccountChip(showAccountChip)
messageTopView.setAttachmentCallback(this)
messageTopView.setMessageCryptoPresenter(messageCryptoPresenter)
@ -152,9 +159,7 @@ class MessageViewFragment :
onToggleFlagged()
}
messageTopView.setOnMenuItemClickListener { item ->
onReplyMenuItemClicked(item.itemId)
}
messageTopView.setMessageHeaderClickListener(messageHeaderClickListener)
messageTopView.setOnDownloadButtonClickListener {
onDownloadButtonClicked()
@ -376,17 +381,23 @@ class MessageViewFragment :
messageTopView.setSubject(displaySubject)
}
private fun onReplyMenuItemClicked(itemId: Int): Boolean {
when (itemId) {
R.id.reply -> onReply()
R.id.reply_all -> onReplyAll()
R.id.forward -> onForward()
R.id.forward_as_attachment -> onForwardAsAttachment()
R.id.share -> onSendAlternate()
else -> error("Missing handler for reply menu item $itemId")
private val messageHeaderClickListener = object : MessageHeaderClickListener {
override fun onParticipantsContainerClick() {
val messageDetailsFragment = MessageDetailsFragment.create(messageReference)
messageDetailsFragment.cryptoResult = messageCryptoPresenter.cryptoResultAnnotation
messageDetailsFragment.show(parentFragmentManager, "message_details")
}
return true
override fun onMenuItemClick(itemId: Int) {
when (itemId) {
R.id.reply -> onReply()
R.id.reply_all -> onReplyAll()
R.id.forward -> onForward()
R.id.forward_as_attachment -> onForwardAsAttachment()
R.id.share -> onSendAlternate()
else -> error("Missing handler for reply menu item $itemId")
}
}
}
private fun onDownloadButtonClicked() {
@ -568,6 +579,20 @@ class MessageViewFragment :
}
}
private fun onMessageDetailsResult(requestKey: String, result: Bundle) {
when (val action = result.getString(MessageDetailsFragment.RESULT_ACTION)) {
MessageDetailsFragment.ACTION_SEARCH_KEYS -> {
messageCryptoPresenter.onClickSearchKey()
}
MessageDetailsFragment.ACTION_SHOW_WARNING -> {
messageCryptoPresenter.onClickShowCryptoWarningDetails()
}
else -> {
error("Unsupported action: $action")
}
}
}
private fun onCreateDocumentResult(data: Intent?) {
if (data != null && data.data != null) {
createAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(data.data)
@ -806,12 +831,6 @@ class MessageViewFragment :
)
}
override fun showCryptoInfoDialog(displayStatus: MessageCryptoDisplayStatus, hasSecurityWarning: Boolean) {
val dialog = CryptoInfoDialog.newInstance(displayStatus, hasSecurityWarning)
dialog.setTargetFragment(this@MessageViewFragment, 0)
dialog.show(parentFragmentManager, "crypto_info_dialog")
}
override fun restartMessageCryptoProcessing() {
messageTopView.setToLoadingState()
messageLoaderHelper.asyncRestartMessageCryptoProcessing()
@ -822,18 +841,6 @@ class MessageViewFragment :
}
}
override fun onClickShowSecurityWarning() {
messageCryptoPresenter.onClickShowCryptoWarningDetails()
}
override fun onClickSearchKey() {
messageCryptoPresenter.onClickSearchKey()
}
override fun onClickShowCryptoKey() {
messageCryptoPresenter.onClickShowCryptoKey()
}
interface MessageViewFragmentListener {
fun onForward(messageReference: MessageReference, decryptionResultForReply: Parcelable?)
fun onForwardAsAttachment(messageReference: MessageReference, decryptionResultForReply: Parcelable?)
@ -960,6 +967,7 @@ class MessageViewFragment :
const val PROGRESS_THRESHOLD_MILLIS = 500 * 1000
private const val ARG_REFERENCE = "reference"
private const val ARG_SHOW_ACCOUNT_CHIP = "showAccountChip"
private const val STATE_WAS_MESSAGE_MARKED_AS_OPENED = "wasMessageMarkedAsOpened"
@ -967,9 +975,10 @@ class MessageViewFragment :
private const val ACTIVITY_CHOOSE_FOLDER_COPY = 2
private const val REQUEST_CODE_CREATE_DOCUMENT = 3
fun newInstance(reference: MessageReference): MessageViewFragment {
fun newInstance(reference: MessageReference, showAccountChip: Boolean): MessageViewFragment {
return MessageViewFragment().withArguments(
ARG_REFERENCE to reference.toIdentityString()
ARG_REFERENCE to reference.toIdentityString(),
ARG_SHOW_ACCOUNT_CHIP to showAccountChip
)
}
}

View file

@ -1,6 +0,0 @@
package com.fsck.k9.ui.messageview;
public interface OnCryptoClickListener {
void onCryptoClick();
}

View file

@ -0,0 +1,119 @@
package com.fsck.k9.ui.messageview
import android.text.SpannableStringBuilder
private const val LIST_SEPARATOR = ", "
/**
* Calculates how many recipient names can be displayed given the available width.
*
* We display up to [maxNumberOfRecipientNames] recipient names, then the number of additional recipients.
*
* Example:
* to me, Alice, Bob, Charly, Dora +11
*
* If there's not enough room to display the first recipient name, we return it anyway and expect the component that is
* actually rendering the text to ellipsize [RecipientLayoutData.recipientNames], but not
* [RecipientLayoutData.additionalRecipients].
*/
internal class RecipientLayoutCreator(
private val textMeasure: TextMeasure,
private val maxNumberOfRecipientNames: Int,
private val recipientsPrefix: String,
private val additionalRecipientSpacing: Int,
private val additionalRecipientsPrefix: String
) {
fun createRecipientLayout(
recipientNames: List<CharSequence>,
totalNumberOfRecipients: Int,
availableWidth: Int
): RecipientLayoutData {
require(recipientNames.isNotEmpty())
val displayRecipientsBuilder = SpannableStringBuilder()
if (recipientNames.size == 1) {
displayRecipientsBuilder.append(recipientsPrefix)
displayRecipientsBuilder.append(recipientNames.first())
return RecipientLayoutData(
recipientNames = displayRecipientsBuilder,
additionalRecipients = null
)
}
val additionalRecipientsBuilder = StringBuilder(additionalRecipientsPrefix + 10)
val maxRecipientNames = recipientNames.size.coerceAtMost(maxNumberOfRecipientNames)
for (numberOfDisplayRecipients in maxRecipientNames downTo 2) {
displayRecipientsBuilder.clear()
displayRecipientsBuilder.append(recipientsPrefix)
recipientNames.asSequence()
.take(numberOfDisplayRecipients)
.joinTo(displayRecipientsBuilder, separator = LIST_SEPARATOR)
additionalRecipientsBuilder.setLength(0)
val numberOfAdditionalRecipients = totalNumberOfRecipients - numberOfDisplayRecipients
if (numberOfAdditionalRecipients > 0) {
additionalRecipientsBuilder.append(additionalRecipientsPrefix)
additionalRecipientsBuilder.append(numberOfAdditionalRecipients)
}
if (doesTextFitAvailableWidth(displayRecipientsBuilder, additionalRecipientsBuilder, availableWidth)) {
return RecipientLayoutData(
recipientNames = displayRecipientsBuilder,
additionalRecipients = additionalRecipientsBuilder.toStringOrNull()
)
}
}
displayRecipientsBuilder.clear()
displayRecipientsBuilder.append(recipientsPrefix)
displayRecipientsBuilder.append(recipientNames.first())
return RecipientLayoutData(
recipientNames = displayRecipientsBuilder,
additionalRecipients = "$additionalRecipientsPrefix${totalNumberOfRecipients - 1}"
)
}
private fun doesTextFitAvailableWidth(
displayRecipients: CharSequence,
additionalRecipients: CharSequence,
availableWidth: Int
): Boolean {
val recipientNamesWidth = textMeasure.measureRecipientNames(displayRecipients)
if (recipientNamesWidth > availableWidth) {
return false
} else if (additionalRecipients.isEmpty()) {
return true
}
val totalWidth = recipientNamesWidth + additionalRecipientSpacing +
textMeasure.measureRecipientCount(additionalRecipients)
return totalWidth <= availableWidth
}
}
private fun StringBuilder.toStringOrNull(): String? {
return if (isEmpty()) null else toString()
}
internal data class RecipientLayoutData(
val recipientNames: CharSequence,
val additionalRecipients: String?
)
internal interface TextMeasure {
/**
* Measure the width of the supplied recipient names when rendered.
*/
fun measureRecipientNames(text: CharSequence): Int
/**
* Measure the width of the supplied recipient count when rendered.
*/
fun measureRecipientCount(text: CharSequence): Int
}

View file

@ -0,0 +1,184 @@
package com.fsck.k9.ui.messageview
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isGone
import com.fsck.k9.ui.R
private const val MAX_NUMBER_OF_RECIPIENT_NAMES = 5
/**
* View that displays the names of recipients of a message.
*
* Up to [MAX_NUMBER_OF_RECIPIENT_NAMES] names of recipients are displayed, followed by the number of recipients that
* weren't displayed.
*
* Examples:
* - to me, Alice, Bob, Charly +3
* - to Camila Hyphenated-Nam +5
*
* This custom layout uses [RecipientLayoutCreator] to figure out how many recipient names can be displayed without
* being truncated. If not even one recipient name can be displayed without being truncated, we first measure the space
* needed for number of additional recipients, then use the rest to display the first recipient and ellipsize the end.
*/
class RecipientNamesView(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
val maxNumberOfRecipientNames: Int = MAX_NUMBER_OF_RECIPIENT_NAMES
private val recipientLayoutCreator: RecipientLayoutCreator
private val recipientNameTextView: TextView
private val recipientCountTextView: TextView
private val additionRecipientSpacing: Int
init {
LayoutInflater.from(context).inflate(R.layout.recipient_names, this, true)
recipientNameTextView = findViewById(R.id.recipient_names)
recipientCountTextView = findViewById(R.id.recipient_count)
additionRecipientSpacing = (recipientCountTextView.layoutParams as MarginLayoutParams).marginStart
}
private var recipientNames: List<CharSequence> = emptyList()
private var numberOfRecipients: Int = 0
private val textMeasure = object : TextMeasure {
override fun measureRecipientNames(text: CharSequence): Int {
return measureWidth(recipientNameTextView, text)
}
override fun measureRecipientCount(text: CharSequence): Int {
return measureWidth(recipientCountTextView, text)
}
private fun measureWidth(textView: TextView, text: CharSequence): Int {
textView.text = text
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)
textView.measure(widthMeasureSpec, heightMeasureSpec)
return textView.measuredWidth
}
}
init {
recipientLayoutCreator = RecipientLayoutCreator(
textMeasure = textMeasure,
maxNumberOfRecipientNames = MAX_NUMBER_OF_RECIPIENT_NAMES,
recipientsPrefix = context.getString(R.string.message_view_recipient_prefix),
additionalRecipientSpacing = additionRecipientSpacing,
additionalRecipientsPrefix = context.getString(R.string.message_view_additional_recipient_prefix)
)
if (isInEditMode) {
recipientNames = listOf(
"Grace Hopper", "Katherine Johnson", "Margaret Hamilton", "Adele Goldberg", "Steve Shirley"
)
numberOfRecipients = 8
}
}
fun setTextSize(textSize: Int) {
recipientNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize.toFloat())
recipientCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize.toFloat())
}
fun setRecipients(recipientNames: List<CharSequence>, numberOfRecipients: Int) {
if (recipientNames != this.recipientNames && numberOfRecipients != this.numberOfRecipients) {
this.recipientNames = recipientNames
this.numberOfRecipients = numberOfRecipients
requestLayout()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
require(MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
"Width of RecipientNamesView needs to be constrained"
}
recipientNameTextView.measure(widthMeasureSpec, heightMeasureSpec)
recipientCountTextView.measure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = maxOf(recipientNameTextView.measuredHeight, recipientCountTextView.measuredHeight)
setMeasuredDimension(width, height)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val availableWidth = width
val recipientLayoutData = recipientLayoutCreator.createRecipientLayout(
recipientNames, numberOfRecipients, availableWidth
)
recipientNameTextView.text = recipientLayoutData.recipientNames
val additionalRecipientsVisible = recipientLayoutData.additionalRecipients != null
val remainingWidth: Int
if (additionalRecipientsVisible) {
recipientCountTextView.isGone = false
recipientCountTextView.text = recipientLayoutData.additionalRecipients
recipientCountTextView.measure(
MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.AT_MOST)
)
remainingWidth = availableWidth - additionRecipientSpacing - recipientCountTextView.measuredWidth
} else {
recipientCountTextView.isGone = true
remainingWidth = availableWidth
}
recipientNameTextView.measure(
MeasureSpec.makeMeasureSpec(remainingWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.AT_MOST)
)
if (layoutDirection == LAYOUT_DIRECTION_LTR) {
val recipientNameRight = recipientNameTextView.measuredWidth
recipientNameTextView.layout(
0,
0,
recipientNameRight,
recipientNameTextView.measuredHeight
)
val recipientCountLeft = recipientNameRight + additionRecipientSpacing
recipientCountTextView.layout(
recipientCountLeft,
0,
recipientCountLeft + recipientCountTextView.measuredWidth,
recipientCountTextView.measuredHeight
)
} else {
val recipientNameLeft = width - recipientNameTextView.measuredWidth
recipientNameTextView.layout(
recipientNameLeft,
0,
right,
recipientNameTextView.measuredHeight
)
val recipientCountRight = recipientNameLeft - additionRecipientSpacing
recipientCountTextView.layout(
recipientCountRight - recipientCountTextView.measuredWidth,
0,
recipientCountRight,
0 + recipientCountTextView.measuredHeight
)
}
}
override fun checkLayoutParams(p: LayoutParams?): Boolean {
return p is MarginLayoutParams
}
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(0, 0)
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}

View file

@ -117,12 +117,9 @@ class GeneralSettingsDataStore(
"message_list_date_font" -> K9.fontSizes.messageListDate.toString()
"message_list_preview_font" -> K9.fontSizes.messageListPreview.toString()
"message_view_sender_font" -> K9.fontSizes.messageViewSender.toString()
"message_view_to_font" -> K9.fontSizes.messageViewTo.toString()
"message_view_cc_font" -> K9.fontSizes.messageViewCC.toString()
"message_view_bcc_font" -> K9.fontSizes.messageViewBCC.toString()
"message_view_recipients_font" -> K9.fontSizes.messageViewRecipients.toString()
"message_view_subject_font" -> K9.fontSizes.messageViewSubject.toString()
"message_view_date_font" -> K9.fontSizes.messageViewDate.toString()
"message_view_additional_headers_font" -> K9.fontSizes.messageViewAdditionalHeaders.toString()
"message_compose_input_font" -> K9.fontSizes.messageComposeInput.toString()
"swipe_action_right" -> swipeActionToString(K9.swipeRightAction)
"swipe_action_left" -> swipeActionToString(K9.swipeLeftAction)
@ -154,12 +151,9 @@ class GeneralSettingsDataStore(
"message_list_date_font" -> K9.fontSizes.messageListDate = value.toInt()
"message_list_preview_font" -> K9.fontSizes.messageListPreview = value.toInt()
"message_view_sender_font" -> K9.fontSizes.messageViewSender = value.toInt()
"message_view_to_font" -> K9.fontSizes.messageViewTo = value.toInt()
"message_view_cc_font" -> K9.fontSizes.messageViewCC = value.toInt()
"message_view_bcc_font" -> K9.fontSizes.messageViewBCC = value.toInt()
"message_view_recipients_font" -> K9.fontSizes.messageViewRecipients = value.toInt()
"message_view_subject_font" -> K9.fontSizes.messageViewSubject = value.toInt()
"message_view_date_font" -> K9.fontSizes.messageViewDate = value.toInt()
"message_view_additional_headers_font" -> K9.fontSizes.messageViewAdditionalHeaders = value.toInt()
"message_compose_input_font" -> K9.fontSizes.messageComposeInput = value.toInt()
"swipe_action_right" -> K9.swipeRightAction = stringToSwipeAction(value)
"swipe_action_left" -> K9.swipeLeftAction = stringToSwipeAction(value)

View file

@ -1,7 +1,13 @@
package com.fsck.k9.view
import com.fsck.k9.helper.ReplyToParser
import com.fsck.k9.message.ReplyActionStrategy
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
import org.koin.dsl.module
val viewModule = module {
single { WebViewConfigProvider(get()) }
single { WebViewConfigProvider(themeManager = get()) }
factory { RelativeDateTimeFormatter(context = get(), clock = get()) }
factory { ReplyToParser() }
factory { ReplyActionStrategy(replyRoParser = get()) }
}

View file

@ -1,24 +1,23 @@
package com.fsck.k9.view;
import java.util.Arrays;
import java.util.List;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.content.res.ColorStateList;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener;
import androidx.appcompat.widget.TooltipCompat;
import com.fsck.k9.Account;
import com.fsck.k9.DI;
import com.fsck.k9.FontSizes;
@ -28,112 +27,90 @@ import com.fsck.k9.contacts.ContactPictureLoader;
import com.fsck.k9.helper.ClipboardManager;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.helper.MessageHelper;
import com.fsck.k9.helper.RealAddressFormatter;
import com.fsck.k9.helper.RealContactNameProvider;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.ui.ContactBadge;
import com.fsck.k9.message.ReplyAction;
import com.fsck.k9.message.ReplyActionStrategy;
import com.fsck.k9.message.ReplyActions;
import com.fsck.k9.ui.R;
import com.fsck.k9.ui.messageview.OnCryptoClickListener;
import timber.log.Timber;
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter;
import com.fsck.k9.ui.messageview.DisplayRecipients;
import com.fsck.k9.ui.messageview.DisplayRecipientsExtractor;
import com.fsck.k9.ui.messageview.MessageHeaderClickListener;
import com.fsck.k9.ui.messageview.RecipientNamesView;
import com.google.android.material.chip.Chip;
public class MessageHeader extends LinearLayout implements OnClickListener, OnLongClickListener {
private static final int DEFAULT_SUBJECT_LINES = 3;
private final ClipboardManager clipboardManager = DI.get(ClipboardManager.class);
private final ReplyActionStrategy replyActionStrategy = DI.get(ReplyActionStrategy.class);
private final FontSizes fontSizes = K9.getFontSizes();
private Context mContext;
private TextView mFromView;
private TextView mSenderView;
private TextView mDateView;
private TextView mToView;
private TextView mToLabel;
private TextView mCcView;
private TextView mCcLabel;
private TextView mBccView;
private TextView mBccLabel;
private TextView mSubjectView;
private ImageView mCryptoStatusIcon;
private Chip accountChip;
private TextView subjectView;
private ImageView starView;
private ImageView contactPictureView;
private TextView fromView;
private ImageView cryptoStatusIcon;
private RecipientNamesView recipientNamesView;
private TextView dateView;
private ImageView menuPrimaryActionView;
private View mChip;
private CheckBox mFlagged;
private int defaultSubjectColor;
private View singleMessageOptionIcon;
private View mAnsweredIcon;
private View mForwardedIcon;
private Message mMessage;
private Account mAccount;
private FontSizes mFontSizes = K9.getFontSizes();
private Contacts mContacts;
private MessageHelper messageHelper;
private RelativeDateTimeFormatter relativeDateTimeFormatter;
private MessageHelper mMessageHelper;
private ContactPictureLoader mContactsPictureLoader;
private ContactBadge mContactBadge;
private OnCryptoClickListener onCryptoClickListener;
private OnMenuItemClickListener onMenuItemClickListener;
private MessageHeaderClickListener messageHeaderClickListener;
private ReplyActions replyActions;
public MessageHeader(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mContacts = Contacts.getInstance(mContext);
if (!isInEditMode()) {
messageHelper = MessageHelper.getInstance(getContext());
relativeDateTimeFormatter = DI.get(RelativeDateTimeFormatter.class);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mAnsweredIcon = findViewById(R.id.answered);
mForwardedIcon = findViewById(R.id.forwarded);
mFromView = findViewById(R.id.from);
mSenderView = findViewById(R.id.sender);
mToView = findViewById(R.id.to);
mToLabel = findViewById(R.id.to_label);
mCcView = findViewById(R.id.cc);
mCcLabel = findViewById(R.id.cc_label);
mBccView = findViewById(R.id.bcc);
mBccLabel = findViewById(R.id.bcc_label);
accountChip = findViewById(R.id.chip);
subjectView = findViewById(R.id.subject);
starView = findViewById(R.id.flagged);
contactPictureView = findViewById(R.id.contact_picture);
fromView = findViewById(R.id.from);
cryptoStatusIcon = findViewById(R.id.crypto_status_icon);
recipientNamesView = findViewById(R.id.recipients);
dateView = findViewById(R.id.date);
mContactBadge = findViewById(R.id.contact_badge);
fontSizes.setViewTextSize(subjectView, fontSizes.getMessageViewSubject());
fontSizes.setViewTextSize(dateView, fontSizes.getMessageViewDate());
fontSizes.setViewTextSize(fromView, fontSizes.getMessageViewSender());
singleMessageOptionIcon = findViewById(R.id.icon_single_message_options);
int recipientTextSize = fontSizes.getMessageViewRecipients();
if (recipientTextSize != FontSizes.FONT_DEFAULT) {
recipientNamesView.setTextSize(recipientTextSize);
}
mSubjectView = findViewById(R.id.subject);
mChip = findViewById(R.id.chip);
mDateView = findViewById(R.id.date);
mFlagged = findViewById(R.id.flagged);
subjectView.setOnClickListener(this);
subjectView.setOnLongClickListener(this);
defaultSubjectColor = mSubjectView.getCurrentTextColor();
mFontSizes.setViewTextSize(mSubjectView, mFontSizes.getMessageViewSubject());
mFontSizes.setViewTextSize(mDateView, mFontSizes.getMessageViewDate());
menuPrimaryActionView = findViewById(R.id.menu_primary_action);
menuPrimaryActionView.setOnClickListener(this);
mFontSizes.setViewTextSize(mFromView, mFontSizes.getMessageViewSender());
mFontSizes.setViewTextSize(mToView, mFontSizes.getMessageViewTo());
mFontSizes.setViewTextSize(mToLabel, mFontSizes.getMessageViewTo());
mFontSizes.setViewTextSize(mCcView, mFontSizes.getMessageViewCC());
mFontSizes.setViewTextSize(mCcLabel, mFontSizes.getMessageViewCC());
mFontSizes.setViewTextSize(mBccView, mFontSizes.getMessageViewBCC());
mFontSizes.setViewTextSize(mBccLabel, mFontSizes.getMessageViewBCC());
View menuOverflowView = findViewById(R.id.menu_overflow);
menuOverflowView.setOnClickListener(this);
String menuOverflowDescription =
getContext().getString(androidx.appcompat.R.string.abc_action_menu_overflow_description);
TooltipCompat.setTooltipText(menuOverflowView, menuOverflowDescription);
singleMessageOptionIcon.setOnClickListener(this);
mSubjectView.setOnClickListener(this);
mFromView.setOnClickListener(this);
mToView.setOnClickListener(this);
mCcView.setOnClickListener(this);
mBccView.setOnClickListener(this);
mSubjectView.setOnLongClickListener(this);
mFromView.setOnLongClickListener(this);
mToView.setOnLongClickListener(this);
mCcView.setOnLongClickListener(this);
mBccView.setOnLongClickListener(this);
mCryptoStatusIcon = findViewById(R.id.crypto_status_icon);
mCryptoStatusIcon.setOnClickListener(this);
mMessageHelper = MessageHelper.getInstance(mContext);
findViewById(R.id.participants_container).setOnClickListener(this);
}
@Override
@ -141,168 +118,217 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
int id = view.getId();
if (id == R.id.subject) {
toggleSubjectViewMaxLines();
} else if (id == R.id.from) {
onAddSenderToContacts();
} else if (id == R.id.to || id == R.id.cc || id == R.id.bcc) {
expand((TextView)view, ((TextView)view).getEllipsize() != null);
} else if (id == R.id.crypto_status_icon) {
onCryptoClickListener.onCryptoClick();
} else if (id == R.id.icon_single_message_options) {
PopupMenu popupMenu = new PopupMenu(getContext(), view);
popupMenu.setOnMenuItemClickListener(onMenuItemClickListener);
popupMenu.inflate(R.menu.single_message_options);
popupMenu.show();
} else if (id == R.id.menu_primary_action) {
performPrimaryReplyAction();
} else if (id == R.id.menu_overflow) {
showOverflowMenu(view);
} else if (id == R.id.participants_container) {
messageHeaderClickListener.onParticipantsContainerClick();
}
}
private void performPrimaryReplyAction() {
ReplyAction defaultAction = replyActions.getDefaultAction();
if (defaultAction == null) {
return;
}
switch (defaultAction) {
case REPLY: {
messageHeaderClickListener.onMenuItemClick(R.id.reply);
break;
}
case REPLY_ALL: {
messageHeaderClickListener.onMenuItemClick(R.id.reply_all);
break;
}
default: {
throw new IllegalStateException("Unknown reply action: " + defaultAction);
}
}
}
private void showOverflowMenu(View view) {
PopupMenu popupMenu = new PopupMenu(getContext(), view);
popupMenu.setOnMenuItemClickListener(item -> {
messageHeaderClickListener.onMenuItemClick(item.getItemId());
return true;
});
popupMenu.inflate(R.menu.single_message_options);
setAdditionalReplyActions(popupMenu);
popupMenu.show();
}
@Override
public boolean onLongClick(View view) {
int id = view.getId();
if (id == R.id.subject) {
onAddSubjectToClipboard(mSubjectView.getText().toString());
} else if (id == R.id.from) {
onAddAddressesToClipboard(mMessage.getFrom());
} else if (id == R.id.to) {
onAddRecipientsToClipboard(Message.RecipientType.TO);
} else if (id == R.id.cc) {
onAddRecipientsToClipboard(Message.RecipientType.CC);
onAddSubjectToClipboard(subjectView.getText().toString());
}
return true;
}
private void toggleSubjectViewMaxLines() {
if (mSubjectView.getMaxLines() == DEFAULT_SUBJECT_LINES) {
mSubjectView.setMaxLines(Integer.MAX_VALUE);
if (subjectView.getMaxLines() == DEFAULT_SUBJECT_LINES) {
subjectView.setMaxLines(Integer.MAX_VALUE);
} else {
mSubjectView.setMaxLines(DEFAULT_SUBJECT_LINES);
subjectView.setMaxLines(DEFAULT_SUBJECT_LINES);
}
}
private void onAddSubjectToClipboard(String subject) {
ClipboardManager clipboardManager = DI.get(ClipboardManager.class);
clipboardManager.setText("subject", subject);
Toast.makeText(mContext, createMessageForSubject(), Toast.LENGTH_LONG).show();
}
private void onAddSenderToContacts() {
if (mMessage != null) {
try {
final Address senderEmail = mMessage.getFrom()[0];
mContacts.createContact(senderEmail);
} catch (Exception e) {
Timber.e(e, "Couldn't create contact");
}
}
Toast.makeText(getContext(), createMessageForSubject(), Toast.LENGTH_LONG).show();
}
public String createMessageForSubject() {
return mContext.getResources().getString(R.string.copy_subject_to_clipboard);
}
public String createMessage(int addressesCount) {
return mContext.getResources().getQuantityString(R.plurals.copy_address_to_clipboard, addressesCount);
}
private void onAddAddressesToClipboard(Address[] addresses) {
String addressList = Address.toString(addresses);
clipboardManager.setText("addresses", addressList);
Toast.makeText(mContext, createMessage(addresses.length), Toast.LENGTH_LONG).show();
}
private void onAddRecipientsToClipboard(Message.RecipientType recipientType) {
onAddAddressesToClipboard(mMessage.getRecipients(recipientType));
return getResources().getString(R.string.copy_subject_to_clipboard);
}
public void setOnFlagListener(OnClickListener listener) {
mFlagged.setOnClickListener(listener);
starView.setOnClickListener(listener);
}
public void populate(final Message message, final Account account, boolean showStar) {
public void populate(final Message message, final Account account, boolean showStar, boolean showAccountChip) {
if (showAccountChip) {
accountChip.setVisibility(View.VISIBLE);
accountChip.setText(account.getDisplayName());
accountChip.setChipBackgroundColor(ColorStateList.valueOf(account.getChipColor()));
} else {
accountChip.setVisibility(View.GONE);
}
Address fromAddress = null;
Address[] fromAddresses = message.getFrom();
if (fromAddresses.length > 0) {
fromAddress = fromAddresses[0];
}
final Contacts contacts = K9.isShowContactName() ? mContacts : null;
final CharSequence from = mMessageHelper.getSenderDisplayName(fromAddress);
final CharSequence to = MessageHelper.toFriendly(message.getRecipients(Message.RecipientType.TO), contacts);
final CharSequence cc = MessageHelper.toFriendly(message.getRecipients(Message.RecipientType.CC), contacts);
final CharSequence bcc = MessageHelper.toFriendly(message.getRecipients(Message.RecipientType.BCC), contacts);
mMessage = message;
mAccount = account;
if (K9.isShowContactPicture()) {
mContactBadge.setVisibility(View.VISIBLE);
mContactsPictureLoader = ContactPicture.getContactPictureLoader();
} else {
mContactBadge.setVisibility(View.GONE);
}
if (shouldShowSender(message)) {
mSenderView.setVisibility(VISIBLE);
String sender = getResources().getString(R.string.message_view_sender_label,
MessageHelper.toFriendly(message.getSender(), contacts));
mSenderView.setText(sender);
} else {
mSenderView.setVisibility(View.GONE);
}
String dateTime = DateUtils.formatDateTime(mContext,
message.getSentDate().getTime(),
DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_ABBREV_ALL
| DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR);
mDateView.setText(dateTime);
if (K9.isShowContactPicture()) {
contactPictureView.setVisibility(View.VISIBLE);
if (fromAddress != null) {
mContactBadge.setContact(fromAddress);
mContactsPictureLoader.setContactPicture(mContactBadge, fromAddress);
ContactPictureLoader contactsPictureLoader = ContactPicture.getContactPictureLoader();
contactsPictureLoader.setContactPicture(contactPictureView, fromAddress);
} else {
mContactBadge.setImageResource(R.drawable.ic_contact_picture);
contactPictureView.setImageResource(R.drawable.ic_contact_picture);
}
} else {
contactPictureView.setVisibility(View.GONE);
}
mFromView.setText(from);
updateAddressField(mToView, to, mToLabel);
updateAddressField(mCcView, cc, mCcLabel);
updateAddressField(mBccView, bcc, mBccLabel);
mAnsweredIcon.setVisibility(message.isSet(Flag.ANSWERED) ? View.VISIBLE : View.GONE);
mForwardedIcon.setVisibility(message.isSet(Flag.FORWARDED) ? View.VISIBLE : View.GONE);
CharSequence from = messageHelper.getSenderDisplayName(fromAddress);
fromView.setText(from);
if (showStar) {
mFlagged.setVisibility(View.VISIBLE);
mFlagged.setChecked(message.isSet(Flag.FLAGGED));
starView.setVisibility(View.VISIBLE);
starView.setSelected(message.isSet(Flag.FLAGGED));
} else {
mFlagged.setVisibility(View.GONE);
starView.setVisibility(View.GONE);
}
mChip.setBackgroundColor(mAccount.getChipColor());
if (message.getSentDate() != null) {
dateView.setText(relativeDateTimeFormatter.formatDate(message.getSentDate().getTime()));
} else {
dateView.setText("");
}
setRecipientNames(message, account);
setReplyActions(message, account);
setVisibility(View.VISIBLE);
}
public void setSubject(@NonNull String subject) {
mSubjectView.setText(subject);
mSubjectView.setTextColor(0xff000000 | defaultSubjectColor);
private void setRecipientNames(Message message, Account account) {
Integer contactNameColor = K9.isChangeContactNameColor() ? K9.getContactNameColor() : null;
RealContactNameProvider contactNameProvider = new RealContactNameProvider(Contacts.getInstance(getContext()));
RealAddressFormatter addressFormatter = new RealAddressFormatter(contactNameProvider, account,
K9.isShowCorrespondentNames(), K9.isShowContactName(), contactNameColor,
getContext().getString(R.string.message_view_me_text));
DisplayRecipientsExtractor displayRecipientsExtractor = new DisplayRecipientsExtractor(addressFormatter,
recipientNamesView.getMaxNumberOfRecipientNames());
DisplayRecipients displayRecipients = displayRecipientsExtractor.extractDisplayRecipients(message, account);
recipientNamesView.setRecipients(displayRecipients.getRecipientNames(),
displayRecipients.getNumberOfRecipients());
}
public static boolean shouldShowSender(Message message) {
Address[] from = message.getFrom();
Address[] sender = message.getSender();
private void setReplyActions(Message message, Account account) {
ReplyActions replyActions = replyActionStrategy.getReplyActions(account, message);
this.replyActions = replyActions;
return sender != null && sender.length != 0 && !Arrays.equals(from, sender);
setDefaultReplyAction(replyActions.getDefaultAction());
}
private void setDefaultReplyAction(ReplyAction defaultAction) {
if (defaultAction == null) {
menuPrimaryActionView.setVisibility(View.GONE);
} else {
int replyIconResource = getReplyImageResource(defaultAction);
menuPrimaryActionView.setImageResource(replyIconResource);
String replyActionName = getReplyActionName(defaultAction);
TooltipCompat.setTooltipText(menuPrimaryActionView, replyActionName);
menuPrimaryActionView.setVisibility(View.VISIBLE);
}
}
@DrawableRes
private int getReplyImageResource(@NonNull ReplyAction replyAction) {
switch (replyAction) {
case REPLY: {
return R.drawable.ic_reply;
}
case REPLY_ALL: {
return R.drawable.ic_reply_all;
}
default: {
throw new IllegalStateException("Unknown reply action: " + replyAction);
}
}
}
@NonNull
private String getReplyActionName(@NonNull ReplyAction replyAction) {
Context context = getContext();
switch (replyAction) {
case REPLY: {
return context.getString(R.string.reply_action);
}
case REPLY_ALL: {
return context.getString(R.string.reply_all_action);
}
default: {
throw new IllegalStateException("Unknown reply action: " + replyAction);
}
}
}
private void setAdditionalReplyActions(PopupMenu popupMenu) {
List<ReplyAction> additionalActions = replyActions.getAdditionalActions();
if (!additionalActions.contains(ReplyAction.REPLY)) {
popupMenu.getMenu().removeItem(R.id.reply);
}
if (!additionalActions.contains(ReplyAction.REPLY_ALL)) {
popupMenu.getMenu().removeItem(R.id.reply_all);
}
}
public void setSubject(@NonNull String subject) {
subjectView.setText(subject);
}
public void hideCryptoStatus() {
mCryptoStatusIcon.setVisibility(View.GONE);
cryptoStatusIcon.setVisibility(View.GONE);
}
public void setCryptoStatusLoading() {
@ -319,38 +345,13 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
private void setCryptoDisplayStatus(MessageCryptoDisplayStatus displayStatus) {
int color = ThemeUtils.getStyledColor(getContext(), displayStatus.getColorAttr());
mCryptoStatusIcon.setEnabled(displayStatus.isEnabled());
mCryptoStatusIcon.setVisibility(View.VISIBLE);
mCryptoStatusIcon.setImageResource(displayStatus.getStatusIconRes());
mCryptoStatusIcon.setColorFilter(color);
cryptoStatusIcon.setEnabled(displayStatus.isEnabled());
cryptoStatusIcon.setVisibility(View.VISIBLE);
cryptoStatusIcon.setImageResource(displayStatus.getStatusIconRes());
cryptoStatusIcon.setColorFilter(color);
}
private void updateAddressField(TextView v, CharSequence text, View label) {
boolean hasText = !TextUtils.isEmpty(text);
v.setText(text);
v.setVisibility(hasText ? View.VISIBLE : View.GONE);
label.setVisibility(hasText ? View.VISIBLE : View.GONE);
}
/**
* Expand or collapse a TextView by removing or adding the 2 lines limitation
*/
private void expand(TextView v, boolean expand) {
if (expand) {
v.setMaxLines(Integer.MAX_VALUE);
v.setEllipsize(null);
} else {
v.setMaxLines(2);
v.setEllipsize(android.text.TextUtils.TruncateAt.END);
}
}
public void setOnCryptoClickListener(OnCryptoClickListener onCryptoClickListener) {
this.onCryptoClickListener = onCryptoClickListener;
}
public void setOnMenuItemClickListener(OnMenuItemClickListener onMenuItemClickListener) {
this.onMenuItemClickListener = onMenuItemClickListener;
public void setMessageHeaderClickListener(MessageHeaderClickListener messageHeaderClickListener) {
this.messageHeaderClickListener = messageHeaderClickListener;
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/ic_star" />
<item android:state_selected="false" android:drawable="@drawable/ic_star_border" />
</selector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

View file

@ -6,5 +6,5 @@
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM6,10L6,7L4,7v3L1,10v2h3v3h2v-3h3v-2L6,10zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
android:pathData="M13,8c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8s1.79,4 4,4S13,10.21 13,8zM15,10v2h3v3h2v-3h3v-2h-3V7h-2v3H15zM1,18v2h16v-2c0,-2.66 -5.33,-4 -8,-4S1,15.34 1,18z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
</vector>

View file

@ -2,7 +2,7 @@
<com.fsck.k9.ui.messageview.MessageTopView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/message_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -25,19 +25,30 @@
<!-- Header area -->
<include layout="@layout/message_view_header"/>
<Button
android:id="@+id/show_pictures"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
<FrameLayout
android:id="@+id/extra_header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginBottom="4dip"
android:layout_marginLeft="6dip"
android:layout_marginRight="6dip"
android:layout_marginTop="4dip"
android:text="@string/message_view_show_pictures_action"
android:background="?attr/extraMessageHeaderBackground"
android:paddingVertical="4dp"
android:visibility="gone"
tools:visibility="visible"/>
tools:visibility="visible">
<Button
android:id="@+id/show_pictures"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/message_view_show_remote_images_action"
app:icon="@drawable/ic_attachment_image" />
</FrameLayout>
<Space
android:id="@+id/line_of_death"
android:layout_width="match_parent"
android:layout_height="8dp" />
<com.fsck.k9.view.ToolableViewAnimator
android:id="@+id/message_layout_animator"
@ -45,7 +56,7 @@
android:layout_height="match_parent"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
custom:previewInitialChild="1">
app:previewInitialChild="1">
<View
android:layout_width="wrap_content"

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/message_details_progress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginVertical="32dp"
tools:visibility="gone" />
<TextView
android:id="@+id/message_details_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="32dp"
android:text="@string/message_details_loading_error"
android:visibility="gone"
tools:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/message_details_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:visibility="visible" />
</FrameLayout>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/keyline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="72dp" />
<ImageView
android:id="@+id/crypto_status_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/keyline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/status_lock_dots_3"
tools:tint="?attr/openpgp_green" />
<TextView
android:id="@+id/crypto_status_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceBody2"
app:layout_constraintBottom_toTopOf="@+id/crypto_status_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/keyline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/crypto_msg_title_encrypted_signed_e2e" />
<TextView
android:id="@+id/crypto_status_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/keyline"
app:layout_constraintTop_toBottomOf="@+id/crypto_status_title"
tools:text="@string/crypto_msg_encrypted_sign_verified" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:focusable="false"
android:gravity="end"
android:textAppearance="?attr/textAppearanceBody2"
tools:text="Wednesday, 11 January 2023, 10:31 pm" />

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginHorizontal="32dp"
android:layout_marginVertical="8dp"
android:background="?attr/messageDetailsDividerColor"
android:focusable="false" />

View file

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/participants_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/contact_picture"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_contact_picture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:attr/textColorPrimary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/email"
app:layout_constraintEnd_toStartOf="@+id/menu_add_contact"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/contact_picture"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginBottom="12dp"
tools:text="Alice" />
<TextView
android:id="@+id/email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="?android:attr/textColorSecondary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/menu_add_contact"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@+id/name"
app:layout_constraintTop_toBottomOf="@+id/name"
tools:text="alice@domain.example" />
<ImageView
android:id="@+id/menu_add_contact"
android:layout_width="48dp"
android:layout_height="72dp"
android:background="?attr/controlBackground"
android:clickable="true"
android:contentDescription="@string/action_add_to_contacts"
android:focusable="true"
android:paddingHorizontal="12dp"
app:layout_constraintEnd_toStartOf="@+id/menu_compose"
app:layout_constraintTop_toTopOf="@+id/menu_compose"
app:srcCompat="?attr/messageDetailsAddContactIcon" />
<ImageView
android:id="@+id/menu_compose"
android:layout_width="48dp"
android:layout_height="72dp"
android:background="?attr/controlBackground"
android:clickable="true"
android:contentDescription="@string/compose_action"
android:focusable="true"
android:paddingHorizontal="12dp"
app:layout_constraintEnd_toStartOf="@id/menu_overflow"
app:layout_constraintTop_toTopOf="@+id/menu_overflow"
app:srcCompat="@drawable/ic_envelope" />
<ImageView
android:id="@+id/menu_overflow"
android:layout_width="40dp"
android:layout_height="72dp"
android:background="?attr/controlBackground"
android:clickable="true"
android:contentDescription="@string/abc_action_menu_overflow_description"
android:focusable="true"
android:paddingStart="6dp"
android:paddingEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:srcCompat="@drawable/dots_vertical" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:focusable="false"
android:orientation="horizontal">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="?attr/textAppearanceOverline"
tools:text="To" />
<TextView
android:id="@+id/extra"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceOverline"
android:textColor="?android:attr/textColorSecondary"
tools:text="3" />
</LinearLayout>

View file

@ -1,297 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<com.fsck.k9.view.MessageHeader
xmlns:android="http://schemas.android.com/apk/res/android"
<com.fsck.k9.view.MessageHeader xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/header_container"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
android:layout_height="wrap_content">
<!-- Color chip -->
<View
<Space
android:id="@+id/margin_top"
android:layout_width="match_parent"
android:layout_height="16dp"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/chip"
android:layout_width="8dip"
android:layout_height="match_parent"
tools:background="#FF1976D2" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:clickable="false"
android:textColor="@android:color/white"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@+id/subject"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:chipBackgroundColor="#1976D2"
tools:text="Account name" />
<LinearLayout
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_margin_barrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
app:barrierDirection="bottom"
app:constraint_referenced_ids="chip,margin_top" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
>
<com.fsck.k9.ui.helper.BottomBaselineTextView
android:id="@+id/subject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/chip"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:maxLines="3"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/flagged"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_margin_barrier"
app:layout_constraintVertical_bias="0.0"
tools:text="Message subject" />
<TextView
android:id="@+id/subject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:maxLines="3"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.K9.MediumSmall"
tools:text="(no subject)"
/>
<ImageView
android:id="@+id/flagged"
android:layout_width="44dp"
android:layout_height="48dp"
android:background="?attr/controlBackground"
android:baseline="33dp"
android:paddingHorizontal="10dp"
android:src="@drawable/btn_select_star"
app:layout_constraintBaseline_toBaselineOf="@+id/subject"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/crypto_status_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="6dp"
app:srcCompat="@drawable/status_lock_disabled"
app:tint="?attr/openpgp_grey"
android:background="?selectableItemBackground"
android:visibility="gone"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<CheckBox
android:id="@+id/flagged"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="false"
android:checked="false"
style="@style/MessageStarStyle"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<com.fsck.k9.ui.ContactBadge
android:id="@+id/contact_badge"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
tools:src="@drawable/ic_contact_picture"/>
<!-- State icons -->
<LinearLayout
android:id="@+id/icon_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:layout_marginBottom="2dip"
android:layout_below="@+id/contact_badge"
android:layout_centerHorizontal="true"
android:orientation="vertical" >
<View
android:id="@+id/answered"
android:layout_width="32sp"
android:layout_height="32sp"
android:paddingStart="0dp"
android:paddingEnd="2dp"
android:background="?attr/messageListAnswered" />
<View
android:id="@+id/forwarded"
android:layout_width="22sp"
android:layout_height="22sp"
android:paddingStart="0dp"
android:paddingEnd="4dp"
android:background="?attr/messageListForwarded" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dip"
android:layout_marginStart="2dp" >
<!-- From -->
<TextView
android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/status_icon_strip"
android:layout_alignBottom="@+id/status_icon_strip"
android:paddingTop="0dp"
android:paddingStart="0dp"
android:paddingEnd="6dp"
android:singleLine="true"
android:ellipsize="end"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.K9.MediumSmall"
android:textStyle="bold"
android:text="@string/general_no_sender"
android:gravity="center_vertical"
/>
<!-- Sender -->
<TextView
android:id="@+id/sender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/status_icon_strip"
android:paddingTop="0dp"
android:layout_below="@+id/from"
android:ellipsize="end"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.K9.Small"
android:textStyle="bold"
android:visibility="gone"
android:gravity="center_vertical"
/>
<!-- To -->
<TextView
android:id="@+id/to_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/from"
android:layout_alignBaseline="@+id/to"
android:paddingTop="2dp"
android:paddingStart="0dp"
android:paddingEnd="4dp"
android:text="@string/message_to_label"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.K9.MediumSmall"
android:textStyle="bold" />
<TextView
android:id="@+id/to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/to_label"
android:layout_below="@+id/sender"
android:maxLines="2"
android:ellipsize="end"
android:paddingTop="2dp"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="@style/TextAppearance.K9.MediumSmall" />
<!-- CC -->
<TextView
android:id="@+id/cc_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/to_label"
android:layout_alignStart="@+id/to_label"
android:layout_alignBaseline="@+id/cc"
android:paddingTop="2dp"
android:paddingStart="0dp"
android:paddingEnd="4dp"
android:text="@string/message_view_cc_label"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:textAppearance="@style/TextAppearance.K9.MediumSmall" />
<TextView
android:id="@+id/cc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/cc_label"
android:layout_below="@+id/to"
android:maxLines="2"
android:ellipsize="end"
android:paddingTop="2dp"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="@style/TextAppearance.K9.MediumSmall" />
<!-- BCC -->
<TextView
android:id="@+id/bcc_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/cc_label"
android:layout_alignStart="@+id/cc_label"
android:layout_alignBaseline="@+id/bcc"
android:paddingTop="2dp"
android:paddingStart="0dp"
android:paddingEnd="4dp"
android:text="@string/message_view_bcc_label"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:textAppearance="@style/TextAppearance.K9.MediumSmall" />
<TextView
android:id="@+id/bcc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/bcc_label"
android:layout_below="@+id/cc"
android:maxLines="2"
android:ellipsize="end"
android:paddingTop="2dp"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="@style/TextAppearance.K9.MediumSmall" />
<!-- Date -->
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/bcc"
android:layout_alignParentEnd="true"
android:paddingTop="8dp"
android:singleLine="true"
android:ellipsize="none"
android:textAppearance="@style/TextAppearance.K9.Small"
android:textColor="?android:attr/textColorSecondary" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:id="@+id/status_icon_strip"
>
<ImageView
android:id="@+id/icon_single_message_options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="?iconActionSingleMessageOptions"
android:padding="8dp"
android:background="?selectableItemBackground"
/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_height="1dip"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/participants_container"
android:layout_width="match_parent"
android:layout_marginBottom="4dip"
android:background="@drawable/divider_horizontal_email" />
android:layout_height="wrap_content"
android:background="?attr/messageHeaderBackground"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/contact_picture"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_contact_picture" />
<TextView
android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:attr/textColorPrimary"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/date"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/contact_picture"
app:layout_constraintTop_toTopOf="parent"
tools:text="Sender name" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:ellipsize="none"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBaseline_toBaselineOf="@+id/from"
app:layout_constraintEnd_toStartOf="@+id/menu_primary_action"
app:layout_constraintStart_toEndOf="@id/from"
tools:text="Sep 19" />
<ImageView
android:id="@+id/crypto_status_icon"
android:layout_width="18sp"
android:layout_height="18sp"
android:layout_marginStart="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/recipients"
app:layout_constraintStart_toEndOf="@id/contact_picture"
app:srcCompat="@drawable/status_lock_disabled"
app:tint="?attr/openpgp_grey"
tools:visibility="visible" />
<com.fsck.k9.ui.messageview.RecipientNamesView
android:id="@+id/recipients"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/menu_primary_action"
app:layout_constraintStart_toEndOf="@id/crypto_status_icon"
app:layout_constraintTop_toBottomOf="@+id/from"
app:layout_constraintVertical_bias="0.0"
app:layout_goneMarginStart="16dp" />
<ImageView
android:id="@+id/menu_primary_action"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/controlBackground"
android:paddingHorizontal="12dp"
app:layout_constraintEnd_toStartOf="@id/menu_overflow"
app:layout_constraintTop_toTopOf="@+id/menu_overflow"
app:srcCompat="?iconActionSingleMessageOptions" />
<ImageView
android:id="@+id/menu_overflow"
android:layout_width="40dp"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="?attr/controlBackground"
android:clickable="true"
android:focusable="true"
android:paddingStart="6dp"
android:paddingEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/dots_vertical" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.fsck.k9.view.MessageHeader>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="LinearLayout"
tools:orientation="horizontal">
<TextView
android:id="@+id/recipient_names"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="?android:attr/textColorSecondary"
tools:text="to me" />
<TextView
android:id="@+id/recipient_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="?attr/colorAccent"
tools:text="+2" />
</merge>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/copy_email_address"
android:title="@string/action_copy_email_address" />
<item
android:id="@+id/copy_name_and_email_address"
android:title="@string/action_copy_name_and_email_address" />
</menu>

View file

@ -208,7 +208,6 @@
<string name="message_view_cc_label">نسخة كربونية :</string>
<string name="message_view_bcc_label">نسخة مخفية:</string>
<string name="message_view_status_attachment_not_saved">غير قادر على حفظ المُرفَق.</string>
<string name="message_view_show_pictures_action">اظهر الصور</string>
<string name="message_view_no_viewer">يتعذر إيجاب تطبيق لعرض <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">تنزيل الرسالة كاملةً</string>
<string name="message_view_sender_label">عبر %1$s</string>
@ -599,9 +598,6 @@
<string name="font_size_message_list_preview">معاينة</string>
<string name="font_size_message_view">رسائل</string>
<string name="font_size_message_view_sender">مُرسِل</string>
<string name="font_size_message_view_to">إلى</string>
<string name="font_size_message_view_cc">نسخة</string>
<string name="font_size_message_view_additional_headers">ترويسات إضافية</string>
<string name="font_size_message_view_subject">العنوان</string>
<string name="font_size_message_view_date">الوقت والتاريخ</string>
<string name="font_size_message_view_content">نصّ الرسالة</string>

View file

@ -208,7 +208,6 @@ K-9 Mail - шматфункцыянальны свабодны паштовы к
<string name="message_view_cc_label">Копія:</string>
<string name="message_view_bcc_label">Схаваная копія:</string>
<string name="message_view_status_attachment_not_saved">Немагчыма захаваць далучаныя файлы.</string>
<string name="message_view_show_pictures_action">Прагляд малюнкаў</string>
<string name="message_view_no_viewer">Няма праграмы для прагляду <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Спампаваць ліст цалкам</string>
<string name="message_view_sender_label">ад %1$s</string>
@ -662,10 +661,6 @@ K-9 Mail - шматфункцыянальны свабодны паштовы к
<string name="font_size_message_list_preview">Папярэдні прагляд</string>
<string name="font_size_message_view">Паведамленне</string>
<string name="font_size_message_view_sender">Адпраўнік</string>
<string name="font_size_message_view_to">Каму</string>
<string name="font_size_message_view_cc">Копія</string>
<string name="font_size_message_view_bcc">Схаваная копія</string>
<string name="font_size_message_view_additional_headers">Дадатковыя загалоўкі</string>
<string name="font_size_message_view_subject">Тэма</string>
<string name="font_size_message_view_date">Час і дата</string>
<string name="font_size_message_view_content">Цела ліста</string>

View file

@ -214,7 +214,6 @@ K-9 Mail е мощен, безплатен имейл клиент за Андр
<string name="message_view_cc_label">Копие:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Не може да запази прикачения файл.</string>
<string name="message_view_show_pictures_action">Покажи картинките</string>
<string name="message_view_no_viewer">Не може да намери програма за <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Свали цялото съобщение</string>
<string name="message_view_sender_label">през %1$s</string>
@ -659,10 +658,6 @@ K-9 Mail е мощен, безплатен имейл клиент за Андр
<string name="font_size_message_list_preview">Преглед</string>
<string name="font_size_message_view">Съобщения</string>
<string name="font_size_message_view_sender">Изпращач</string>
<string name="font_size_message_view_to">До</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Допълнителни header-и</string>
<string name="font_size_message_view_subject">Тема</string>
<string name="font_size_message_view_date">Дата и час</string>
<string name="font_size_message_view_content">Тяло на съобщението</string>

View file

@ -195,7 +195,6 @@ Danevellit beugoù, kenlabourit war keweriusterioù nevez ha savit goulennoù wa
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">N\'haller ket enrollañ ar stagadenn.</string>
<string name="message_view_show_pictures_action">Diskouez ar skeudennoù</string>
<string name="message_view_no_viewer">Nhaller ket kavout ur gwelerez evit <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Pellgargañ ar gemennadenn glok</string>
<string name="message_view_sender_label">dre %1$s</string>
@ -630,10 +629,6 @@ Danevellit beugoù, kenlabourit war keweriusterioù nevez ha savit goulennoù wa
<string name="font_size_message_list_preview">Alberz</string>
<string name="font_size_message_view">Kemennadennoù</string>
<string name="font_size_message_view_sender">Kaser</string>
<string name="font_size_message_view_to">Da</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Talbennoù ouzhpenn</string>
<string name="font_size_message_view_subject">Danvez</string>
<string name="font_size_message_view_date">Eur ha deiziad</string>
<string name="font_size_message_view_content">Korf ar gemennadenn</string>

View file

@ -224,7 +224,6 @@ Si us plau, envieu informes d\'errors, contribuïu-hi amb noves millores i feu p
<string name="message_view_cc_label">A/c:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">No s\'ha pogut desar l\'adjunt.</string>
<string name="message_view_show_pictures_action">Mostra les imatges</string>
<string name="message_view_no_viewer">No s\'ha pogut trobar un visualitzador per <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Baixa el missatge complet</string>
<string name="message_view_sender_label">mitjançant %1$s</string>
@ -724,10 +723,6 @@ Si us plau, envieu informes d\'errors, contribuïu-hi amb noves millores i feu p
<string name="font_size_message_list_preview">Vista prèvia</string>
<string name="font_size_message_view">Missatges</string>
<string name="font_size_message_view_sender">Remitent</string>
<string name="font_size_message_view_to">A</string>
<string name="font_size_message_view_cc">A/c</string>
<string name="font_size_message_view_bcc">C/o</string>
<string name="font_size_message_view_additional_headers">Capçaleres addicionals</string>
<string name="font_size_message_view_subject">Assumpte</string>
<string name="font_size_message_view_date">Data i hora</string>
<string name="font_size_message_view_content">Cos del missatge</string>

View file

@ -228,7 +228,6 @@ Hlášení o chyb, úpravy pro nové funkce a dotazy zadávejte prostřednictví
<string name="message_view_cc_label">Kopie:</string>
<string name="message_view_bcc_label">Skrytá kopie (Bcc):</string>
<string name="message_view_status_attachment_not_saved">Přílohu se nedaří uložit.</string>
<string name="message_view_show_pictures_action">Zobrazovat obrázky</string>
<string name="message_view_no_viewer">Nelze nalézt prohlížeč pro <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Stáhnout úplnou zprávu</string>
<string name="message_view_sender_label">přes %1$s</string>
@ -721,10 +720,6 @@ Hlášení o chyb, úpravy pro nové funkce a dotazy zadávejte prostřednictví
<string name="font_size_message_list_preview">Náhled</string>
<string name="font_size_message_view">Zpráva</string>
<string name="font_size_message_view_sender">Odesílatel</string>
<string name="font_size_message_view_to">Příjemce (Komu)</string>
<string name="font_size_message_view_cc">Příjemce (Kopie)</string>
<string name="font_size_message_view_bcc">Skrytá kopie</string>
<string name="font_size_message_view_additional_headers">Další záhlaví</string>
<string name="font_size_message_view_subject">Předmět</string>
<string name="font_size_message_view_date">Datum a čas</string>
<string name="font_size_message_view_content">Tělo</string>

View file

@ -229,7 +229,6 @@ Plîs rho wybod am unrhyw wallau, syniadau am nodweddion newydd, neu ofyn cwesti
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Methu â chadw\'r atodiad.</string>
<string name="message_view_show_pictures_action">Dangos delweddau</string>
<string name="message_view_no_viewer">Methwyd cael hyd i ap er mwyn darllen <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Lawrlwytho\'r neges gyfan</string>
<string name="message_view_sender_label">trwy %1$s</string>
@ -723,10 +722,6 @@ Plîs rho wybod am unrhyw wallau, syniadau am nodweddion newydd, neu ofyn cwesti
<string name="font_size_message_list_preview">Rhagolwg</string>
<string name="font_size_message_view">Negeseuon</string>
<string name="font_size_message_view_sender">Anfonwr</string>
<string name="font_size_message_view_to">At</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Penynnau ychwanegol</string>
<string name="font_size_message_view_subject">Pwnc</string>
<string name="font_size_message_view_date">Amser a dyddiad</string>
<string name="font_size_message_view_content">Corff neges</string>

View file

@ -218,7 +218,6 @@ Rapporter venligst fejl, forslag til nye funktioner eller stil spørgsmål på:
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Kunne ikke gemme vedhæftning.</string>
<string name="message_view_show_pictures_action">Vis billeder</string>
<string name="message_view_no_viewer">Kan ikke finde program som kan vise <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Hent hele meddelelsen</string>
<string name="message_view_sender_label">via %1$s</string>
@ -688,10 +687,6 @@ Rapporter venligst fejl, forslag til nye funktioner eller stil spørgsmål på:
<string name="font_size_message_list_preview">Forhåndsvisning</string>
<string name="font_size_message_view">Meddelelse</string>
<string name="font_size_message_view_sender">Afsender</string>
<string name="font_size_message_view_to">Modtagere (Til)</string>
<string name="font_size_message_view_cc">Modtagere (Cc)</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Yderligere headere</string>
<string name="font_size_message_view_subject">Emne</string>
<string name="font_size_message_view_date">Tid og dato</string>
<string name="font_size_message_view_content">Indhold</string>

View file

@ -223,7 +223,6 @@ Bitte senden Sie Fehlerberichte, Ideen für neue Funktionen und stellen Sie Frag
<string name="message_view_cc_label">CC:</string>
<string name="message_view_bcc_label">BCC:</string>
<string name="message_view_status_attachment_not_saved">Anhang konnte nicht gespeichert werden</string>
<string name="message_view_show_pictures_action">Bilder herunterladen</string>
<string name="message_view_no_viewer">Es wurde kein Anzeigeprogramm für <xliff:g id="mimetype">%s</xliff:g> gefunden.</string>
<string name="message_view_download_remainder">Gesamte Nachricht herunterladen</string>
<string name="message_view_sender_label">über %1$s</string>
@ -724,10 +723,6 @@ Bitte senden Sie Fehlerberichte, Ideen für neue Funktionen und stellen Sie Frag
<string name="font_size_message_list_preview">Vorschau</string>
<string name="font_size_message_view">Nachrichtenanzeige</string>
<string name="font_size_message_view_sender">Absender</string>
<string name="font_size_message_view_to">Empfänger (To)</string>
<string name="font_size_message_view_cc">Empfänger (CC)</string>
<string name="font_size_message_view_bcc">Blindkopie (BCC)</string>
<string name="font_size_message_view_additional_headers">Zusätzliche Header</string>
<string name="font_size_message_view_subject">Betreff</string>
<string name="font_size_message_view_date">Uhrzeit und Datum</string>
<string name="font_size_message_view_content">Nachrichtentext</string>

View file

@ -225,7 +225,6 @@
<string name="message_view_cc_label">Κοιν:</string>
<string name="message_view_bcc_label">Κρυφή κοιν:</string>
<string name="message_view_status_attachment_not_saved">Αδυναμία αποθήκευσης συνημμένου.</string>
<string name="message_view_show_pictures_action">Εμφάνιση εικόνων</string>
<string name="message_view_no_viewer">Δεν υπάρχει πρόγραμμα προβολής για <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Λήψη πλήρους μηνύματος</string>
<string name="message_view_sender_label">μέσω %1$s</string>
@ -719,10 +718,6 @@
<string name="font_size_message_list_preview">Προεπισκόπηση</string>
<string name="font_size_message_view">Μηνύματα</string>
<string name="font_size_message_view_sender">Αποστολέας</string>
<string name="font_size_message_view_to">Προς</string>
<string name="font_size_message_view_cc">Κοινοποίηση</string>
<string name="font_size_message_view_bcc">Κρυφή Κοιν:</string>
<string name="font_size_message_view_additional_headers">Επιπλέον επικεφαλίδες</string>
<string name="font_size_message_view_subject">Θέμα</string>
<string name="font_size_message_view_date">Ώρα και Ημερομηνία</string>
<string name="font_size_message_view_content">Σώμα μηνύματος</string>

View file

@ -212,7 +212,6 @@ Bonvolu raporti erarojn, kontribui novajn eblojn kaj peti pri novaj funkcioj per
<string name="message_view_cc_label">Kopio:</string>
<string name="message_view_bcc_label">Kaŝkopio:</string>
<string name="message_view_status_attachment_not_saved">Ne povis konservi kunsendaĵon.</string>
<string name="message_view_show_pictures_action">Montri bildojn</string>
<string name="message_view_no_viewer">Ne povas trovi vidigilon por <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Elŝuti tutan mesaĝon</string>
<string name="message_view_sender_label">per %1$s</string>
@ -677,10 +676,6 @@ Bonvolu raporti erarojn, kontribui novajn eblojn kaj peti pri novaj funkcioj per
<string name="font_size_message_list_preview">Antaŭmontro</string>
<string name="font_size_message_view">Mesaĝoj</string>
<string name="font_size_message_view_sender">Sendinto</string>
<string name="font_size_message_view_to">Ricevonto</string>
<string name="font_size_message_view_cc">Ricevonto de kopio</string>
<string name="font_size_message_view_bcc">Kaŝkopio</string>
<string name="font_size_message_view_additional_headers">Aldonaj kapoj</string>
<string name="font_size_message_view_subject">Temo</string>
<string name="font_size_message_view_date">Tempo kaj dato</string>
<string name="font_size_message_view_content">Mesaĝa enhavo</string>

View file

@ -224,7 +224,6 @@ Puedes informar de fallos, contribuir con su desarrollo y hacer preguntas en <a
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Cco:</string>
<string name="message_view_status_attachment_not_saved">No se puede guardar el adjunto.</string>
<string name="message_view_show_pictures_action">Mostrar imágenes</string>
<string name="message_view_no_viewer">Imposible encontrar visor de <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Descargar mensaje completo</string>
<string name="message_view_sender_label">vía %1$s</string>
@ -724,10 +723,6 @@ Puedes informar de fallos, contribuir con su desarrollo y hacer preguntas en <a
<string name="font_size_message_list_preview">Previsualizar</string>
<string name="font_size_message_view">Mensajes</string>
<string name="font_size_message_view_sender">Remitente</string>
<string name="font_size_message_view_to">Para</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Cco</string>
<string name="font_size_message_view_additional_headers">Encabezados adicionales</string>
<string name="font_size_message_view_subject">Asunto</string>
<string name="font_size_message_view_date">Hora y fecha</string>
<string name="font_size_message_view_content">Cuerpo de mensaje</string>

View file

@ -225,7 +225,6 @@ Veateated saad saata, kaastööd teha ning küsida teavet järgmisel lehel:
<string name="message_view_cc_label">Koopia:</string>
<string name="message_view_bcc_label">Pimekoopia:</string>
<string name="message_view_status_attachment_not_saved">Manuse salvestamine ebaõnnestus</string>
<string name="message_view_show_pictures_action">Näita pilte</string>
<string name="message_view_no_viewer">Ei leia <xliff:g id="mimetype">%s</xliff:g> kuvamiseks programmi.</string>
<string name="message_view_download_remainder">Laadi kogu kiri alla</string>
<string name="message_view_sender_label">%1$s kaudu</string>
@ -722,10 +721,6 @@ Veateated saad saata, kaastööd teha ning küsida teavet järgmisel lehel:
<string name="font_size_message_list_preview">Eelvaade</string>
<string name="font_size_message_view">Kirjad</string>
<string name="font_size_message_view_sender">Saatja</string>
<string name="font_size_message_view_to">Kellele</string>
<string name="font_size_message_view_cc">Koopia</string>
<string name="font_size_message_view_bcc">Pimekoopia</string>
<string name="font_size_message_view_additional_headers">Täiendavad pealkirjad</string>
<string name="font_size_message_view_subject">Teema</string>
<string name="font_size_message_view_date">Kellaaeg ja kuupäev</string>
<string name="font_size_message_view_content">Kirja põhiosa</string>

View file

@ -223,7 +223,6 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Ezin izan da eranskina gorde.</string>
<string name="message_view_show_pictures_action">Erakutsi irudiak</string>
<string name="message_view_no_viewer">Ezin da <xliff:g id="mimetype">%s</xliff:g>-(r)entzako ikustailerik aurkitu.</string>
<string name="message_view_download_remainder">Deskargatu mezu osoa</string>
<string name="message_view_sender_label">%1$s-(r)en bidez</string>
@ -723,10 +722,6 @@ Mesedez akatsen berri emateko, ezaugarri berriak gehitzeko eta galderak egiteko
<string name="font_size_message_list_preview">Aurreikusi</string>
<string name="font_size_message_view">Mezuak</string>
<string name="font_size_message_view_sender">Bidaltzailea</string>
<string name="font_size_message_view_to">Nori</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Goiburu osagarriak</string>
<string name="font_size_message_view_subject">Gaia</string>
<string name="font_size_message_view_date">Ordua eta data</string>
<string name="font_size_message_view_content">Mezuaren gorputza</string>

View file

@ -224,7 +224,6 @@
<string name="message_view_cc_label">رونوشت به:</string>
<string name="message_view_bcc_label">مخفیانه به:</string>
<string name="message_view_status_attachment_not_saved">نمی‌توان پیوست را ذخیره کرد.</string>
<string name="message_view_show_pictures_action">نمایش تصاویر</string>
<string name="message_view_no_viewer">نمایش‌دهنده‌ای برای <xliff:g id="mimetype">%s</xliff:g> پیدا نشد.</string>
<string name="message_view_download_remainder">دانلود پیام کامل</string>
<string name="message_view_sender_label">از طریق %1$s</string>
@ -717,10 +716,6 @@
<string name="font_size_message_list_preview">پیش‌نمایش</string>
<string name="font_size_message_view">پیام‌ها</string>
<string name="font_size_message_view_sender">فرستنده</string>
<string name="font_size_message_view_to">به</string>
<string name="font_size_message_view_cc">رونوشت به</string>
<string name="font_size_message_view_bcc">مخفیانه به</string>
<string name="font_size_message_view_additional_headers">سرایندهای اضافی</string>
<string name="font_size_message_view_subject">موضوع</string>
<string name="font_size_message_view_date">ساعت و تاریخ</string>
<string name="font_size_message_view_content">متن پیام</string>

View file

@ -223,7 +223,6 @@ Ilmoita virheistä, ota osaa sovelluskehitykseen ja esitä kysymyksiä osoittees
<string name="message_view_cc_label">Kopio:</string>
<string name="message_view_bcc_label">Piilokopio:</string>
<string name="message_view_status_attachment_not_saved">Liitteen tallentaminen epäonnistui.</string>
<string name="message_view_show_pictures_action">Näytä kuvat</string>
<string name="message_view_no_viewer">Tiedostotyypille <xliff:g id="mimetype">%s</xliff:g> ei löydy katseluohjelmaa.</string>
<string name="message_view_download_remainder">Lataa koko viesti</string>
<string name="message_view_sender_label">%1$s kautta</string>
@ -723,10 +722,6 @@ Ilmoita virheistä, ota osaa sovelluskehitykseen ja esitä kysymyksiä osoittees
<string name="font_size_message_list_preview">Esikatselu</string>
<string name="font_size_message_view">Viestinäkymä</string>
<string name="font_size_message_view_sender">Lähettäjä</string>
<string name="font_size_message_view_to">Vastaanottaja</string>
<string name="font_size_message_view_cc">Kopio</string>
<string name="font_size_message_view_bcc">Piilokopio</string>
<string name="font_size_message_view_additional_headers">Tarkemmat otsikkotiedot</string>
<string name="font_size_message_view_subject">Viestin aihe</string>
<string name="font_size_message_view_date">Aika ja päivämäärä</string>
<string name="font_size_message_view_content">Viestin sisältö</string>

View file

@ -227,7 +227,6 @@ Rapportez les bogues, recommandez de nouvelles fonctions et posez vos questions
<string name="message_view_cc_label">Cc :</string>
<string name="message_view_bcc_label">Cci :</string>
<string name="message_view_status_attachment_not_saved">Impossible denregistrer le fichier joint.</string>
<string name="message_view_show_pictures_action">Afficher les images</string>
<string name="message_view_no_viewer">Impossible de trouver un visualiseur pour <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Télécharger le courriel complet</string>
<string name="message_view_sender_label">par %1$s</string>
@ -727,10 +726,6 @@ Rapportez les bogues, recommandez de nouvelles fonctions et posez vos questions
<string name="font_size_message_list_preview">Aperçu</string>
<string name="font_size_message_view">Courriels</string>
<string name="font_size_message_view_sender">Expéditeur</string>
<string name="font_size_message_view_to">À</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Cci</string>
<string name="font_size_message_view_additional_headers">En-têtes supplémentaires</string>
<string name="font_size_message_view_subject">Objet</string>
<string name="font_size_message_view_date">Horodatage</string>
<string name="font_size_message_view_content">Corps du courriel</string>

View file

@ -219,7 +219,6 @@ Graach flaterrapporten stjoere, bydragen foar nije funksjes en fragen stelle op
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Kin bylage net bewarje.</string>
<string name="message_view_show_pictures_action">Ofbyldingen toane</string>
<string name="message_view_no_viewer">Gjin viewer te finen foar <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Folslein berjocht downloade</string>
<string name="message_view_sender_label">fia %1$s</string>
@ -719,10 +718,6 @@ Graach flaterrapporten stjoere, bydragen foar nije funksjes en fragen stelle op
<string name="font_size_message_list_preview">Foarbyld</string>
<string name="font_size_message_view">Berjochten</string>
<string name="font_size_message_view_sender">Ofstjoerder</string>
<string name="font_size_message_view_to">Oan</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Ekstra koppen</string>
<string name="font_size_message_view_subject">Underwerp</string>
<string name="font_size_message_view_date">Tiid en datum</string>
<string name="font_size_message_view_content">Berichtynhâld</string>

View file

@ -180,7 +180,6 @@
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Cha b urrainn dhuinn an ceanglachan a shàbhaladh.</string>
<string name="message_view_show_pictures_action">Seall na dealbhan</string>
<string name="message_view_no_viewer">Chan urrainn dhuinn aplacaid a lorg a sheallas <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Luchdaich a-nuas an teachdaireachd shlàn</string>
<string name="message_view_sender_label">slighe %1$s</string>
@ -625,10 +624,6 @@
<string name="font_size_message_list_preview">Ro-shealladh</string>
<string name="font_size_message_view">Teachdaireachdan</string>
<string name="font_size_message_view_sender">Seòladair</string>
<string name="font_size_message_view_to">Gu</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Bannan-cinn a bharrachd</string>
<string name="font_size_message_view_subject">Cuspair</string>
<string name="font_size_message_view_date">Ceann-là s àm</string>
<string name="font_size_message_view_content">Bodhaig na teachdaireachd</string>

View file

@ -129,7 +129,6 @@
<string name="message_view_from_format">Desde: <xliff:g id="name">%1$s</xliff:g> &lt;<xliff:g id="email">%2$s</xliff:g>&gt;</string>
<string name="message_to_label">Para:</string>
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_show_pictures_action">Mostrar imaxes</string>
<string name="message_view_no_viewer">Non é posible atopar un visor para <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Descargar mensaxe completa</string>
<string name="from_same_sender">Máis deste remitente</string>
@ -530,10 +529,6 @@
<string name="font_size_message_list_preview">Previsualización</string>
<string name="font_size_message_view">Mensaxes</string>
<string name="font_size_message_view_sender">Remitente</string>
<string name="font_size_message_view_to">Para</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Cabeceiras adicionais</string>
<string name="font_size_message_view_subject">Asunto</string>
<string name="font_size_message_view_date">Hora e data</string>
<string name="font_size_message_view_content">Corpo da mensaxe</string>

View file

@ -218,7 +218,6 @@ Por favor envíen informes de fallos, contribúa con novas características e co
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Incapaz de gardar o anexo.</string>
<string name="message_view_show_pictures_action">Amosar imaxe</string>
<string name="message_view_no_viewer">Imposible atopar visor para <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Descargar mensaxe completa</string>
<string name="message_view_sender_label">vía %1$s</string>
@ -683,10 +682,6 @@ Por favor envíen informes de fallos, contribúa con novas características e co
<string name="font_size_message_list_preview">Vista previa</string>
<string name="font_size_message_view">mensaxes</string>
<string name="font_size_message_view_sender">Remitente</string>
<string name="font_size_message_view_to">Para</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">CCo</string>
<string name="font_size_message_view_additional_headers">Encabezados adicionáis</string>
<string name="font_size_message_view_subject">Asunto</string>
<string name="font_size_message_view_date">Data e Hora</string>
<string name="font_size_message_view_content">Corpo da mensaxe</string>

View file

@ -168,7 +168,6 @@
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Nije moguće spremiti privitak.</string>
<string name="message_view_show_pictures_action">Prikaži slike</string>
<string name="message_view_no_viewer">Ne mogu pronaći preglednik za <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Preuzmi cijelu poruku</string>
<string name="message_view_sender_label">putem %1$s</string>
@ -621,10 +620,6 @@
<string name="font_size_message_list_preview">Pedpregled</string>
<string name="font_size_message_view">Poruke</string>
<string name="font_size_message_view_sender">Pošiljatelj</string>
<string name="font_size_message_view_to">Za</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Dodatna zaglavlja</string>
<string name="font_size_message_view_subject">Subjekt</string>
<string name="font_size_message_view_date">Vrijeme i datum</string>
<string name="font_size_message_view_content">Tijelo poruke</string>

View file

@ -222,7 +222,6 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd
<string name="message_view_cc_label">Másolat:</string>
<string name="message_view_bcc_label">Titkos másolat:</string>
<string name="message_view_status_attachment_not_saved">Nem lehet elmenteni a mellékletet.</string>
<string name="message_view_show_pictures_action">Fényképek megjelenítése</string>
<string name="message_view_no_viewer">Nem sikerült megjelenítőt találni ehhez: <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Teljes üzenet letöltése</string>
<string name="message_view_sender_label">ezen keresztül: %1$s</string>
@ -715,10 +714,6 @@ Hibajelentések beküldésével közreműködhet az új funkciókban, és kérd
<string name="font_size_message_list_preview">Előnézet</string>
<string name="font_size_message_view">Üzenetek</string>
<string name="font_size_message_view_sender">Feladó</string>
<string name="font_size_message_view_to">Címzett</string>
<string name="font_size_message_view_cc">Másolat</string>
<string name="font_size_message_view_bcc">Titkos másolat</string>
<string name="font_size_message_view_additional_headers">További fejlécek</string>
<string name="font_size_message_view_subject">Tárgy</string>
<string name="font_size_message_view_date">Idő és dátum</string>
<string name="font_size_message_view_content">Üzenet törzse</string>

View file

@ -203,7 +203,6 @@ Kirimkan laporan bug, kontribusikan fitur baru dan ajukan pertanyaan di
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Tidak dapat menyimpan lampiran.</string>
<string name="message_view_show_pictures_action">Tampilkan gambar</string>
<string name="message_view_no_viewer">Tidak bisa menemukan penampil untuk <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Unduh seluruh pesan</string>
<string name="message_view_sender_label">melalui %1$s</string>
@ -657,10 +656,6 @@ Kirimkan laporan bug, kontribusikan fitur baru dan ajukan pertanyaan di
<string name="font_size_message_list_preview">Pratinjau</string>
<string name="font_size_message_view">Pesan</string>
<string name="font_size_message_view_sender">Pengirim</string>
<string name="font_size_message_view_to">Kepada</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Header tambahan</string>
<string name="font_size_message_view_subject">Subyek</string>
<string name="font_size_message_view_date">Tanggal dan waktu</string>
<string name="font_size_message_view_content">Isi pesan</string>

View file

@ -224,7 +224,6 @@ Sendu inn villuskýrslur, leggðu fram nýja eiginleika og spurðu spurninga á
<string name="message_view_cc_label">Afrit:</string>
<string name="message_view_bcc_label">Falið afrit:</string>
<string name="message_view_status_attachment_not_saved">Tókst ekki að vista viðhengi</string>
<string name="message_view_show_pictures_action">Sýna myndir</string>
<string name="message_view_no_viewer">Gat ekki fundið neitt skoðunarforrit fyrir <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Niðurhala heil skilaboð</string>
<string name="message_view_sender_label">í gegnum %1$s</string>
@ -724,10 +723,6 @@ Sendu inn villuskýrslur, leggðu fram nýja eiginleika og spurðu spurninga á
<string name="font_size_message_list_preview">Forskoðun</string>
<string name="font_size_message_view">Skilaboð</string>
<string name="font_size_message_view_sender">Sendandi</string>
<string name="font_size_message_view_to">Til</string>
<string name="font_size_message_view_cc">Afrit</string>
<string name="font_size_message_view_bcc">Falið afrit</string>
<string name="font_size_message_view_additional_headers">Viðbótarhausar</string>
<string name="font_size_message_view_subject">Viðfangsefni</string>
<string name="font_size_message_view_date">Tími og dagsetning</string>
<string name="font_size_message_view_content">Efni skilaboða</string>

View file

@ -227,7 +227,6 @@ Invia segnalazioni di bug, contribuisci con nuove funzionalità e poni domande s
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Ccn:</string>
<string name="message_view_status_attachment_not_saved">Impossibile salvare l\'allegato.</string>
<string name="message_view_show_pictures_action">Mostra immagini</string>
<string name="message_view_no_viewer">Impossibile trovare un visualizzatore per <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Scarica messaggio completo</string>
<string name="message_view_sender_label">tramite %1$s</string>
@ -720,10 +719,6 @@ Invia segnalazioni di bug, contribuisci con nuove funzionalità e poni domande s
<string name="font_size_message_list_preview">Anteprima</string>
<string name="font_size_message_view">Messaggi</string>
<string name="font_size_message_view_sender">Mittente</string>
<string name="font_size_message_view_to">A</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Ccn</string>
<string name="font_size_message_view_additional_headers">Intestazioni aggiuntive</string>
<string name="font_size_message_view_subject">Oggetto</string>
<string name="font_size_message_view_date">Ora e data</string>
<string name="font_size_message_view_content">Corpo del messaggio</string>

View file

@ -182,7 +182,6 @@
<string name="message_view_from_format">מ: <xliff:g id="name">%1$s</xliff:g> &lt;<xliff:g id="email">%2$s</xliff:g>&gt;</string>
<string name="message_to_label">ל:</string>
<string name="message_view_cc_label">עותק:</string>
<string name="message_view_show_pictures_action">הצג תמונות</string>
<string name="message_view_no_viewer">לא ניתן למצוא את מציג בשביל <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">הורד את ההודעה המלאה</string>
<string name="message_view_sender_label">באמצעות %1$s</string>
@ -532,10 +531,6 @@
<string name="font_size_message_list_preview">תצוגה מקדימה</string>
<string name="font_size_message_view">הודעות</string>
<string name="font_size_message_view_sender">שולח</string>
<string name="font_size_message_view_to">ל</string>
<string name="font_size_message_view_cc">עותק</string>
<string name="font_size_message_view_bcc">עותק מוסתר</string>
<string name="font_size_message_view_additional_headers">כותרות נוספות</string>
<string name="font_size_message_view_subject">כותרת</string>
<string name="font_size_message_view_date">שעה ותאריך</string>
<string name="font_size_message_view_content">גוף ההודעה</string>

View file

@ -222,7 +222,6 @@ K-9 は大多数のメールクライアントと同様に、ほとんどのフ
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">添付ファイルを保存できません。</string>
<string name="message_view_show_pictures_action">画像表示</string>
<string name="message_view_no_viewer"><xliff:g id="mimetype">%s</xliff:g>のビューワーが見つかりません</string>
<string name="message_view_download_remainder">すべてダウンロード</string>
<string name="message_view_sender_label">%1$s 経由</string>
@ -722,10 +721,6 @@ K-9 は大多数のメールクライアントと同様に、ほとんどのフ
<string name="font_size_message_list_preview">プレビュー</string>
<string name="font_size_message_view">メッセージ表示</string>
<string name="font_size_message_view_sender">送信者</string>
<string name="font_size_message_view_to">To</string>
<string name="font_size_message_view_cc">CC</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">追加ヘッダ</string>
<string name="font_size_message_view_subject">件名</string>
<string name="font_size_message_view_date">日時</string>
<string name="font_size_message_view_content">本文</string>

View file

@ -162,7 +162,6 @@
<string name="message_to_label">수신자:</string>
<string name="message_view_cc_label">참조:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_show_pictures_action">그림 보기</string>
<string name="message_view_no_viewer"><xliff:g id="mimetype">%s</xliff:g>에 대한 뷰어를 찾을 수 없습니다.</string>
<string name="message_view_download_remainder">메시지의 남은 부분 다운로드</string>
<string name="message_view_sender_label">%1$s을 통하여</string>
@ -576,10 +575,6 @@
<string name="font_size_message_list_preview">미리 보기</string>
<string name="font_size_message_view">메시지</string>
<string name="font_size_message_view_sender">보낸이</string>
<string name="font_size_message_view_to">받는이</string>
<string name="font_size_message_view_cc">참조</string>
<string name="font_size_message_view_bcc">숨은 참조</string>
<string name="font_size_message_view_additional_headers">추가 헤더 정보</string>
<string name="font_size_message_view_subject">제목</string>
<string name="font_size_message_view_date">시간 및 날짜</string>
<string name="font_size_message_view_content">내용</string>

View file

@ -226,7 +226,6 @@ Pateikite pranešimus apie klaidas, prisidėkite prie naujų funkcijų kūrimo i
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Nepavyko išsaugoti priedo.</string>
<string name="message_view_show_pictures_action">Rodyti paveikslėlius</string>
<string name="message_view_no_viewer">Nepavyko rasto <xliff:g id="mimetype">%s</xliff:g> peržiūros programos.</string>
<string name="message_view_download_remainder">Parsiųsti visą laišką</string>
<string name="message_view_sender_label">per %1$s</string>
@ -716,10 +715,6 @@ Pateikite pranešimus apie klaidas, prisidėkite prie naujų funkcijų kūrimo i
<string name="font_size_message_list_preview">Peržiūra</string>
<string name="font_size_message_view">Laiškai</string>
<string name="font_size_message_view_sender">Siuntėjas</string>
<string name="font_size_message_view_to">Kam</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Slapta kopija</string>
<string name="font_size_message_view_additional_headers">Papildomos antraštės</string>
<string name="font_size_message_view_subject">Tema</string>
<string name="font_size_message_view_date">Laikas ir data</string>
<string name="font_size_message_view_content">Pagrindinė laiško dalis</string>

View file

@ -226,7 +226,6 @@ pat <xliff:g id="messages_to_load">%d</xliff:g> vairāk</string>
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Neredzamais:</string>
<string name="message_view_status_attachment_not_saved">Nevar saglabāt pielikumu.</string>
<string name="message_view_show_pictures_action">Rādīt bildes</string>
<string name="message_view_no_viewer">Nevar atrast programmu, lai atvērtu <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">lejupielādēt pilnu vēstuli</string>
<string name="message_view_sender_label">caur %1$s</string>
@ -726,10 +725,6 @@ pat <xliff:g id="messages_to_load">%d</xliff:g> vairāk</string>
<string name="font_size_message_list_preview">Priekšskatījums</string>
<string name="font_size_message_view">Vēstules</string>
<string name="font_size_message_view_sender">Sūtītājs</string>
<string name="font_size_message_view_to">Kam</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Neredzamais</string>
<string name="font_size_message_view_additional_headers">Izvērsta vēstules papildinformācija</string>
<string name="font_size_message_view_subject">Temats</string>
<string name="font_size_message_view_date">Laiks un datums</string>
<string name="font_size_message_view_content">Vēstules pamatteksts</string>

View file

@ -218,7 +218,6 @@
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">അറ്റാച്ചുമെന്റ് സേവ് ചെയ്യാനായില്ല.</string>
<string name="message_view_show_pictures_action">ചിത്രങ്ങൾ കാണിക്കുക</string>
<string name="message_view_no_viewer"><xliff:g id="mimetype">%s</xliff:g> എന്നതിനുള്ള വീക്ഷണസഹായസാമഗ്രി കണ്ടെത്താനായില്ല.</string>
<string name="message_view_download_remainder">പൂർണ്ണ സന്ദേശം ഡൗൺ‌ലോഡു ചെയ്യുക</string>
<string name="message_view_sender_label">%1$s വഴി</string>
@ -685,10 +684,6 @@
<string name="font_size_message_list_preview">പ്രിവ്യൂ</string>
<string name="font_size_message_view">സന്ദേശങ്ങൾ</string>
<string name="font_size_message_view_sender">അയച്ചയാൾ</string>
<string name="font_size_message_view_to">To</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">അധിക തലക്കെട്ടുകൾ</string>
<string name="font_size_message_view_subject">വിഷയം</string>
<string name="font_size_message_view_date">സമയവും തീയതിയും</string>
<string name="font_size_message_view_content">സന്ദേശ ബോഡി</string>

View file

@ -204,7 +204,6 @@ til <xliff:g id="messages_to_load">%d</xliff:g> flere</string>
<string name="message_view_cc_label">Kopi:</string>
<string name="message_view_bcc_label">Blindkopi:</string>
<string name="message_view_status_attachment_not_saved">Klarte ikke lagre vedlegg</string>
<string name="message_view_show_pictures_action">Vis bilder</string>
<string name="message_view_no_viewer">Finner ikke visningsprogram for <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Last ned komplett melding</string>
<string name="message_view_sender_label">via %1$s</string>
@ -656,10 +655,6 @@ til <xliff:g id="messages_to_load">%d</xliff:g> flere</string>
<string name="font_size_message_list_preview">Forhåndsvisning</string>
<string name="font_size_message_view">Meldinger</string>
<string name="font_size_message_view_sender">Avsender</string>
<string name="font_size_message_view_to">Til</string>
<string name="font_size_message_view_cc">Kopi</string>
<string name="font_size_message_view_bcc">Blindkopi</string>
<string name="font_size_message_view_additional_headers">Flere meldingshoder</string>
<string name="font_size_message_view_subject">Emne</string>
<string name="font_size_message_view_date">Tid og dato</string>
<string name="font_size_message_view_content">Meldingstekst</string>

View file

@ -219,7 +219,6 @@ Graag foutrapporten sturen, bijdragen voor nieuwe functies en vragen stellen op
<string name="message_view_cc_label">CC:</string>
<string name="message_view_bcc_label">BCC:</string>
<string name="message_view_status_attachment_not_saved">Kan bijlage niet opslaan.</string>
<string name="message_view_show_pictures_action">Afbeeldingen tonen</string>
<string name="message_view_no_viewer">Niet in staat viewer te vinden voor <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Compleet bericht downloaden</string>
<string name="message_view_sender_label">via %1$s</string>
@ -719,10 +718,6 @@ Graag foutrapporten sturen, bijdragen voor nieuwe functies en vragen stellen op
<string name="font_size_message_list_preview">Voorbeeld</string>
<string name="font_size_message_view">Berichten</string>
<string name="font_size_message_view_sender">Afzender</string>
<string name="font_size_message_view_to">Bericht ontvanger (Aan)</string>
<string name="font_size_message_view_cc">Bericht ontvanger (CC)</string>
<string name="font_size_message_view_bcc">Bericht ontvanger (BCC)</string>
<string name="font_size_message_view_additional_headers">Extra koppen</string>
<string name="font_size_message_view_subject">Onderwerp</string>
<string name="font_size_message_view_date">Tijd en datum</string>
<string name="font_size_message_view_content">Berichtinhoud</string>

View file

@ -228,7 +228,6 @@ Wysłane z urządzenia Android za pomocą K-9 Mail. Proszę wybaczyć moją zwi
<string name="message_view_cc_label">DW:</string>
<string name="message_view_bcc_label">UDW:</string>
<string name="message_view_status_attachment_not_saved">Nie można zapisać załącznika.</string>
<string name="message_view_show_pictures_action">Wyświetl grafikę</string>
<string name="message_view_no_viewer">Nie moge znaleźć aplikacji do wyświetlenia pliku <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Pobierz całą wiadomość</string>
<string name="message_view_sender_label">przez %1$s</string>
@ -728,10 +727,6 @@ Wysłane z urządzenia Android za pomocą K-9 Mail. Proszę wybaczyć moją zwi
<string name="font_size_message_list_preview">Podgląd</string>
<string name="font_size_message_view">Wiadomości</string>
<string name="font_size_message_view_sender">Nadawca</string>
<string name="font_size_message_view_to">Adresat (Do:)</string>
<string name="font_size_message_view_cc">Odbiorca kopii (DW:)</string>
<string name="font_size_message_view_bcc">UDW</string>
<string name="font_size_message_view_additional_headers">Dodatkowe nagłówki</string>
<string name="font_size_message_view_subject">Temat</string>
<string name="font_size_message_view_date">Czas i data</string>
<string name="font_size_message_view_content">Treść wiadomości</string>

View file

@ -225,7 +225,6 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Cco:</string>
<string name="message_view_status_attachment_not_saved">Não foi possível salvar o anexo.</string>
<string name="message_view_show_pictures_action">Exibir imagens</string>
<string name="message_view_no_viewer">Não foi possível encontrar um visualizador para <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Baixar a mensagem completa</string>
<string name="message_view_sender_label">via %1$s</string>
@ -725,10 +724,6 @@ Por favor encaminhe relatórios de bugs, contribua com novos recursos e tire dú
<string name="font_size_message_list_preview">Visualização</string>
<string name="font_size_message_view">Mensagens</string>
<string name="font_size_message_view_sender">Remetente</string>
<string name="font_size_message_view_to">Para</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Cco</string>
<string name="font_size_message_view_additional_headers">Cabeçalhos adicionais</string>
<string name="font_size_message_view_subject">Assunto</string>
<string name="font_size_message_view_date">Data e hora</string>
<string name="font_size_message_view_content">Corpo da mensagem</string>

View file

@ -227,7 +227,6 @@ Por favor envie relatórios de falhas, contribua com novas funcionalidades e col
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Não foi possível guardar o anexo</string>
<string name="message_view_show_pictures_action">Mostrar imagens</string>
<string name="message_view_no_viewer">Incapaz de encontrar visualizador para <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Transferir mensagem completa</string>
<string name="message_view_sender_label">via %1$s</string>
@ -722,10 +721,6 @@ Por favor envie relatórios de falhas, contribua com novas funcionalidades e col
<string name="font_size_message_list_preview">Pré-visualização</string>
<string name="font_size_message_view">Mensagens</string>
<string name="font_size_message_view_sender">Remetente</string>
<string name="font_size_message_view_to">Para</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Cabeçalhos adicionais</string>
<string name="font_size_message_view_subject">Assunto</string>
<string name="font_size_message_view_date">Data e hora</string>
<string name="font_size_message_view_content">Corpo da mensagem</string>

View file

@ -226,7 +226,6 @@ cel mult încă <xliff:g id="messages_to_load">%d</xliff:g></string>
<string name="message_view_cc_label">Cc:</string>
<string name="message_view_bcc_label">Bcc:</string>
<string name="message_view_status_attachment_not_saved">Nu se poate salva atașamentul.</string>
<string name="message_view_show_pictures_action">Arată pozele</string>
<string name="message_view_no_viewer">Nu se poate găsi vizualizator pentru <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Descarcă mesajul complet</string>
<string name="message_view_sender_label">via %1$s</string>
@ -728,10 +727,6 @@ Uneori datorită faptului că cineva încearcă să te atace pe tine sau serveru
<string name="font_size_message_list_preview">Previzualizare</string>
<string name="font_size_message_view">Mesaje</string>
<string name="font_size_message_view_sender">Expeditor</string>
<string name="font_size_message_view_to">Către</string>
<string name="font_size_message_view_cc">Cc</string>
<string name="font_size_message_view_bcc">Bcc</string>
<string name="font_size_message_view_additional_headers">Antete adiționale</string>
<string name="font_size_message_view_subject">Subiect</string>
<string name="font_size_message_view_date">Timp și dată</string>
<string name="font_size_message_view_content">Conținut mesaj</string>

View file

@ -227,7 +227,6 @@ K-9 Mail — почтовый клиент для Android.
<string name="message_view_cc_label">Копия:</string>
<string name="message_view_bcc_label">Скрытая копия:</string>
<string name="message_view_status_attachment_not_saved">Не получается сохранить вложение.</string>
<string name="message_view_show_pictures_action">Изображения</string>
<string name="message_view_no_viewer">Отсутствует просмотрщик <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Загрузить полностью</string>
<string name="message_view_sender_label">от %1$s</string>
@ -727,10 +726,6 @@ K-9 Mail — почтовый клиент для Android.
<string name="font_size_message_list_preview">Просмотр</string>
<string name="font_size_message_view">Сообщение</string>
<string name="font_size_message_view_sender">Отправитель</string>
<string name="font_size_message_view_to">Кому</string>
<string name="font_size_message_view_cc">Копия</string>
<string name="font_size_message_view_bcc">Скрытая</string>
<string name="font_size_message_view_additional_headers">Все заголовки</string>
<string name="font_size_message_view_subject">Тема</string>
<string name="font_size_message_view_date">Дата и время</string>
<string name="font_size_message_view_content">Тело сообщения</string>

View file

@ -209,7 +209,6 @@ Prosím, nahlasujte prípadné chyby, prispievajte novými funkciami a pýtajte
<string name="message_view_cc_label">Kópia:</string>
<string name="message_view_bcc_label">Skrytá kópia (Bcc):</string>
<string name="message_view_status_attachment_not_saved">Prílohu sa nepodarilo uložiť.</string>
<string name="message_view_show_pictures_action">Zobraziť obrázky</string>
<string name="message_view_no_viewer">Nemožno nájsť prehliadač pre <xliff:g id="mimetype">%s</xliff:g>.</string>
<string name="message_view_download_remainder">Stiahnuť kompletnú správu</string>
<string name="message_view_sender_label">cez %1$s</string>
@ -663,10 +662,6 @@ Prosím, nahlasujte prípadné chyby, prispievajte novými funkciami a pýtajte
<string name="font_size_message_list_preview">Náhľad</string>
<string name="font_size_message_view">Správy</string>
<string name="font_size_message_view_sender">Odosielateľ</string>
<string name="font_size_message_view_to">Komu</string>
<string name="font_size_message_view_cc">Kópia</string>
<string name="font_size_message_view_bcc">Skrytá kópia</string>
<string name="font_size_message_view_additional_headers">Prídavné hlavičky</string>
<string name="font_size_message_view_subject">Skrytá kópia</string>
<string name="font_size_message_view_date">Čas a dátum</string>
<string name="font_size_message_view_content">Telo správy</string>

Some files were not shown because too many files have changed in this diff Show more