Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
33f1f4c
feat: Add function to get list of contact with query and limit
Elouan1411 Apr 9, 2026
5659f2f
feat: Display contacts on search and handle click actions
Elouan1411 Apr 10, 2026
1ce3d37
fix: Clear previous contact search results when search button is clic…
Elouan1411 Apr 10, 2026
a1fbe76
feat: Clear contacts when a filter is selected
Elouan1411 Apr 10, 2026
c29dcd2
feat: Clear contacts when Enter key is pressed
Elouan1411 Apr 10, 2026
dee0f8d
feat: Add custom item_contact_search to replace existing implementati…
Elouan1411 Apr 13, 2026
b6eaeef
fix: Reimplement contact display logic to properly show or hide contacts
Elouan1411 Apr 13, 2026
f44adef
fix: Display contacts even when no threads are present in results and…
Elouan1411 Apr 14, 2026
4b1de5c
feat: Add rounded ripple effect matching thread style
Elouan1411 Apr 14, 2026
3298174
fix: Clean code
Elouan1411 Apr 14, 2026
bbfbd93
fix: Prevent contacts from displaying when performing a search, switc…
Elouan1411 Apr 16, 2026
66dd343
fix: Resolve LiveData race condition in state checking
Elouan1411 Apr 16, 2026
b782edd
refactor: Clean code
Elouan1411 Apr 16, 2026
1092a79
fix: Add missing element from commit 01d14c15e3e456873a28199878978611…
Elouan1411 Apr 16, 2026
26f4a63
fix: Prevent dataset overwrite
Elouan1411 Apr 16, 2026
c6b7508
refactor: Clean code
Elouan1411 Apr 17, 2026
61a487f
refactor: Use DateSeparator in contact header and rename it
Elouan1411 Apr 17, 2026
a89962a
fix: Combine contacts and thread results for search
solrubado Apr 17, 2026
014e323
fix: Update query in realm to support diacritic
Elouan1411 Apr 20, 2026
5e76073
fix: Move formatSearchList in viewmodel
Elouan1411 Apr 20, 2026
bbee23e
fix: Reduce method length
Elouan1411 Apr 21, 2026
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
Expand Up @@ -43,6 +43,19 @@ class MergedContactController @Inject constructor(@UserInfoRealm private val use
.sort(MergedContact::comesFromApi.name, Sort.DESCENDING)
}

fun searchMergedContacts(searchQuery: String, searchQueryClean: String, limit: Int = 5): List<MergedContact> {
val queryStr = if (searchQuery != searchQueryClean) {
"(name CONTAINS[c] $0 OR email CONTAINS[c] $0) OR (name CONTAINS[c] $1 OR email CONTAINS[c] $1)"
} else {
"name CONTAINS[c] $0 OR email CONTAINS[c] $0"
}
return userInfoRealm.query<MergedContact>(queryStr, searchQuery, searchQueryClean)
.sort(MergedContact::name.name)
.sort(MergedContact::comesFromApi.name, Sort.DESCENDING)
.limit(limit)
.find()
}

private fun getMergedContactFromContactGroupQuery(contact: ContactGroup): RealmQuery<MergedContact> {
return userInfoRealm.query<MergedContact>("${MergedContact::remoteContactGroupIds.name} == $0", contact.id)
.sort(MergedContact::name.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import androidx.annotation.ColorInt
import androidx.core.text.toSpannable
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.core.view.updateLayoutParams
import com.infomaniak.core.legacy.utils.getAttributes
import com.infomaniak.core.legacy.utils.setMarginsRelative
import com.infomaniak.mail.R
Expand Down Expand Up @@ -145,6 +147,16 @@ class AvatarNameEmailView @JvmOverloads constructor(
userEmail.text = searchQuery
}

fun setAvatarMarginStart(margin: Int) {
binding.avatarLayout.updateLayoutParams<MarginLayoutParams> {
marginStart = margin
}
}

fun removeBackground(){
binding.root.background = null
}

override fun setOnClickListener(onClickListener: OnClickListener?) {
binding.root.setOnClickListener(onClickListener)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
package com.infomaniak.mail.ui.main.folder

import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.correspondent.MergedContact
import com.infomaniak.mail.data.models.thread.Thread

sealed interface ThreadListItem {
data class Content(val thread: Thread) : ThreadListItem
data class DateSeparator(val title: String) : ThreadListItem
data class SectionTitle(val title: String) : ThreadListItem
data class ContactItem(val contact: MergedContact) : ThreadListItem
data class FlushFolderButton(val folderRole: Folder.FolderRole) : ThreadListItem
data object LoadMore : ThreadListItem
data object Spacer : ThreadListItem
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,6 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.android.material.card.MaterialCardView
import com.infomaniak.core.common.utils.format
import com.infomaniak.core.common.utils.isInTheFuture
import com.infomaniak.core.common.utils.isThisMonth
import com.infomaniak.core.common.utils.isThisWeek
import com.infomaniak.core.common.utils.isThisYear
import com.infomaniak.core.common.utils.isToday
import com.infomaniak.core.common.utils.isYesterday
import com.infomaniak.core.legacy.utils.capitalizeFirstChar
import com.infomaniak.core.legacy.utils.context
import com.infomaniak.core.legacy.utils.setMarginsRelative
import com.infomaniak.core.matomo.Matomo.TrackerAction
Expand All @@ -61,12 +53,15 @@ import com.infomaniak.mail.data.LocalSettings.ThreadDensity
import com.infomaniak.mail.data.cache.RealmDatabase
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.SwipeAction
import com.infomaniak.mail.data.models.correspondent.MergedContact
import com.infomaniak.mail.data.models.correspondent.Recipient
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.databinding.CardviewThreadItemBinding
import com.infomaniak.mail.databinding.ItemBannerWithActionViewBinding
import com.infomaniak.mail.databinding.ItemThreadDateSeparatorBinding
import com.infomaniak.mail.databinding.ItemContactSearchBinding
import com.infomaniak.mail.databinding.ItemSpacerSmallBinding
import com.infomaniak.mail.databinding.ItemThreadLoadMoreButtonBinding
import com.infomaniak.mail.databinding.ItemThreadSectionTitleBinding
import com.infomaniak.mail.ui.main.folder.ThreadListAdapter.ThreadListViewHolder
import com.infomaniak.mail.ui.main.thread.SubjectFormatter
import com.infomaniak.mail.ui.main.thread.SubjectFormatter.TagColor
Expand All @@ -75,13 +70,12 @@ import com.infomaniak.mail.ui.main.thread.ThreadFragment.NextThreadTarget.NEXT_C
import com.infomaniak.mail.ui.main.thread.ThreadFragment.NextThreadTarget.PREVIOUS_CHRONOLOGICAL_THREAD
import com.infomaniak.mail.utils.RealmChangesBinding
import com.infomaniak.mail.utils.SentryDebug
import com.infomaniak.mail.utils.ThreadListUtils
import com.infomaniak.mail.utils.Utils.runCatchingRealm
import com.infomaniak.mail.utils.extensions.formatSubject
import com.infomaniak.mail.utils.extensions.getAttributeColor
import com.infomaniak.mail.utils.extensions.isEmail
import com.infomaniak.mail.utils.extensions.isLastWeek
import com.infomaniak.mail.utils.extensions.postfixWithTag
import com.infomaniak.mail.utils.extensions.toDate
import dagger.hilt.android.qualifiers.ActivityContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -154,8 +148,10 @@ class ThreadListAdapter @Inject constructor(
override fun getItemViewType(position: Int): Int = runCatchingRealm {
return when (dataSet[position]) {
is ThreadListItem.Content -> DisplayType.THREAD.layout
is ThreadListItem.DateSeparator -> DisplayType.DATE_SEPARATOR.layout
is ThreadListItem.SectionTitle -> DisplayType.TEXT_SEPARATOR.layout
is ThreadListItem.FlushFolderButton -> DisplayType.FLUSH_FOLDER_BUTTON.layout
is ThreadListItem.ContactItem -> DisplayType.CONTACT_ITEM.layout
is ThreadListItem.Spacer -> DisplayType.SPACER.layout
ThreadListItem.LoadMore -> DisplayType.LOAD_MORE_BUTTON.layout
}
}.getOrDefault(super.getItemViewType(position))
Expand All @@ -172,7 +168,8 @@ class ThreadListAdapter @Inject constructor(
override fun getItemId(position: Int): Long = runCatchingRealm {
return when (val item = dataSet[position]) {
is ThreadListItem.Content -> item.thread.uid.hashCode().toLong()
is ThreadListItem.DateSeparator -> item.title.hashCode().toLong()
is ThreadListItem.SectionTitle -> item.title.hashCode().toLong()
is ThreadListItem.ContactItem -> item.contact.email.hashCode().toLong()
Comment thread
Elouan1411 marked this conversation as resolved.
else -> super.getItemId(position)
}
}.getOrDefault(super.getItemId(position))
Expand All @@ -186,17 +183,18 @@ class ThreadListAdapter @Inject constructor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThreadListViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = when (viewType) {
R.layout.item_thread_date_separator -> ItemThreadDateSeparatorBinding.inflate(layoutInflater, parent, false)
R.layout.item_thread_section_title -> ItemThreadSectionTitleBinding.inflate(layoutInflater, parent, false)
R.layout.item_banner_with_action_view -> ItemBannerWithActionViewBinding.inflate(layoutInflater, parent, false)
R.layout.item_thread_load_more_button -> ItemThreadLoadMoreButtonBinding.inflate(layoutInflater, parent, false)
R.layout.item_contact_search -> ItemContactSearchBinding.inflate(layoutInflater, parent, false)
R.layout.item_spacer_small -> ItemSpacerSmallBinding.inflate(layoutInflater, parent, false)
else -> CardviewThreadItemBinding.inflate(layoutInflater, parent, false)
}

return ThreadListViewHolder(binding)
}

override fun onBindViewHolder(holder: ThreadListViewHolder, position: Int, payloads: MutableList<Any>) = runCatchingRealm {

val payload = payloads.firstOrNull()
if (payload !is NotificationType) {
super.onBindViewHolder(holder, position, payloads)
Expand All @@ -219,16 +217,19 @@ class ThreadListAdapter @Inject constructor(
DisplayType.THREAD.layout -> {
(this as CardviewThreadItemBinding).displayThread((item as ThreadListItem.Content).thread, position)
}
DisplayType.DATE_SEPARATOR.layout -> {
(this as ItemThreadDateSeparatorBinding).displayDateSeparator((item as ThreadListItem.DateSeparator).title)
DisplayType.TEXT_SEPARATOR.layout -> {
(this as ItemThreadSectionTitleBinding).displayDateSeparator((item as ThreadListItem.SectionTitle).title)
}
DisplayType.FLUSH_FOLDER_BUTTON.layout -> {
(this as ItemBannerWithActionViewBinding)
.displayFlushFolderButton((item as ThreadListItem.FlushFolderButton).folderRole)
}
DisplayType.LOAD_MORE_BUTTON.layout -> {
(this as ItemThreadLoadMoreButtonBinding).displayLoadMoreButton()
DisplayType.LOAD_MORE_BUTTON.layout -> (this as ItemThreadLoadMoreButtonBinding).displayLoadMoreButton()
DisplayType.CONTACT_ITEM.layout -> {
val contactItem = item as ThreadListItem.ContactItem
(this as ItemContactSearchBinding).displayContactItem(contactItem.contact)
}
DisplayType.SPACER.layout -> Unit
}
}

Expand Down Expand Up @@ -566,7 +567,7 @@ class ThreadListAdapter @Inject constructor(
setTextColor(context.getColor(R.color.primaryTextColor))
}

private fun ItemThreadDateSeparatorBinding.displayDateSeparator(title: String) {
private fun ItemThreadSectionTitleBinding.displayDateSeparator(title: String) {
sectionTitle.text = title
}

Expand Down Expand Up @@ -594,6 +595,16 @@ class ThreadListAdapter @Inject constructor(
}
}

private fun ItemContactSearchBinding.displayContactItem(contact: MergedContact) {
contactDetails.setMergedContact(contact)
contactDetails.setAvatarMarginStart(0)
contactDetails.removeBackground()

contactWithSpace.setOnClickListener {
callbacks?.onContactClicked?.invoke(contact)
}
}

override fun onSwipeStarted(item: ThreadListItem, viewHolder: ThreadListViewHolder) {
if (item is ThreadListItem.Content) item.thread.updateDynamicIcons()
}
Expand Down Expand Up @@ -714,6 +725,20 @@ class ThreadListAdapter @Inject constructor(
}
}

fun updateListWithThreadListItems(
threadListItems: List<ThreadListItem>,
lifecycleScope: LifecycleCoroutineScope
) {
formatListJob?.cancel()
formatListJob = lifecycleScope.launch {
Dispatchers.Main {
// Put back "Load more" button if it was already there
dataSet = if (isLoadMoreDisplayed) threadListItems + ThreadListItem.LoadMore else threadListItems
refreshSelectedPositionIfPossible()
}
}
}

private fun checkShouldUpdateOpenedThreadPosition(currentUid: String): Boolean {

val currentPos = openedThreadPosition
Expand Down Expand Up @@ -760,9 +785,9 @@ class ThreadListAdapter @Inject constructor(
threads.forEach { thread ->
scope.ensureActive()

val sectionTitle = thread.getSectionTitle(context)
val sectionTitle = ThreadListUtils.getSectionTitle(thread, context)
if (sectionTitle != previousSectionTitle) {
add(ThreadListItem.DateSeparator(sectionTitle))
add(ThreadListItem.SectionTitle(sectionTitle))
previousSectionTitle = sectionTitle
}

Expand All @@ -772,7 +797,7 @@ class ThreadListAdapter @Inject constructor(
}
}

private fun cleanMultiSelectionItems(threads: List<Thread>, scope: CoroutineScope) {
fun cleanMultiSelectionItems(threads: List<Thread>, scope: CoroutineScope) {
if (multiSelection?.selectedItems?.let(threads::containsAll) == false) {
multiSelection?.selectedItems?.removeAll {
scope.ensureActive()
Expand All @@ -781,19 +806,6 @@ class ThreadListAdapter @Inject constructor(
}
}

private fun Thread.getSectionTitle(context: Context): String = with(internalDate.toDate()) {
return when {
isInTheFuture() -> context.getString(R.string.comingSoon)
isToday() -> context.getString(R.string.threadListSectionToday)
isYesterday() -> context.getString(R.string.messageDetailsYesterday)
isThisWeek() -> context.getString(R.string.threadListSectionThisWeek)
isLastWeek() -> context.getString(R.string.threadListSectionLastWeek)
isThisMonth() -> context.getString(R.string.threadListSectionThisMonth)
isThisYear() -> format(FULL_MONTH).capitalizeFirstChar()
else -> format(MONTH_AND_YEAR).capitalizeFirstChar()
}
}

fun updateFolderRole(newRole: FolderRole?) {
folderRole = newRole
}
Expand Down Expand Up @@ -821,9 +833,11 @@ class ThreadListAdapter @Inject constructor(

private enum class DisplayType(val layout: Int) {
THREAD(R.layout.cardview_thread_item),
DATE_SEPARATOR(R.layout.item_thread_date_separator),
TEXT_SEPARATOR(R.layout.item_thread_section_title),
FLUSH_FOLDER_BUTTON(R.layout.item_banner_with_action_view),
LOAD_MORE_BUTTON(R.layout.item_thread_load_more_button),
CONTACT_ITEM(R.layout.item_contact_search),
SPACER(R.layout.item_spacer_small)
}

enum class NotificationType {
Expand All @@ -834,9 +848,6 @@ class ThreadListAdapter @Inject constructor(

companion object {
private const val SWIPE_ANIMATION_THRESHOLD = 0.15f

private const val FULL_MONTH = "MMMM"
private const val MONTH_AND_YEAR = "MMMM yyyy"
}

class ThreadListViewHolder(val binding: ViewBinding) : ViewHolder(binding.root) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package com.infomaniak.mail.ui.main.folder

import com.infomaniak.mail.data.models.correspondent.MergedContact
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.data.models.thread.Thread

Expand All @@ -28,4 +29,5 @@ interface ThreadListAdapterCallbacks {
var onPositionClickedChanged: (position: Int, previousPosition: Int) -> Unit
var deleteThreadInRealm: (threadUid: String) -> Unit
val getFeatureFlags: () -> Mailbox.FeatureFlagSet?
var onContactClicked: ((MergedContact) -> Unit)?
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import com.infomaniak.mail.data.LocalSettings.ThreadDensity.COMPACT
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.SwipeAction
import com.infomaniak.mail.data.models.correspondent.MergedContact
import com.infomaniak.mail.data.models.mailbox.Mailbox.FeatureFlagSet
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.data.models.thread.Thread.ThreadFilter
Expand Down Expand Up @@ -384,6 +385,8 @@ class ThreadListFragment : TwoPaneFragment(), PickerEmojiObserver {
override var deleteThreadInRealm: (String) -> Unit = { threadUid -> mainViewModel.deleteThreadInRealm(threadUid) }

override val getFeatureFlags: () -> FeatureFlagSet? = { mainViewModel.featureFlagsLive.value }

override var onContactClicked: ((MergedContact) -> Unit)? = null
},
multiSelection = object : MultiSelectionListener<Thread> {
override var isEnabled by mainViewModel::isMultiSelectOn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ abstract class TwoPaneFragment : Fragment() {
}
}

fun handleOnBackPressed() {
open fun handleOnBackPressed() {
when {
isOnlyRightShown() -> {
if (SDK_INT >= 29) requireActivity().window.isNavigationBarContrastEnforced = true
Expand Down
Loading
Loading