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

View file

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

View file

@ -40,6 +40,7 @@ internal class ParticipantItem(
if (participant.displayName != null) { if (participant.displayName != null) {
name.text = participant.displayName name.text = participant.displayName
name.isVisible = true
} else { } else {
name.isVisible = false name.isVisible = false
} }
@ -48,12 +49,16 @@ internal class ParticipantItem(
menuAddContact.isVisible = !item.alwaysHideAddContactsButton && !participant.isInContacts menuAddContact.isVisible = !item.alwaysHideAddContactsButton && !participant.isInContacts
if (item.showContactsPicture) { if (item.showContactsPicture) {
contactPicture.isVisible = true
item.contactPictureLoader.setContactPicture(contactPicture, participant.address) item.contactPictureLoader.setContactPicture(contactPicture, participant.address)
} else { } else {
contactPicture.isVisible = false contactPicture.isVisible = false
} }
if (!item.participant.isInContacts) { if (item.participant.isInContacts) {
itemView.isClickable = true
itemView.background = originalBackground
} else {
itemView.isClickable = false itemView.isClickable = false
itemView.background = null itemView.background = null
} }
@ -61,11 +66,7 @@ internal class ParticipantItem(
override fun unbindView(item: ParticipantItem) { override fun unbindView(item: ParticipantItem) {
name.text = null name.text = null
name.isVisible = true
email.text = null 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_participant"/>
<item type="id" name="message_details_folder_name"/> <item type="id" name="message_details_folder_name"/>
<item type="id" name="message_details_divider"/> <item type="id" name="message_details_divider"/>
<item type="id" name="message_details_empty"/>
</resources> </resources>