Merge pull request #6760 from thundernest/message_details_refresh

Message details: Update participants when system contacts change
This commit is contained in:
cketti 2023-03-16 12:54:57 +01:00 committed by GitHub
commit 16186ca62d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 50 deletions

View file

@ -37,7 +37,9 @@ internal class CryptoStatusItem(val cryptoDetails: CryptoDetails) : AbstractItem
descriptionTextView.text = context.getString(stringResId)
}
if (!cryptoDetails.isClickable) {
if (cryptoDetails.isClickable) {
itemView.background = originalBackground
} else {
itemView.background = null
}
}
@ -46,7 +48,6 @@ internal class CryptoStatusItem(val cryptoDetails: CryptoDetails) : AbstractItem
imageView.setImageDrawable(null)
titleTextView.text = null
descriptionTextView.text = null
itemView.background = originalBackground
}
}
}

View file

@ -0,0 +1,25 @@
package com.fsck.k9.ui.messagedetails
import android.content.Context
import android.view.View
import android.view.ViewGroup
import com.fsck.k9.ui.R
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
internal class EmptyItem : AbstractItem<EmptyItem.ViewHolder>() {
override val type: Int = R.id.message_details_empty
override val layoutRes = 0
override fun createView(ctx: Context, parent: ViewGroup?): View {
return View(ctx)
}
override fun getViewHolder(v: View) = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<EmptyItem>(view) {
override fun bindView(item: EmptyItem, payloads: List<Any>) = Unit
override fun unbindView(item: EmptyItem) = Unit
}
}

View file

@ -18,6 +18,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResult
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import app.k9mail.ui.utils.bottomsheet.ToolbarBottomSheetDialog
import app.k9mail.ui.utils.bottomsheet.ToolbarBottomSheetDialogFragment
import com.fsck.k9.activity.MessageCompose
import com.fsck.k9.contacts.ContactPictureLoader
@ -44,6 +45,7 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
private val folderIconProvider: FolderIconProvider by inject { parametersOf(requireContext().theme) }
private lateinit var messageReference: MessageReference
private val itemAdapter = ItemAdapter<GenericItem>()
// FIXME: Replace this with a mechanism that survives process death
var cryptoResult: CryptoResultAnnotation? = null
@ -87,15 +89,7 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
val errorView = view.findViewById<View>(R.id.message_details_error)
val recyclerView = view.findViewById<RecyclerView>(R.id.message_details_list)
// Don't allow dragging down the bottom sheet (by dragging the toolbar) unless the list is scrolled all the way
// to the top.
recyclerView.addOnScrollListener(
object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
dialog.behavior.isDraggable = !recyclerView.canScrollVertically(-1)
}
},
)
initializeRecyclerView(recyclerView, dialog)
viewModel.uiEvents.observe(this) { event ->
when (event) {
@ -105,7 +99,7 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
}
}
viewModel.loadData(messageReference).observe(this) { state ->
viewModel.uiState.observe(this) { state ->
when (state) {
MessageDetailsState.Loading -> {
progressBar.isVisible = true
@ -121,23 +115,44 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
progressBar.isVisible = false
errorView.isVisible = false
recyclerView.isVisible = true
setMessageDetails(recyclerView, state.details, state.appearance)
setMessageDetails(state.details, state.appearance)
}
}
}
viewModel.initialize(messageReference)
}
private fun setMessageDetails(
recyclerView: RecyclerView,
details: MessageDetailsUi,
appearance: MessageDetailsAppearance,
) {
val itemAdapter = ItemAdapter<GenericItem>().apply {
override fun onResume() {
super.onResume()
viewModel.reload()
}
private fun initializeRecyclerView(recyclerView: RecyclerView, dialog: ToolbarBottomSheetDialog) {
// Don't allow dragging down the bottom sheet (by dragging the toolbar) unless the list is scrolled all the way
// to the top.
recyclerView.addOnScrollListener(
object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
dialog.behavior.isDraggable = !recyclerView.canScrollVertically(-1)
}
},
)
recyclerView.adapter = FastAdapter.with(itemAdapter).apply {
addEventHook(cryptoStatusClickEventHook)
addEventHook(participantClickEventHook)
addEventHook(addToContactsClickEventHook)
addEventHook(overflowClickEventHook)
}
}
private fun setMessageDetails(details: MessageDetailsUi, appearance: MessageDetailsAppearance) {
val items = buildList {
add(MessageDateItem(details.date ?: getString(R.string.message_details_missing_date)))
if (details.cryptoDetails != null) {
add(CryptoStatusItem(details.cryptoDetails))
}
addCryptoStatus(details)
addParticipants(details.from, R.string.message_details_from_section_title, appearance)
addParticipants(details.sender, R.string.message_details_sender_section_title, appearance)
@ -149,22 +164,27 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
addParticipants(details.cc, R.string.message_details_cc_section_title, appearance)
addParticipants(details.bcc, R.string.message_details_bcc_section_title, appearance)
if (details.folder != null) {
addFolderName(details.folder)
}
addFolderName(details.folder)
}
val adapter = FastAdapter.with(itemAdapter).apply {
addEventHook(cryptoStatusClickEventHook)
addEventHook(participantClickEventHook)
addEventHook(addToContactsClickEventHook)
addEventHook(overflowClickEventHook)
// Use list index as stable identifier. This means changes to the list may only update existing items or add
// new items to the end of the list. Use place holder items (EmptyItem) if necessary.
items.forEachIndexed { index, item ->
item.identifier = index.toLong()
}
recyclerView.adapter = adapter
itemAdapter.setNewList(items)
}
private fun ItemAdapter<GenericItem>.addParticipants(
private fun MutableList<GenericItem>.addCryptoStatus(details: MessageDetailsUi) {
if (details.cryptoDetails != null) {
add(CryptoStatusItem(details.cryptoDetails))
} else {
add(EmptyItem())
}
}
private fun MutableList<GenericItem>.addParticipants(
participants: List<Participant>,
@StringRes title: Int,
appearance: MessageDetailsAppearance,
@ -186,12 +206,16 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
}
}
private fun ItemAdapter<GenericItem>.addFolderName(folder: FolderInfoUi) {
val folderNameItem = FolderNameItem(
displayName = folder.displayName,
iconResourceId = folderIconProvider.getFolderIcon(folder.type),
)
add(folderNameItem)
private fun MutableList<GenericItem>.addFolderName(folder: FolderInfoUi?) {
if (folder != null) {
val folderNameItem = FolderNameItem(
displayName = folder.displayName,
iconResourceId = folderIconProvider.getFolderIcon(folder.type),
)
add(folderNameItem)
} else {
add(EmptyItem())
}
}
private val cryptoStatusClickEventHook = object : ClickEventHook<CryptoStatusItem>() {

View file

@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.k9mail.core.android.common.contact.CachingRepository
import app.k9mail.core.android.common.contact.ContactPermissionResolver
import app.k9mail.core.android.common.contact.ContactRepository
import app.k9mail.core.common.mail.EmailAddress
@ -24,8 +25,8 @@ import java.text.DateFormat
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
@ -42,15 +43,35 @@ internal class MessageDetailsViewModel(
private val folderNameFormatter: FolderNameFormatter,
) : ViewModel() {
private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.getDefault())
private val uiState = MutableStateFlow<MessageDetailsState>(MessageDetailsState.Loading)
private val eventChannel = Channel<MessageDetailEvent>()
private val internalUiState = MutableStateFlow<MessageDetailsState>(MessageDetailsState.Loading)
val uiState: Flow<MessageDetailsState>
get() = internalUiState
private val eventChannel = Channel<MessageDetailEvent>()
val uiEvents = eventChannel.receiveAsFlow()
private var messageReference: MessageReference? = null
var cryptoResult: CryptoResultAnnotation? = null
fun loadData(messageReference: MessageReference): StateFlow<MessageDetailsState> {
fun initialize(messageReference: MessageReference) {
this.messageReference = messageReference
loadData(messageReference)
}
fun reload() {
messageReference?.let { messageReference ->
if (contactRepository is CachingRepository) {
contactRepository.clearCache()
}
loadData(messageReference)
}
}
private fun loadData(messageReference: MessageReference) {
viewModelScope.launch(Dispatchers.IO) {
uiState.value = try {
internalUiState.value = try {
val account = accountManager.getAccount(messageReference.accountUuid) ?: error("Account not found")
val messageDetails = messageRepository.getMessageDetails(messageReference)
@ -82,8 +103,6 @@ internal class MessageDetailsViewModel(
MessageDetailsState.Error
}
}
return uiState
}
private fun buildDisplayDate(messageDate: MessageDate): String? {

View file

@ -40,6 +40,7 @@ internal class ParticipantItem(
if (participant.displayName != null) {
name.text = participant.displayName
name.isVisible = true
} else {
name.isVisible = false
}
@ -48,12 +49,16 @@ internal class ParticipantItem(
menuAddContact.isVisible = !item.alwaysHideAddContactsButton && !participant.isInContacts
if (item.showContactsPicture) {
contactPicture.isVisible = true
item.contactPictureLoader.setContactPicture(contactPicture, participant.address)
} else {
contactPicture.isVisible = false
}
if (!item.participant.isInContacts) {
if (item.participant.isInContacts) {
itemView.isClickable = true
itemView.background = originalBackground
} else {
itemView.isClickable = false
itemView.background = null
}
@ -61,11 +66,7 @@ internal class ParticipantItem(
override fun unbindView(item: ParticipantItem) {
name.text = null
name.isVisible = true
email.text = null
contactPicture.isVisible = true
itemView.background = originalBackground
itemView.isClickable = true
}
}
}

View file

@ -6,4 +6,5 @@
<item type="id" name="message_details_participant"/>
<item type="id" name="message_details_folder_name"/>
<item type="id" name="message_details_divider"/>
<item type="id" name="message_details_empty"/>
</resources>