Skip to content

Commit 2118bcb

Browse files
Merge pull request #1684 from Infomaniak/collapsed-thread
Add SuperCollapsedBlock
2 parents ebab49b + 5f4f8e6 commit 2118bcb

File tree

15 files changed

+384
-92
lines changed

15 files changed

+384
-92
lines changed

app/src/main/java/com/infomaniak/mail/data/models/message/Message.kt

+3
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ class Message : RealmObject {
144144
@Transient
145145
@Ignore
146146
var splitBody: MessageBodyUtils.SplitBody? = null
147+
@Transient
148+
@Ignore
149+
var shouldHideDivider: Boolean = false
147150
//endregion
148151

149152
val threads by backlinks(Thread::messages)

app/src/main/java/com/infomaniak/mail/ui/main/thread/PrintMailFragment.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import androidx.navigation.fragment.navArgs
3030
import com.infomaniak.lib.core.utils.safeBinding
3131
import com.infomaniak.mail.R
3232
import com.infomaniak.mail.data.LocalSettings
33+
import com.infomaniak.mail.data.models.message.Message
3334
import com.infomaniak.mail.databinding.FragmentPrintMailBinding
3435
import com.infomaniak.mail.ui.main.thread.ThreadAdapter.ThreadAdapterCallbacks
3536
import dagger.hilt.android.AndroidEntryPoint
@@ -55,8 +56,8 @@ class PrintMailFragment : Fragment() {
5556
override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = with(threadViewModel) {
5657
super.onViewCreated(view, savedInstanceState)
5758
setupAdapter()
58-
messagesLive.observe(viewLifecycleOwner) { messages ->
59-
threadAdapter.submitList(messages.filter { it.uid == navigationArgs.messageUid })
59+
messagesLive.observe(viewLifecycleOwner) { (items, _) ->
60+
threadAdapter.submitList(items.filter { it is Message && it.uid == navigationArgs.messageUid })
6061
}
6162
navigationArgs.openThreadUid?.let {
6263
reassignThreadLive(it)

app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt

+128-48
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import androidx.core.view.isVisible
3333
import androidx.recyclerview.widget.DiffUtil
3434
import androidx.recyclerview.widget.ListAdapter
3535
import androidx.recyclerview.widget.RecyclerView
36-
import androidx.recyclerview.widget.RecyclerView.ViewHolder
36+
import androidx.recyclerview.widget.RecyclerView.*
37+
import androidx.viewbinding.ViewBinding
3738
import androidx.webkit.WebSettingsCompat
3839
import androidx.webkit.WebViewFeature
3940
import com.infomaniak.lib.core.utils.context
@@ -45,8 +46,9 @@ import com.infomaniak.mail.data.models.calendar.Attendee
4546
import com.infomaniak.mail.data.models.calendar.Attendee.AttendanceState
4647
import com.infomaniak.mail.data.models.correspondent.Recipient
4748
import com.infomaniak.mail.data.models.message.Message
48-
import com.infomaniak.mail.databinding.ItemMessageBinding
49-
import com.infomaniak.mail.ui.main.thread.ThreadAdapter.MessageViewHolder
49+
import com.infomaniak.mail.data.models.message.Message.*
50+
import com.infomaniak.mail.databinding.*
51+
import com.infomaniak.mail.ui.main.thread.ThreadAdapter.*
5052
import com.infomaniak.mail.utils.*
5153
import com.infomaniak.mail.utils.MailDateFormatUtils.mailFormattedDate
5254
import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate
@@ -70,9 +72,9 @@ class ThreadAdapter(
7072
private val isForPrinting: Boolean = false,
7173
private val isCalendarEventExpandedMap: MutableMap<String, Boolean> = mutableMapOf(),
7274
private var threadAdapterCallbacks: ThreadAdapterCallbacks? = null,
73-
) : ListAdapter<Message, MessageViewHolder>(MessageDiffCallback()) {
75+
) : ListAdapter<Any, ThreadAdapterViewHolder>(MessageDiffCallback()) {
7476

75-
inline val messages: MutableList<Message> get() = currentList
77+
inline val items: MutableList<Any> get() = currentList
7678

7779
var isExpandedMap = mutableMapOf<String, Boolean>()
7880

@@ -105,50 +107,87 @@ class ThreadAdapter(
105107
super.onAttachedToRecyclerView(recyclerView)
106108
}
107109

108-
override fun getItemCount(): Int = runCatchingRealm { messages.count() }.getOrDefault(0)
110+
override fun getItemCount(): Int = runCatchingRealm { items.count() }.getOrDefault(0)
109111

110-
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
111-
return MessageViewHolder(
112-
ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
113-
shouldLoadDistantResources,
114-
threadAdapterCallbacks?.onContactClicked,
115-
threadAdapterCallbacks?.onAttachmentClicked,
116-
threadAdapterCallbacks?.onAttachmentOptionsClicked,
117-
)
112+
override fun getItemViewType(position: Int): Int = runCatchingRealm {
113+
return when (items[position]) {
114+
is Message -> DisplayType.MAIL.layout
115+
else -> DisplayType.SUPER_COLLAPSED_BLOCK.layout
116+
}
117+
}.getOrDefault(super.getItemViewType(position))
118+
119+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThreadAdapterViewHolder {
120+
val layoutInflater = LayoutInflater.from(parent.context)
121+
return if (viewType == DisplayType.MAIL.layout) {
122+
MessageViewHolder(
123+
ItemMessageBinding.inflate(layoutInflater, parent, false),
124+
shouldLoadDistantResources,
125+
threadAdapterCallbacks?.onContactClicked,
126+
threadAdapterCallbacks?.onAttachmentClicked,
127+
threadAdapterCallbacks?.onAttachmentOptionsClicked,
128+
)
129+
} else {
130+
SuperCollapsedBlockViewHolder(ItemSuperCollapsedBlockBinding.inflate(layoutInflater, parent, false))
131+
}
118132
}
119133

120-
override fun onBindViewHolder(holder: MessageViewHolder, position: Int, payloads: MutableList<Any>) = runCatchingRealm {
121-
with(holder.binding) {
122-
val payload = payloads.firstOrNull()
123-
if (payload !is NotifyType) {
124-
super.onBindViewHolder(holder, position, payloads)
125-
return@runCatchingRealm
126-
}
134+
override fun onBindViewHolder(holder: ThreadAdapterViewHolder, position: Int, payloads: MutableList<Any>) = runCatchingRealm {
127135

128-
val message = messages[position]
136+
val payload = payloads.firstOrNull()
137+
if (payload !is NotifyType) {
138+
super.onBindViewHolder(holder, position, payloads)
139+
return@runCatchingRealm
140+
}
129141

142+
val item = items[position]
143+
if (item is Message && holder is MessageViewHolder) with(holder.binding) {
130144
when (payload) {
131145
NotifyType.TOGGLE_LIGHT_MODE -> {
132-
isThemeTheSameMap[message.uid] = !isThemeTheSameMap[message.uid]!!
133-
holder.toggleContentAndQuoteTheme(message.uid)
146+
isThemeTheSameMap[item.uid] = !isThemeTheSameMap[item.uid]!!
147+
holder.toggleContentAndQuoteTheme(item.uid)
134148
}
135149
NotifyType.RE_RENDER -> reloadVisibleWebView()
136150
NotifyType.FAILED_MESSAGE -> {
137151
messageLoader.isGone = true
138152
failedLoadingErrorMessage.isVisible = true
139-
if (isExpandedMap[message.uid] == true) onExpandedMessageLoaded(message.uid)
153+
if (isExpandedMap[item.uid] == true) onExpandedMessageLoaded(item.uid)
140154
}
141155
NotifyType.ONLY_REBIND_CALENDAR_ATTENDANCE -> {
142-
val attendees = message.latestCalendarEventResponse?.calendarEvent?.attendees ?: emptyList()
156+
val attendees = item.latestCalendarEventResponse?.calendarEvent?.attendees ?: emptyList()
143157
holder.binding.calendarEvent.onlyUpdateAttendance(attendees)
144158
}
145159
}
146160
}
147161
}.getOrDefault(Unit)
148162

149-
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) = with(holder) {
150-
val message = messages[position]
163+
override fun onBindViewHolder(holder: ThreadAdapterViewHolder, position: Int) {
164+
165+
val item = items[position]
166+
167+
holder.binding.root.tag = if (item is SuperCollapsedBlock || (item is Message && item.shouldHideDivider)) {
168+
IGNORE_DIVIDER_TAG
169+
} else {
170+
null
171+
}
172+
173+
if (item is Message) {
174+
(holder as MessageViewHolder).bindMail(item, position)
175+
} else {
176+
(holder as SuperCollapsedBlockViewHolder).bindSuperCollapsedBlock(item as SuperCollapsedBlock)
177+
}
178+
}
179+
180+
private fun SuperCollapsedBlockViewHolder.bindSuperCollapsedBlock(
181+
item: SuperCollapsedBlock,
182+
) = with(binding.superCollapsedBlock) {
183+
text = context.getString(R.string.superCollapsedBlock, item.messagesUids.count())
184+
setOnClickListener {
185+
text = context.getString(R.string.loadingText)
186+
threadAdapterCallbacks?.onSuperCollapsedBlockClicked?.invoke()
187+
}
188+
}
151189

190+
private fun MessageViewHolder.bindMail(message: Message, position: Int) {
152191
initMapForNewMessage(message, position)
153192

154193
bindHeader(message)
@@ -197,7 +236,7 @@ class ThreadAdapter(
197236

198237
private fun initMapForNewMessage(message: Message, position: Int) {
199238
if (isExpandedMap[message.uid] == null) {
200-
isExpandedMap[message.uid] = message.shouldBeExpanded(position, messages.lastIndex)
239+
isExpandedMap[message.uid] = message.shouldBeExpanded(position, items.lastIndex)
201240
}
202241

203242
if (isThemeTheSameMap[message.uid] == null) isThemeTheSameMap[message.uid] = true
@@ -255,7 +294,7 @@ class ThreadAdapter(
255294
private fun WebView.processMailDisplay(styledBody: String, uid: String, isForPrinting: Boolean): String {
256295
val isDisplayedInDark = context.isNightModeEnabled() && isThemeTheSameMap[uid] == true && !isForPrinting
257296
return if (isForPrinting) {
258-
webViewUtils.processHtmlForPrint(styledBody, HtmlFormatter.PrintData(context, messages.first()))
297+
webViewUtils.processHtmlForPrint(styledBody, HtmlFormatter.PrintData(context, items.first() as Message))
259298
} else {
260299
webViewUtils.processHtmlForDisplay(styledBody, isDisplayedInDark)
261300
}
@@ -534,7 +573,7 @@ class ThreadAdapter(
534573
fun isMessageUidManuallyAllowed(messageUid: String) = manuallyAllowedMessageUids.contains(messageUid)
535574

536575
fun toggleLightMode(message: Message) {
537-
val index = messages.indexOf(message)
576+
val index = items.indexOf(message)
538577
notifyItemChanged(index, NotifyType.TOGGLE_LIGHT_MODE)
539578
}
540579

@@ -544,7 +583,7 @@ class ThreadAdapter(
544583

545584
fun updateFailedMessages(uids: List<String>) {
546585
uids.forEach { uid ->
547-
val index = messages.indexOfFirst { it.uid == uid }
586+
val index = items.indexOfFirst { it is Message && it.uid == uid }
548587
notifyItemChanged(index, NotifyType.FAILED_MESSAGE)
549588
}
550589
}
@@ -554,7 +593,7 @@ class ThreadAdapter(
554593
}
555594

556595
fun undoUserAttendanceClick(message: Message) {
557-
val indexOfMessage = messages.indexOfFirst { it.uid == message.uid }.takeIf { it >= 0 }
596+
val indexOfMessage = items.indexOfFirst { it is Message && it.uid == message.uid }.takeIf { it >= 0 }
558597
indexOfMessage?.let { notifyItemChanged(it, NotifyType.ONLY_REBIND_CALENDAR_ATTENDANCE) }
559598
}
560599

@@ -571,34 +610,53 @@ class ThreadAdapter(
571610
PHONE,
572611
}
573612

574-
class MessageDiffCallback : DiffUtil.ItemCallback<Message>() {
575-
override fun areItemsTheSame(oldMessage: Message, newMessage: Message): Boolean {
576-
return oldMessage.uid == newMessage.uid
613+
class MessageDiffCallback : DiffUtil.ItemCallback<Any>() {
614+
615+
override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
616+
return when (oldItem) {
617+
is Message -> newItem is Message && newItem.uid == oldItem.uid
618+
is SuperCollapsedBlock -> newItem is SuperCollapsedBlock
619+
else -> false
620+
}
577621
}
578622

579-
override fun areContentsTheSame(oldMessage: Message, newMessage: Message): Boolean {
580-
return areMessageContentsTheSameExceptCalendar(oldMessage, newMessage) &&
581-
newMessage.latestCalendarEventResponse == oldMessage.latestCalendarEventResponse
623+
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
624+
return when (oldItem) {
625+
is Message -> {
626+
newItem is Message &&
627+
areMessageContentsTheSameExceptCalendar(oldItem, newItem) &&
628+
newItem.latestCalendarEventResponse == oldItem.latestCalendarEventResponse
629+
}
630+
is SuperCollapsedBlock -> {
631+
newItem is SuperCollapsedBlock &&
632+
newItem.messagesUids.count() == oldItem.messagesUids.count()
633+
}
634+
else -> false
635+
}
582636
}
583637

584-
override fun getChangePayload(oldItem: Message, newItem: Message): Any? {
638+
override fun getChangePayload(oldItem: Any, newItem: Any): Any? {
639+
640+
if (oldItem !is Message || newItem !is Message) return null
641+
585642
// If everything but Attendees is the same, then we know the only thing that could've changed is Attendees.
586643
return if (everythingButAttendeesIsTheSame(oldItem, newItem)) NotifyType.ONLY_REBIND_CALENDAR_ATTENDANCE else null
587644
}
588645

589646
companion object {
590-
fun everythingButAttendeesIsTheSame(oldItem: Message, newItem: Message): Boolean {
591-
val newCalendarEventResponse = newItem.latestCalendarEventResponse
592-
val oldCalendarEventResponse = oldItem.latestCalendarEventResponse
647+
fun everythingButAttendeesIsTheSame(oldMessage: Message, newMessage: Message): Boolean {
648+
val newCalendarEventResponse = newMessage.latestCalendarEventResponse
649+
val oldCalendarEventResponse = oldMessage.latestCalendarEventResponse
593650

594-
return (areMessageContentsTheSameExceptCalendar(oldItem, newItem) &&
651+
return (areMessageContentsTheSameExceptCalendar(oldMessage, newMessage) &&
595652
!(newCalendarEventResponse == null && oldCalendarEventResponse == null)
596653
&& newCalendarEventResponse?.everythingButAttendeesIsTheSame(oldCalendarEventResponse) == true)
597654
}
598655

599656
private fun areMessageContentsTheSameExceptCalendar(oldMessage: Message, newMessage: Message): Boolean {
600657
return newMessage.body?.value == oldMessage.body?.value &&
601-
newMessage.splitBody == oldMessage.splitBody
658+
newMessage.splitBody == oldMessage.splitBody &&
659+
newMessage.shouldHideDivider == oldMessage.shouldHideDivider
602660
}
603661
}
604662
}
@@ -614,20 +672,40 @@ class ThreadAdapter(
614672
var onReplyClicked: ((Message) -> Unit)? = null,
615673
var onMenuClicked: ((Message) -> Unit)? = null,
616674
var onAllExpandedMessagesLoaded: (() -> Unit)? = null,
675+
var onSuperCollapsedBlockClicked: (() -> Unit)? = null,
617676
var navigateToNewMessageActivity: ((Uri) -> Unit)? = null,
618677
var navigateToAttendeeBottomSheet: ((List<Attendee>) -> Unit)? = null,
619678
var navigateToDownloadProgressDialog: ((Attachment, AttachmentIntentType) -> Unit)? = null,
620679
var replyToCalendarEvent: ((AttendanceState, Message) -> Unit)? = null,
621680
var promptLink: ((String, ContextMenuType) -> Unit)? = null,
622681
)
623682

624-
class MessageViewHolder(
625-
val binding: ItemMessageBinding,
683+
private enum class DisplayType(val layout: Int) {
684+
MAIL(R.layout.item_message),
685+
SUPER_COLLAPSED_BLOCK(R.layout.item_super_collapsed_block),
686+
}
687+
688+
data class SuperCollapsedBlock(
689+
var shouldBeDisplayed: Boolean = true,
690+
var hasBeenClicked: Boolean = false,
691+
val messagesUids: MutableSet<String> = mutableSetOf(),
692+
) {
693+
fun isFirstTime() = shouldBeDisplayed && messagesUids.isEmpty()
694+
}
695+
696+
abstract class ThreadAdapterViewHolder(open val binding: ViewBinding) : ViewHolder(binding.root)
697+
698+
private class SuperCollapsedBlockViewHolder(
699+
override val binding: ItemSuperCollapsedBlockBinding,
700+
) : ThreadAdapterViewHolder(binding)
701+
702+
private class MessageViewHolder(
703+
override val binding: ItemMessageBinding,
626704
private val shouldLoadDistantResources: Boolean,
627705
onContactClicked: ((contact: Recipient) -> Unit)?,
628706
onAttachmentClicked: ((attachment: Attachment) -> Unit)?,
629707
onAttachmentOptionsClicked: ((attachment: Attachment) -> Unit)?,
630-
) : ViewHolder(binding.root) {
708+
) : ThreadAdapterViewHolder(binding) {
631709

632710
val fromAdapter = DetailedRecipientAdapter(onContactClicked)
633711
val toAdapter = DetailedRecipientAdapter(onContactClicked)
@@ -697,6 +775,8 @@ class ThreadAdapter(
697775

698776
companion object {
699777

778+
const val IGNORE_DIVIDER_TAG = "ignoreDividerTag"
779+
700780
private val contextMenuTypeForHitTestResultType = mapOf(
701781
HitTestResult.PHONE_TYPE to ContextMenuType.PHONE,
702782
HitTestResult.EMAIL_TYPE to ContextMenuType.EMAIL,

0 commit comments

Comments
 (0)