Skip to content

Commit

Permalink
Create a container for icons
Browse files Browse the repository at this point in the history
wip Reorganize subject formatting methods

Code review
  • Loading branch information
tevincent committed Feb 15, 2024
1 parent 5905708 commit 938f091
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import okhttp3.OkHttpClient
import javax.inject.Inject

class ThreadController @Inject constructor(
private val context: Context,
private val mailboxContentRealm: RealmDatabase.MailboxContent,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {
Expand Down Expand Up @@ -81,7 +82,6 @@ class ThreadController @Inject constructor(
* @return A list of search Threads. The search only returns Messages from SPAM or TRASH if we explicitly selected those folders
*/
suspend fun initAndGetSearchFolderThreads(
context: Context,
remoteThreads: List<Thread>,
filterFolder: Folder?,
): List<Thread> = withContext(ioDispatcher) {
Expand Down Expand Up @@ -115,9 +115,11 @@ class ThreadController @Inject constructor(
val searchFolder = FolderController.getOrCreateSearchFolder(realm = this)
remoteThreads.map { remoteThread ->
ensureActive()
val firstMessageFolderId = remoteThread.messages.single().folderId
if (remoteThread.messages.size == 1) {
val folderId = remoteThread.messages.first().folderId
getFolder(folderId, this@writeBlocking)?.let { folder -> remoteThread.folderName = folder.getLocalizedName(context) }
getFolder(firstMessageFolderId, this@writeBlocking)?.let { folder ->
remoteThread.folderName = folder.getLocalizedName(context)
}
}
remoteThread.isFromSearch = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.os.Build
import android.text.TextUtils
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
Expand Down Expand Up @@ -53,7 +54,7 @@ import com.infomaniak.mail.data.models.correspondent.Recipient
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.databinding.*
import com.infomaniak.mail.ui.main.folder.ThreadListAdapter.ThreadListViewHolder
import com.infomaniak.mail.ui.main.thread.SubjectFormatter.getFolderName
import com.infomaniak.mail.ui.main.thread.SubjectFormatter
import com.infomaniak.mail.utils.RealmChangesBinding
import com.infomaniak.mail.utils.Utils.runCatchingRealm
import com.infomaniak.mail.utils.extensions.*
Expand Down Expand Up @@ -87,7 +88,6 @@ class ThreadListAdapter @Inject constructor(
private var swipingIsAuthorized: Boolean = true
private var displaySeeAllButton = false // TODO: Manage this for intelligent mailbox
private var isLoadMoreDisplayed = false
private var isFolderNameVisible: Boolean = false

var onThreadClicked: ((thread: Thread) -> Unit)? = null
var onFlushClicked: ((dialogTitle: String) -> Unit)? = null
Expand All @@ -96,6 +96,7 @@ class ThreadListAdapter @Inject constructor(
private var folderRole: FolderRole? = null
private var onSwipeFinished: (() -> Unit)? = null
private var multiSelection: MultiSelectionListener<Thread>? = null
private var isFolderNameVisible: Boolean = false

//region Tablet mode
private var openedThreadPosition: Int? = null
Expand Down Expand Up @@ -189,10 +190,18 @@ class ThreadListAdapter @Inject constructor(
private fun shouldDisplayFolderName(folderName: String) = isFolderNameVisible && folderName.isNotEmpty()

private fun CardviewThreadItemBinding.displayThread(thread: Thread, position: Int) {
val folderName = getFolderName(thread)
if (shouldDisplayFolderName(folderName)) {
if (shouldDisplayFolderName(thread.folderName)) {
folderNameView.isVisible = true
folderNameView.text = folderName
folderNameView.text = context.postfixWithTag(
tag = thread.folderName,
tagColor = TagColor(R.color.folderNameBackground, R.color.folderNameTextColor),
ellipsizeConfiguration = SubjectFormatter.EllipsizeConfiguration(
maxWidth = context.resources.getDimension(R.dimen.subjectTagMaxSize).toInt(),
truncateAt = TextUtils.TruncateAt.END
),
)
} else {
folderNameView.isVisible = false
}

refreshCachedSelectedPosition(thread.uid, position) // If item changed position, update cached position.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
*/
package com.infomaniak.mail.ui.main.thread

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Paint.FontMetricsInt
import android.graphics.RectF
import android.graphics.Typeface
import android.text.TextPaint
import android.text.style.LineHeightSpan
import android.text.style.ReplacementSpan
import androidx.core.content.res.ResourcesCompat
import com.infomaniak.mail.R

/**
* A span to create a rounded background on a text.
Expand All @@ -36,6 +40,7 @@ class RoundedBackgroundSpan(
private val textColor: Int,
private val textTypeface: Typeface,
private val fontSize: Float,
private val cornerRadius: Float = CORNER_RADIUS
) : ReplacementSpan(), LineHeightSpan {

override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: FontMetricsInt?): Int {
Expand Down Expand Up @@ -65,7 +70,7 @@ class RoundedBackgroundSpan(
)

paint.color = backgroundColor
canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint)
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)

paint.setGivenTextStyle()
canvas.drawText(
Expand Down Expand Up @@ -98,5 +103,11 @@ class RoundedBackgroundSpan(
private const val PADDING = 16
private const val VERTICAL_OFFSET = 4
private const val CORNER_RADIUS = 10f

fun getTagsPaint(context: Context) = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = ResourcesCompat.getColor(context.resources, R.color.folderNameTextColor, null)
textSize = context.resources.getDimension(R.dimen.externalTagTextSize)
typeface = ResourcesCompat.getFont(context, R.font.tag_font)
}
}
}
204 changes: 77 additions & 127 deletions app/src/main/java/com/infomaniak/mail/ui/main/thread/SubjectFormatter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,164 +19,114 @@ package com.infomaniak.mail.ui.main.thread

import android.content.Context
import android.content.res.Resources
import android.graphics.Paint
import android.text.Spannable
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import androidx.core.content.res.ResourcesCompat
import androidx.core.text.toSpannable
import com.infomaniak.mail.MatomoMail.trackExternalEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.utils.ExternalUtils.findExternalRecipients
import com.infomaniak.mail.utils.extensions.MergedContactDictionary
import com.infomaniak.mail.utils.extensions.TagColor
import com.infomaniak.mail.utils.extensions.formatSubject
import com.infomaniak.mail.utils.extensions.postfixWithTag
import javax.inject.Inject
import javax.inject.Singleton
import com.infomaniak.lib.core.R as CoreR

object SubjectFormatter {
@Singleton
class SubjectFormatter @Inject constructor(private val context: Context) {

fun generateSubjectContent(
context: Context,
subjectData: SubjectData,
onTagClicked: (String) -> Unit
onExternalClicked: (String) -> Unit
): Pair<String, CharSequence> = with(subjectData) {
val subject = context.formatSubject(thread.subject)

var spannedSubject: Pair<String, CharSequence>? = null
if (!externalMailFlagEnabled) spannedSubject = subject to subject
val spannedSubjectWithExternal = handleExternals(subject, onExternalClicked)

val (externalRecipientEmail, externalRecipientQuantity) = thread.findExternalRecipients(emailDictionary, aliases)
if (externalRecipientQuantity == 0) spannedSubject = subject to subject

val spannedSubjectWithExternal = createSpannedSubjectWithExternal(
context,
subject,
externalRecipientEmail,
externalRecipientQuantity,
onTagClicked
val spannedSubjectWithFolder = handleFolders(
spannedSubjectWithExternal,
getEllipsizeConfiguration(spannedSubjectWithExternal, getFolderName(thread))
)

val folderName = getFolderName(
context,
subject,
getFolderName(thread),
externalRecipientQuantity != 0
)
getSpannedFolderName(context, folderName, spannedSubjectWithExternal)?.let { spannedFolderName ->
spannedSubject = subject to spannedFolderName
}

return spannedSubject!!
return subject to spannedSubjectWithFolder
}

fun getFolderName(thread: Thread) = if (thread.messages.size > 1) "" else thread.folderName
private fun SubjectData.handleExternals(
previousContent: String,
onExternalClicked: (String) -> Unit
): CharSequence {
if (!externalMailFlagEnabled) return previousContent

private fun createSpannedSubjectWithExternal(
context: Context,
subject: String,
externalRecipientEmail: String?,
externalRecipientQuantity: Int,
onTagClicked: (String) -> Unit
): Spannable {
return context.postfixWithTag(
subject.toSpannable(),
R.string.externalTag,
R.color.externalTagBackground,
R.color.externalTagOnBackground,
) {
context.trackExternalEvent("threadTag")

val description = context.resources.getQuantityString(
R.plurals.externalDialogDescriptionExpeditor,
externalRecipientQuantity,
externalRecipientEmail,
)
onTagClicked(description)
}
}
val (externalRecipientEmail, externalRecipientQuantity) = thread.findExternalRecipients(emailDictionary, aliases)
if (externalRecipientQuantity == 0) return previousContent

private fun getSpannedFolderName(
context: Context,
folderName: CharSequence?,
spannedSubjectWithExternal: Spannable
): Spannable? {
return if (!folderName.isNullOrEmpty()) {
context.postfixWithTag(
spannedSubjectWithExternal,
folderName,
R.color.backgroundFolderName,
R.color.textColorFolderName,
) {
context.trackExternalEvent("threadTag")
}
} else {
null
}
return postFixWithExternal(previousContent, externalRecipientQuantity, externalRecipientEmail, onExternalClicked)
}

private fun getFolderName(
context: Context,
subject: String,
fullFolderName: String,
hasExternalTag: Boolean
): CharSequence {

val (subjectAndExternalLayout, fullSubjectLayout, folderNameLayout) = getStaticLayouts(
context,
subject,
fullFolderName,
hasExternalTag
private fun postFixWithExternal(
previousContent: CharSequence,
externalRecipientQuantity: Int,
externalRecipientEmail: String?,
onExternalClicked: (String) -> Unit
) = context.postfixWithTag(
previousContent,
R.string.externalTag,
TagColor(R.color.externalTagBackground, R.color.externalTagOnBackground)
) {
context.trackExternalEvent("threadTag")

val description = context.resources.getQuantityString(
R.plurals.externalDialogDescriptionExpeditor,
externalRecipientQuantity,
externalRecipientEmail,
)

// In case we know that the folder name take more than one line, we insert a break line to have more space
// If in any case, the folder name take more than the width of the screen, the string will be ellipsized
// the middle.
return if (fullSubjectLayout.lineCount - subjectAndExternalLayout.lineCount > 0) {
"\n ${folderNameLayout.text}"
} else {
fullFolderName
}
onExternalClicked(description)
}

private fun getTagsTextPaint(context: Context) : TextPaint {
val tagsTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
tagsTextPaint.textSize = context.resources.getDimension(R.dimen.externalTagTextSize)
tagsTextPaint.typeface = ResourcesCompat.getFont(context, R.font.tag_font)
tagsTextPaint.density
private fun SubjectData.handleFolders(
previousContent: CharSequence,
ellipsizeTag: EllipsizeConfiguration
): CharSequence {
val folderName = getFolderName(thread)
if (folderName.isEmpty()) return previousContent

return tagsTextPaint
return postFixWithFolder(previousContent, folderName, ellipsizeTag)
}

private fun getStaticLayouts(
context: Context,
subjectString: String,
fullFolderName: String,
hasExternalTag: Boolean
) : StaticLayouts {
val tagsTextPaint = getTagsTextPaint(context)
private fun postFixWithFolder(
previousContent: CharSequence,
folderName: String,
ellipsizeTag: EllipsizeConfiguration
) = context.postfixWithTag(
previousContent,
folderName,
TagColor(R.color.folderNameBackground, R.color.folderNameTextColor),
ellipsizeTag
)

private fun getFolderName(thread: Thread) = if (thread.messages.size > 1) "" else thread.folderName

private fun getEllipsizeConfiguration(previousContent: CharSequence, tag: String): EllipsizeConfiguration {
val paddingsInPixels = (context.resources.getDimension(CoreR.dimen.marginStandard) * 2).toInt()
val width = Resources.getSystem().displayMetrics.widthPixels - paddingsInPixels

val externalString = if (hasExternalTag) context.getString(R.string.externalTag) else ""
val subjectAndExternalString = subjectString + externalString
val fullString = subjectString + externalString + fullFolderName

val folderNameLayout =
StaticLayout.Builder.obtain(fullFolderName, 0, fullFolderName.length, tagsTextPaint, width)
.setEllipsizedWidth(width)
.setEllipsize(TextUtils.TruncateAt.MIDDLE)
.setMaxLines(1)
.build()

val subjectAndExternalLayout =
StaticLayout.Builder.obtain(subjectAndExternalString, 0, subjectAndExternalString.length, tagsTextPaint, width)
.build()
val fullSubjectLayout = StaticLayout.Builder.obtain(fullString, 0, fullString.length, tagsTextPaint, width).build()

return StaticLayouts(subjectAndExternalLayout, fullSubjectLayout, folderNameLayout)
val tagsTextPaint = RoundedBackgroundSpan.getTagsPaint(context)

val layoutBeforeAddingTag = StaticLayout.Builder.obtain(
previousContent,
0,
previousContent.length,
tagsTextPaint,
width
).build()

val fullString = "$previousContent $tag"
val layoutAfterAddingTag = StaticLayout.Builder.obtain(fullString, 0, fullString.length, tagsTextPaint, width).build()

val positionLastChar = layoutBeforeAddingTag.getPrimaryHorizontal(previousContent.length).toInt()
val linesCountDifferent = layoutAfterAddingTag.lineCount != layoutBeforeAddingTag.lineCount
val maxWidth = if (layoutAfterAddingTag.lineCount != layoutBeforeAddingTag.lineCount) width else width - positionLastChar
return EllipsizeConfiguration(maxWidth, TextUtils.TruncateAt.MIDDLE, linesCountDifferent)
}

data class SubjectData(
Expand All @@ -186,9 +136,9 @@ object SubjectFormatter {
val externalMailFlagEnabled: Boolean
)

private data class StaticLayouts(
val subjectAndExternalLayout: StaticLayout,
val fullSubjectLayout: StaticLayout,
val folderNameLayout: StaticLayout
data class EllipsizeConfiguration(
val maxWidth: Int,
val truncateAt: TextUtils.TruncateAt = TextUtils.TruncateAt.MIDDLE,
val withNewLine: Boolean = false
)
}
Loading

0 comments on commit 938f091

Please sign in to comment.