Skip to content

Commit 0f942a1

Browse files
authored
Merge pull request #1699 from Infomaniak/folder_message_search
Display Folder name when we search/read a Thread
2 parents 9f7345a + 91794f4 commit 0f942a1

File tree

19 files changed

+403
-126
lines changed

19 files changed

+403
-126
lines changed

app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ object RealmDatabase {
162162
//region Configurations versions
163163
const val USER_INFO_SCHEMA_VERSION = 1L
164164
const val MAILBOX_INFO_SCHEMA_VERSION = 4L
165-
const val MAILBOX_CONTENT_SCHEMA_VERSION = 9L
165+
const val MAILBOX_CONTENT_SCHEMA_VERSION = 10L
166166
//endregion
167167

168168
//region Configurations names

app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ThreadController.kt

+22-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package com.infomaniak.mail.data.cache.mailboxContent
1919

20+
import android.content.Context
2021
import com.infomaniak.mail.data.api.ApiRepository
2122
import com.infomaniak.mail.data.cache.RealmDatabase
2223
import com.infomaniak.mail.data.models.Folder
@@ -44,6 +45,7 @@ import okhttp3.OkHttpClient
4445
import javax.inject.Inject
4546

4647
class ThreadController @Inject constructor(
48+
private val context: Context,
4749
private val mailboxContentRealm: RealmDatabase.MailboxContent,
4850
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
4951
) {
@@ -110,12 +112,18 @@ class ThreadController @Inject constructor(
110112

111113
return@withContext mailboxContentRealm().writeBlocking {
112114
val searchFolder = FolderController.getOrCreateSearchFolder(realm = this)
115+
val cachedFolderIds = mutableMapOf<String, String>()
116+
113117
remoteThreads.map { remoteThread ->
114118
ensureActive()
119+
115120
remoteThread.isFromSearch = true
116121

122+
// If we only have 1 Message, we want to display its Folder name.
117123
val folderId = if (remoteThread.messages.count() == 1) {
118-
remoteThread.messages.single().folderId
124+
val firstMessageFolderId = remoteThread.messages.single().folderId
125+
setFolderName(firstMessageFolderId, remoteThread, cachedFolderIds)
126+
firstMessageFolderId
119127
} else {
120128
filterFolder!!.id
121129
}
@@ -127,6 +135,19 @@ class ThreadController @Inject constructor(
127135
}.also(searchFolder.threads::addAll)
128136
}
129137
}
138+
139+
private fun MutableRealm.setFolderName(
140+
firstMessageFolderId: String,
141+
remoteThread: Thread,
142+
cachedFolderIds: MutableMap<String, String>,
143+
) {
144+
val folderName = cachedFolderIds[firstMessageFolderId]
145+
?: FolderController.getFolder(firstMessageFolderId, this)
146+
?.getLocalizedName(context)
147+
?.also { cachedFolderIds[firstMessageFolderId] = it }
148+
149+
folderName?.let { remoteThread.folderName = it }
150+
}
130151
//endregion
131152

132153
//region Edit data

app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ class Thread : RealmObject {
8484
@Transient
8585
var folderId: String = ""
8686
@Transient
87+
var folderName: String = ""
88+
@Transient
8789
var duplicates: RealmList<Message> = realmListOf()
8890
@Transient
8991
var messagesIds: RealmSet<String> = realmSetOf()

app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt

+47-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import android.content.Context
2121
import android.content.res.ColorStateList
2222
import android.graphics.Canvas
2323
import android.os.Build
24+
import android.text.Spannable
25+
import android.text.TextUtils.TruncateAt
2426
import android.view.HapticFeedbackConstants
2527
import android.view.LayoutInflater
2628
import android.view.View
@@ -46,13 +48,13 @@ import com.infomaniak.mail.R
4648
import com.infomaniak.mail.data.LocalSettings
4749
import com.infomaniak.mail.data.LocalSettings.SwipeAction
4850
import com.infomaniak.mail.data.LocalSettings.ThreadDensity
49-
import com.infomaniak.mail.data.LocalSettings.ThreadDensity.COMPACT
50-
import com.infomaniak.mail.data.LocalSettings.ThreadDensity.LARGE
5151
import com.infomaniak.mail.data.models.Folder.FolderRole
5252
import com.infomaniak.mail.data.models.correspondent.Recipient
5353
import com.infomaniak.mail.data.models.thread.Thread
5454
import com.infomaniak.mail.databinding.*
5555
import com.infomaniak.mail.ui.main.folder.ThreadListAdapter.ThreadListViewHolder
56+
import com.infomaniak.mail.ui.main.thread.SubjectFormatter
57+
import com.infomaniak.mail.ui.main.thread.SubjectFormatter.TagColor
5658
import com.infomaniak.mail.utils.RealmChangesBinding
5759
import com.infomaniak.mail.utils.Utils.runCatchingRealm
5860
import com.infomaniak.mail.utils.extensions.*
@@ -94,6 +96,7 @@ class ThreadListAdapter @Inject constructor(
9496
private var folderRole: FolderRole? = null
9597
private var onSwipeFinished: (() -> Unit)? = null
9698
private var multiSelection: MultiSelectionListener<Thread>? = null
99+
private var isFolderNameVisible: Boolean = false
97100

98101
//region Tablet mode
99102
private var openedThreadPosition: Int? = null
@@ -108,10 +111,12 @@ class ThreadListAdapter @Inject constructor(
108111
folderRole: FolderRole?,
109112
onSwipeFinished: (() -> Unit)? = null,
110113
multiSelection: MultiSelectionListener<Thread>? = null,
114+
isFolderNameVisible: Boolean = false,
111115
) {
112116
this.folderRole = folderRole
113117
this.onSwipeFinished = onSwipeFinished
114118
this.multiSelection = multiSelection
119+
this.isFolderNameVisible = isFolderNameVisible
115120
}
116121

117122
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
@@ -188,6 +193,8 @@ class ThreadListAdapter @Inject constructor(
188193
setupThreadDensityDependentUi()
189194
displayAvatar(thread)
190195

196+
displayFolderName(thread)
197+
191198
with(thread) {
192199
expeditor.text = formatRecipientNames(computeDisplayedRecipients())
193200
mailSubject.text = context.formatSubject(subject)
@@ -226,6 +233,37 @@ class ThreadListAdapter @Inject constructor(
226233
updateSelectedUi(thread)
227234
}
228235

236+
private fun CardviewThreadItemBinding.displayFolderName(thread: Thread) {
237+
val isCompactMode = localSettings.threadDensity == ThreadDensity.COMPACT
238+
239+
fun setFolderNameVisibility(isVisible: Boolean) {
240+
folderNameExpandMode.isVisible = !isCompactMode && isVisible
241+
folderNameCompactMode.isVisible = isCompactMode && isVisible
242+
}
243+
244+
val folderNameView = if (isCompactMode) folderNameCompactMode else folderNameExpandMode
245+
246+
if (shouldDisplayFolderName(thread.folderName)) {
247+
folderNameView.text = computeFolderName(thread)
248+
setFolderNameVisibility(isVisible = true)
249+
} else {
250+
setFolderNameVisibility(isVisible = false)
251+
}
252+
}
253+
254+
private fun shouldDisplayFolderName(folderName: String) = isFolderNameVisible && folderName.isNotEmpty()
255+
256+
private fun CardviewThreadItemBinding.computeFolderName(thread: Thread): Spannable {
257+
return context.postfixWithTag(
258+
tag = thread.folderName,
259+
tagColor = TagColor(R.color.folderTagBackground, R.color.folderTagTextColor),
260+
ellipsizeConfiguration = SubjectFormatter.EllipsizeConfiguration(
261+
maxWidth = context.resources.getDimension(R.dimen.folderNameTagMaxSize),
262+
truncateAt = TruncateAt.END,
263+
),
264+
)
265+
}
266+
229267
private fun CardviewThreadItemBinding.onThreadClickWithAbilityToOpenMultiSelection(
230268
thread: Thread,
231269
listener: MultiSelectionListener<Thread>,
@@ -302,27 +340,27 @@ class ThreadListAdapter @Inject constructor(
302340

303341
multiSelection?.let {
304342
with(localSettings) {
305-
expeditorAvatar.isVisible = !isMultiSelected && threadDensity == LARGE
343+
expeditorAvatar.isVisible = !isMultiSelected && threadDensity == ThreadDensity.LARGE
306344
checkMarkLayout.isVisible = it.isEnabled
307345
checkedState.isVisible = isMultiSelected
308-
uncheckedState.isVisible = !isMultiSelected && threadDensity != LARGE
346+
uncheckedState.isVisible = !isMultiSelected && threadDensity != ThreadDensity.LARGE
309347
}
310348
}
311349
}
312350

313351
private fun CardviewThreadItemBinding.setupThreadDensityDependentUi() = with(localSettings) {
314-
val margin = if (threadDensity == COMPACT) threadMarginCompact else threadMarginOther
352+
val margin = if (threadDensity == ThreadDensity.COMPACT) threadMarginCompact else threadMarginOther
315353
threadCard.setMarginsRelative(top = margin, bottom = margin)
316354

317-
expeditorAvatar.isVisible = threadDensity == LARGE
318-
mailBodyPreview.isGone = threadDensity == COMPACT
355+
expeditorAvatar.isVisible = threadDensity == ThreadDensity.LARGE
356+
mailBodyPreview.isGone = threadDensity == ThreadDensity.COMPACT
319357

320358
checkMarkBackground.reshapeToDensity()
321359
uncheckedState.reshapeToDensity()
322360
}
323361

324362
private fun ImageView.reshapeToDensity() {
325-
val checkMarkSize = if (localSettings.threadDensity == LARGE) checkMarkSizeLarge else checkMarkSizeOther
363+
val checkMarkSize = if (localSettings.threadDensity == ThreadDensity.LARGE) checkMarkSizeLarge else checkMarkSizeOther
326364
layoutParams.apply {
327365
width = checkMarkSize
328366
height = checkMarkSize
@@ -542,7 +580,7 @@ class ThreadListAdapter @Inject constructor(
542580
add(folderRole)
543581
}
544582

545-
if (threadDensity == COMPACT) {
583+
if (threadDensity == ThreadDensity.COMPACT) {
546584
if (multiSelection?.selectedItems?.let(threads::containsAll) == false) {
547585
multiSelection?.selectedItems?.removeAll { !threads.contains(it) }
548586
}

app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class SearchFragment : TwoPaneFragment() {
143143
}
144144

145145
private fun setupAdapter() {
146-
threadListAdapter(folderRole = null)
146+
threadListAdapter(folderRole = null, isFolderNameVisible = true)
147147
}
148148

149149
private fun setupListeners() = with(binding) {

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Infomaniak Mail - Android
3-
* Copyright (C) 2023 Infomaniak Network SA
3+
* Copyright (C) 2023-2024 Infomaniak Network SA
44
*
55
* This program is free software: you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -26,17 +26,16 @@ import android.text.style.LineHeightSpan
2626
import android.text.style.ReplacementSpan
2727

2828
/**
29-
* A span to create a rounded background on a text.
30-
*
31-
* If radius is set, it generates a rounded background.
32-
* If radius is null, it generates a circle background.
29+
* A span to create a rounded background with the specified radius on a text.
3330
*/
3431
class RoundedBackgroundSpan(
3532
private val backgroundColor: Int,
3633
private val textColor: Int,
3734
private val textTypeface: Typeface,
3835
private val fontSize: Float,
36+
private val cornerRadius: Float,
3937
) : ReplacementSpan(), LineHeightSpan {
38+
4039
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: FontMetricsInt?): Int {
4140
paint.setGivenTextStyle()
4241
return (LEFT_MARGIN + PADDING + paint.measureText(text, start, end) + PADDING).toInt()
@@ -64,7 +63,7 @@ class RoundedBackgroundSpan(
6463
)
6564

6665
paint.color = backgroundColor
67-
canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint)
66+
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
6867

6968
paint.setGivenTextStyle()
7069
canvas.drawText(
@@ -96,6 +95,7 @@ class RoundedBackgroundSpan(
9695
private const val LEFT_MARGIN = 4
9796
private const val PADDING = 16
9897
private const val VERTICAL_OFFSET = 4
99-
private const val CORNER_RADIUS = 6.0f
98+
99+
fun getTotalHorizontalSpace(): Int = PADDING * 2 + LEFT_MARGIN
100100
}
101101
}

0 commit comments

Comments
 (0)