Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package com.fsck.k9.ui.messageview

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material.icons.outlined.Download
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonIcon
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodySmall
import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelLarge
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
import com.fsck.k9.mailstore.AttachmentViewInfo
import com.fsck.k9.ui.R
import com.fsck.k9.ui.helper.SizeFormatter
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.thunderbird.core.ui.compose.designsystem.atom.icon.Icon
import net.thunderbird.core.ui.compose.designsystem.atom.icon.Icons as DesignIcons
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
import org.koin.android.ext.android.inject

class AttachmentListBottomSheet : BottomSheetDialogFragment() {

private val themeProvider: FeatureThemeProvider by inject()

private var attachments: List<AttachmentViewInfo> = emptyList()
private var attachmentCallback: AttachmentViewCallback? = null

fun setData(attachments: List<AttachmentViewInfo>, callback: AttachmentViewCallback) {
this.attachments = attachments
this.attachmentCallback = callback
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
if (attachments.isEmpty()) {
dismissAllowingStateLoss()
}

val sizeFormatter = SizeFormatter(resources)

return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
themeProvider.WithTheme {
AttachmentListContent(
attachments = attachments,
sizeFormatter = sizeFormatter,
onAttachmentClick = { attachmentCallback?.onViewAttachment(it) },
onSaveClick = { attachmentCallback?.onSaveAttachment(it) },
onSaveAllClick = {
attachments.forEach { attachmentCallback?.onSaveAttachment(it) }
},
)
}
}
}
}

companion object {
const val TAG = "AttachmentListBottomSheet"

fun newInstance(): AttachmentListBottomSheet {
return AttachmentListBottomSheet()
}
}
}

@Composable
private fun AttachmentListContent(
attachments: List<AttachmentViewInfo>,
sizeFormatter: SizeFormatter,
onAttachmentClick: (AttachmentViewInfo) -> Unit,
onSaveClick: (AttachmentViewInfo) -> Unit,
onSaveAllClick: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
AttachmentListHeader(onSaveAllClick = onSaveAllClick)

attachments.forEach { attachment ->
AttachmentListItem(
attachment = attachment,
sizeFormatter = sizeFormatter,
onClick = { onAttachmentClick(attachment) },
onSaveClick = { onSaveClick(attachment) },
)
}
}
}

@Composable
private fun AttachmentListHeader(
onSaveAllClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = DesignIcons.Outlined.Attachment,
contentDescription = null,
tint = MainTheme.colors.onSurfaceVariant,
modifier = Modifier.size(24.dp),
)
Spacer(modifier = Modifier.width(8.dp))
TextTitleMedium(
text = stringResource(R.string.message_view_attachments_title),
modifier = Modifier.weight(1f),
)
Row(
modifier = Modifier.clickable(onClick = onSaveAllClick),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Download,
contentDescription = null,
tint = MainTheme.colors.primary,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(4.dp))
TextLabelLarge(
text = stringResource(R.string.message_view_attachments_save_all),
color = MainTheme.colors.primary,
)
}
}
}

@Composable
private fun AttachmentListItem(
attachment: AttachmentViewInfo,
sizeFormatter: SizeFormatter,
onClick: () -> Unit,
onSaveClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(start = 16.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Description,
contentDescription = null,
tint = MainTheme.colors.onSurfaceVariant,
modifier = Modifier.size(40.dp),
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
TextBodyMedium(
text = attachment.displayName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (attachment.size != AttachmentViewInfo.UNKNOWN_SIZE) {
TextBodySmall(
text = sizeFormatter.formatSize(attachment.size),
color = MainTheme.colors.onSurfaceVariant,
)
}
}
ButtonIcon(
onClick = onSaveClick,
imageVector = Icons.Outlined.Download,
contentDescription = stringResource(R.string.save_attachment_action),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -490,41 +490,12 @@ class MessageContainerView(context: Context, attrs: AttributeSet?) :
if (messageViewInfo.attachments != null) {
for (attachment in messageViewInfo.attachments) {
attachments[attachment.internalUri] = attachment
if (attachment.inlineAttachment) {
continue
}

val attachmentView = layoutInflater.inflate(
R.layout.message_view_attachment,
attachmentsContainer,
false,
) as AttachmentView

attachmentView.setCallback(attachmentCallback)
attachmentView.setAttachment(attachment)

attachmentViewMap[attachment] = attachmentView
attachmentsContainer.addView(attachmentView)
}
}

if (messageViewInfo.extraAttachments != null) {
for (attachment in messageViewInfo.extraAttachments) {
attachments[attachment.internalUri] = attachment
if (attachment.inlineAttachment) {
continue
}

val lockedAttachmentView = layoutInflater.inflate(
R.layout.message_view_attachment_locked,
attachmentsContainer,
false,
) as LockedAttachmentView

lockedAttachmentView.setCallback(attachmentCallback)
lockedAttachmentView.setAttachment(attachment)

attachmentsContainer.addView(lockedAttachmentView)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package com.fsck.k9.ui.messageview
interface MessageHeaderClickListener {
fun onParticipantsContainerClick()
fun onMenuItemClick(itemId: Int)
fun onViewAllAttachmentsClick()
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.fsck.k9.mail.Message
import com.fsck.k9.mailstore.AttachmentViewInfo
import com.fsck.k9.mailstore.MessageViewInfo
import com.fsck.k9.ui.R
import com.fsck.k9.ui.helper.SizeFormatter
import com.fsck.k9.ui.messageview.MessageContainerView.OnRenderingFinishedListener
import com.fsck.k9.view.MessageHeader
import com.fsck.k9.view.ThemeUtils
Expand Down Expand Up @@ -147,6 +148,49 @@ class MessageTopView(
} else {
hideShowPicturesButton()
}

updateAttachmentSummary(messageViewInfo)
}

private fun updateAttachmentSummary(messageViewInfo: MessageViewInfo) {
val nonInlineAttachments = messageViewInfo.attachments
?.filter { !it.inlineAttachment }
.orEmpty()

val extraNonInlineAttachments = messageViewInfo.extraAttachments
?.filter { !it.inlineAttachment }
.orEmpty()

val allAttachments = nonInlineAttachments + extraNonInlineAttachments

if (allAttachments.isEmpty()) {
messageHeaderView.hideAttachmentSummary()
return
}

val count = allAttachments.size
val totalSize = allAttachments.sumOf { it.size.coerceAtLeast(0) }
val sizeFormatter = SizeFormatter(context.resources)
val sizeText = sizeFormatter.formatSize(totalSize)

val summaryText: String
val viewButtonText: String

if (count == 1) {
val fileName = allAttachments[0].displayName
summaryText = context.getString(R.string.message_view_single_attachment_summary, fileName, sizeText)
viewButtonText = context.getString(R.string.message_view_attachments_view)
} else {
summaryText = context.resources.getQuantityString(
R.plurals.message_view_attachment_summary,
count,
count,
sizeText,
)
viewButtonText = context.getString(R.string.message_view_attachments_view_all)
}

messageHeaderView.setAttachmentSummary(summaryText, viewButtonText)
}

fun showMessageEncryptedButIncomplete(messageViewInfo: MessageViewInfo, providerIcon: Drawable?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,29 @@ class MessageViewFragment :
else -> error("Missing handler for reply menu item $itemId")
}
}

override fun onViewAllAttachmentsClick() {
showAttachmentListBottomSheet()
}
}

private fun showAttachmentListBottomSheet() {
val messageViewInfo = mMessageViewInfo ?: return

val nonInlineAttachments = messageViewInfo.attachments
?.filter { !it.inlineAttachment }
.orEmpty()

val extraNonInlineAttachments = messageViewInfo.extraAttachments
?.filter { !it.inlineAttachment }
.orEmpty()

val allAttachments = nonInlineAttachments + extraNonInlineAttachments
if (allAttachments.isEmpty()) return

val bottomSheet = AttachmentListBottomSheet.newInstance()
bottomSheet.setData(allAttachments, this)
bottomSheet.show(parentFragmentManager, AttachmentListBottomSheet.TAG)
}

private fun onDownloadButtonClicked() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
private RecipientNamesView recipientNamesView;
private MaterialTextView dateView;
private ImageView menuPrimaryActionView;
private View attachmentSummaryContainer;
private MaterialTextView attachmentSummaryText;
private MaterialTextView viewAllAttachmentsButton;

private RelativeDateTimeFormatter relativeDateTimeFormatter;

Expand Down Expand Up @@ -115,6 +118,11 @@ protected void onFinishInflate() {
TooltipCompat.setTooltipText(menuOverflowView, menuOverflowDescription);

findViewById(R.id.participants_container).setOnClickListener(this);

attachmentSummaryContainer = findViewById(R.id.attachment_summary_container);
attachmentSummaryText = findViewById(R.id.attachment_summary_text);
viewAllAttachmentsButton = findViewById(R.id.view_all_attachments);
viewAllAttachmentsButton.setOnClickListener(this);
}

@Override
Expand All @@ -128,6 +136,8 @@ public void onClick(View view) {
showOverflowMenu(view);
} else if (id == R.id.participants_container) {
messageHeaderClickListener.onParticipantsContainerClick();
} else if (id == R.id.view_all_attachments) {
messageHeaderClickListener.onViewAllAttachmentsClick();
}
}

Expand Down Expand Up @@ -355,4 +365,14 @@ private void setCryptoDisplayStatus(MessageCryptoDisplayStatus displayStatus) {
public void setMessageHeaderClickListener(MessageHeaderClickListener messageHeaderClickListener) {
this.messageHeaderClickListener = messageHeaderClickListener;
}

public void setAttachmentSummary(@NonNull String summaryText, @NonNull String viewButtonText) {
attachmentSummaryText.setText(summaryText);
viewAllAttachmentsButton.setText(viewButtonText);
attachmentSummaryContainer.setVisibility(View.VISIBLE);
}

public void hideAttachmentSummary() {
attachmentSummaryContainer.setVisibility(View.GONE);
}
}
Loading
Loading