Merge pull request #6760 from thundernest/message_details_refresh
Message details: Update participants when system contacts change
This commit is contained in:
commit
16186ca62d
6 changed files with 121 additions and 50 deletions
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMessageDetails(
|
viewModel.initialize(messageReference)
|
||||||
recyclerView: RecyclerView,
|
}
|
||||||
details: MessageDetailsUi,
|
|
||||||
appearance: MessageDetailsAppearance,
|
override fun onResume() {
|
||||||
) {
|
super.onResume()
|
||||||
val itemAdapter = ItemAdapter<GenericItem>().apply {
|
|
||||||
|
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)))
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = FastAdapter.with(itemAdapter).apply {
|
itemAdapter.setNewList(items)
|
||||||
addEventHook(cryptoStatusClickEventHook)
|
|
||||||
addEventHook(participantClickEventHook)
|
|
||||||
addEventHook(addToContactsClickEventHook)
|
|
||||||
addEventHook(overflowClickEventHook)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView.adapter = adapter
|
private fun MutableList<GenericItem>.addCryptoStatus(details: MessageDetailsUi) {
|
||||||
|
if (details.cryptoDetails != null) {
|
||||||
|
add(CryptoStatusItem(details.cryptoDetails))
|
||||||
|
} else {
|
||||||
|
add(EmptyItem())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ItemAdapter<GenericItem>.addParticipants(
|
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?) {
|
||||||
|
if (folder != null) {
|
||||||
val folderNameItem = FolderNameItem(
|
val folderNameItem = FolderNameItem(
|
||||||
displayName = folder.displayName,
|
displayName = folder.displayName,
|
||||||
iconResourceId = folderIconProvider.getFolderIcon(folder.type),
|
iconResourceId = folderIconProvider.getFolderIcon(folder.type),
|
||||||
)
|
)
|
||||||
add(folderNameItem)
|
add(folderNameItem)
|
||||||
|
} else {
|
||||||
|
add(EmptyItem())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cryptoStatusClickEventHook = object : ClickEventHook<CryptoStatusItem>() {
|
private val cryptoStatusClickEventHook = object : ClickEventHook<CryptoStatusItem>() {
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue