Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Bimi feature #1913

Merged
merged 17 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ dependencies {

implementation libs.sentry.android.fragment

implementation libs.coil.svg

// Test
testImplementation libs.junit
androidTestImplementation libs.ext.junit
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/com/infomaniak/mail/MainApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.work.Configuration
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.SvgDecoder
import com.facebook.stetho.Stetho
import com.infomaniak.lib.core.InfomaniakCore
import com.infomaniak.lib.core.auth.TokenInterceptorListener
Expand Down Expand Up @@ -274,6 +275,14 @@ open class MainApplication : Application(), ImageLoaderFactory, DefaultLifecycle
lastAppClosingTime = null
}

fun createSvgImageLoader(): ImageLoader {
return CoilUtils.newImageLoader(
applicationContext,
tokenInterceptorListener(),
customFactories = listOf(SvgDecoder.Factory())
)
}

companion object {
private const val FIRST_LAUNCH_TIME = 0L
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,8 @@ object ApiRoutes {
fun resource(resource: String): String {
return "$MAIL_API$resource"
}

fun bimi(bimi: String): String {
return "$MAIL_API$bimi"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.infomaniak.mail.data.models.AppSettings
import com.infomaniak.mail.data.models.Attachment
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.Quotas
import com.infomaniak.mail.data.models.Bimi
import com.infomaniak.mail.data.models.addressBook.AddressBook
import com.infomaniak.mail.data.models.calendar.Attendee
import com.infomaniak.mail.data.models.calendar.CalendarEvent
Expand Down Expand Up @@ -199,6 +200,7 @@ object RealmDatabase {
CalendarEvent::class,
Attendee::class,
Signature::class,
Bimi::class,
)
//endregion

Expand Down
64 changes: 64 additions & 0 deletions app/src/main/java/com/infomaniak/mail/data/models/Bimi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Infomaniak Mail - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.mail.data.models

import android.os.Parcel
import android.os.Parcelable
import io.realm.kotlin.types.EmbeddedRealmObject
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Parcelize
@Serializable
class Bimi() : EmbeddedRealmObject, Parcelable {

//region Remote data
@SerialName("svg_content")
var svgContentUrl: String? = null
@SerialName("is_certified")
var isCertified: Boolean = false
NicolasBourdin88 marked this conversation as resolved.
Show resolved Hide resolved
//endregion

constructor(svgContentUrl: String, isCertified: Boolean) : this() {
this.svgContentUrl = svgContentUrl
this.isCertified = isCertified
}

companion object : Parceler<Bimi> {

override fun create(parcel: Parcel): Bimi = with(parcel) {
val svgContentUrl = readString()!!
val isCertified = customReadBoolean()

return Bimi(svgContentUrl, isCertified)
}

override fun Bimi.write(parcel: Parcel, flags: Int) = with(parcel) {
writeString(svgContentUrl)
customWriteBoolean(isCertified)
}

private fun Parcel.customWriteBoolean(value: Boolean) {
writeInt(if (value) 1 else 0)
}

private fun Parcel.customReadBoolean(): Boolean = readInt() != 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.infomaniak.mail.data.api.RealmInstantSerializer
import com.infomaniak.mail.data.api.UnwrappingJsonListSerializer
import com.infomaniak.mail.data.cache.mailboxContent.FolderController.Companion.SEARCH_FOLDER_ID
import com.infomaniak.mail.data.models.Attachment
import com.infomaniak.mail.data.models.Bimi
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.calendar.CalendarEventResponse
import com.infomaniak.mail.data.models.correspondent.Recipient
Expand Down Expand Up @@ -97,6 +98,7 @@ class Message : RealmObject {
var size: Int = 0
@SerialName("has_unsubscribe_link")
var hasUnsubscribeLink: Boolean? = null
var bimi : Bimi? = null

// TODO: Those are unused for now, but if we ever want to use them, we need to save them in `Message.keepHeavyData()`.
// If we don't do it now, we'll probably forget to do it in the future.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.infomaniak.mail.MatomoMail.SEARCH_FOLDER_FILTER_NAME
import com.infomaniak.mail.R
import com.infomaniak.mail.data.api.RealmInstantSerializer
import com.infomaniak.mail.data.cache.mailboxContent.FolderController
import com.infomaniak.mail.data.models.Bimi
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.correspondent.Recipient
Expand Down Expand Up @@ -213,7 +214,7 @@ class Thread : RealmObject {
}
}

fun computeAvatarRecipient(): Recipient? = runCatching {
fun computeAvatarRecipient(): Pair<Recipient?, Bimi?> = runCatching {

val message = messages
.lastOrNull { it.folder.role != FolderRole.SENT && it.folder.role != FolderRole.DRAFT }
Expand All @@ -224,7 +225,7 @@ class Thread : RealmObject {
else -> message.from
}

recipients.firstOrNull()
recipients.firstOrNull() to message.bimi

}.getOrElse { throwable ->
Sentry.withScope { scope ->
Expand All @@ -239,7 +240,7 @@ class Thread : RealmObject {
Sentry.captureException(throwable)
}

null
null to null
}

fun computeDisplayedRecipients(): RealmList<Recipient> = when (folder.role) {
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/infomaniak/mail/di/ApplicationModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.app.Application
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import androidx.work.WorkManager
import coil.ImageLoader
import com.infomaniak.lib.stores.AppUpdateScheduler
import com.infomaniak.lib.stores.StoresSettingsRepository
import com.infomaniak.mail.MainApplication
Expand Down Expand Up @@ -74,4 +75,8 @@ object ApplicationModule {
appContext: Context,
workManager: WorkManager,
): AppUpdateScheduler = AppUpdateScheduler(appContext, workManager)

@Provides
@Singleton
fun providesSvgImageLoader(mainApplication: MainApplication): ImageLoader = mainApplication.createSvgImageLoader()
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.core.view.isVisible
import com.infomaniak.lib.core.utils.getAttributes
import com.infomaniak.lib.core.utils.setMarginsRelative
import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.Bimi
import com.infomaniak.mail.data.models.calendar.Attendee
import com.infomaniak.mail.data.models.correspondent.Correspondent
import com.infomaniak.mail.data.models.correspondent.MergedContact
Expand Down Expand Up @@ -71,9 +72,9 @@ class AvatarNameEmailView @JvmOverloads constructor(
}
}

fun setCorrespondent(correspondent: Correspondent) = with(binding) {
userAvatar.loadAvatar(correspondent)
setNameAndEmail(correspondent)
fun setCorrespondent(correspondent: Correspondent, bimi: Bimi? = null) = with(binding) {
userAvatar.loadAvatar(correspondent, bimi)
setNameAndEmail(correspondent, isCorrespondentCertified = bimi?.isCertified ?: false)
}

fun setMergedContact(mergedContact: MergedContact) = with(binding) {
Expand All @@ -86,12 +87,17 @@ class AvatarNameEmailView @JvmOverloads constructor(
setNameAndEmail(attendee)
}

private fun ViewAvatarNameEmailBinding.setNameAndEmail(correspondent: Correspondent) {
private fun ViewAvatarNameEmailBinding.setNameAndEmail(
correspondent: Correspondent,
isCorrespondentCertified: Boolean = false,
) {
val filledSingleField = fillInUserNameAndEmail(correspondent, userName, userEmail, ignoreIsMe = !processNameAndEmail)
if (displayAsAttendee) {
val userNameTextColor = if (filledSingleField) R.style.AvatarNameEmailSecondary else R.style.AvatarNameEmailPrimary
userName.setTextAppearance(userNameTextColor)
}

iconCertified.isVisible = isCorrespondentCertified
}

fun setAutocompleteUnknownContact(searchQuery: String) = with(binding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.LocalSettings.SwipeAction
import com.infomaniak.mail.data.LocalSettings.ThreadDensity
import com.infomaniak.mail.data.api.ApiRoutes
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.correspondent.Recipient
import com.infomaniak.mail.data.models.thread.Thread
Expand Down Expand Up @@ -400,7 +401,12 @@ class ThreadListAdapter @Inject constructor(
}

private fun CardviewThreadItemBinding.displayAvatar(thread: Thread) {
expeditorAvatar.loadAvatar(thread.computeAvatarRecipient())
val (recipient, bimi) = thread.computeAvatarRecipient()
if (bimi?.isCertified == true) {
expeditorAvatar.loadBimiAvatar(ApiRoutes.bimi(bimi.svgContentUrl.toString()), recipient)
} else {
expeditorAvatar.loadAvatar(recipient)
}
}

private fun CardviewThreadItemBinding.formatRecipientNames(recipients: List<Recipient>): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.navArgs
import com.infomaniak.lib.core.utils.safeBinding
Expand Down Expand Up @@ -53,7 +54,11 @@ class DetailedContactBottomSheetDialog : ActionsBottomSheetDialog() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
super.onViewCreated(view, savedInstanceState)
contactDetails.setCorrespondent(navigationArgs.recipient)

val bimi = navigationArgs.bimi
containerInfoCertified.isVisible = bimi?.isCertified == true
contactDetails.setCorrespondent(navigationArgs.recipient, bimi)

setupListeners()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.infomaniak.lib.core.utils.context
import com.infomaniak.mail.MatomoMail.trackMessageEvent
import com.infomaniak.mail.data.models.Bimi
import com.infomaniak.mail.data.models.correspondent.Recipient
import com.infomaniak.mail.databinding.ItemDetailedContactBinding
import com.infomaniak.mail.ui.main.thread.DetailedRecipientAdapter.DetailedRecipientViewHolder
import com.infomaniak.mail.utils.UiUtils.fillInUserNameAndEmail

class DetailedRecipientAdapter(
private val onContactClicked: ((contact: Recipient) -> Unit)?,
private val onContactClicked: ((contact: Recipient, bimi: Bimi?) -> Unit)?,
) : Adapter<DetailedRecipientViewHolder>() {

private var recipients = emptyList<Recipient>()

private var bimi: Bimi? = null
NicolasBourdin88 marked this conversation as resolved.
Show resolved Hide resolved

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailedRecipientViewHolder {
return DetailedRecipientViewHolder(ItemDetailedContactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
Expand All @@ -45,14 +48,15 @@ class DetailedRecipientAdapter(

name.setOnClickListener {
context.trackMessageEvent("selectRecipient")
onContactClicked?.invoke(recipient)
onContactClicked?.invoke(recipient, bimi)
}
}

override fun getItemCount(): Int = recipients.count()

fun updateList(newList: List<Recipient>) {
fun updateList(newList: List<Recipient>, newBimi: Bimi? = null) {
recipients = newList
bimi = newBimi
}

class DetailedRecipientViewHolder(val binding: ItemDetailedContactBinding) : ViewHolder(binding.root)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.infomaniak.lib.core.utils.isNightModeEnabled
import com.infomaniak.mail.MatomoMail.trackMessageEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.Attachment
import com.infomaniak.mail.data.models.Bimi
import com.infomaniak.mail.data.models.calendar.Attendee
import com.infomaniak.mail.data.models.calendar.Attendee.AttendanceState
import com.infomaniak.mail.data.models.correspondent.Recipient
Expand Down Expand Up @@ -337,19 +338,23 @@ class ThreadAdapter(
shortMessageDate.text = ""
} else {
val firstSender = message.sender
userAvatar.loadAvatar(firstSender)

expeditorName.apply {
text = firstSender?.let { context.getPrettyNameAndEmail(it).first }
?: run { context.getString(R.string.unknownRecipientTitle) }
setTextAppearance(R.style.BodyMedium)
}

userAvatar.loadAvatar(firstSender, message.bimi)
iconCertified.isVisible = message.bimi?.isCertified ?: false

shortMessageDate.text = mailFormattedDate(context, messageDate)
}

val listener: OnClickListener? = message.sender?.let { recipient ->
OnClickListener {
context.trackMessageEvent("selectAvatar")
threadAdapterCallbacks?.onContactClicked?.invoke(recipient)
threadAdapterCallbacks?.onContactClicked?.invoke(recipient, message.bimi)
}
}

Expand Down Expand Up @@ -397,7 +402,7 @@ class ThreadAdapter(

private fun MessageViewHolder.bindRecipientDetails(message: Message, messageDate: Date) = with(binding) {

fromAdapter.updateList(message.from.toList())
fromAdapter.updateList(message.from.toList(), message.bimi)
toAdapter.updateList(message.to.toList())

val ccIsNotEmpty = message.cc.isNotEmpty()
Expand Down Expand Up @@ -670,7 +675,7 @@ class ThreadAdapter(

data class ThreadAdapterCallbacks(
var onBodyWebViewFinishedLoading: (() -> Unit)? = null,
var onContactClicked: ((contact: Recipient) -> Unit)? = null,
var onContactClicked: ((contact: Recipient, bimi: Bimi?) -> Unit)? = null,
var onDeleteDraftClicked: ((message: Message) -> Unit)? = null,
var onDraftClicked: ((message: Message) -> Unit)? = null,
var onAttachmentClicked: ((attachment: Attachment) -> Unit)? = null,
Expand Down Expand Up @@ -708,7 +713,7 @@ class ThreadAdapter(
private class MessageViewHolder(
override val binding: ItemMessageBinding,
private val shouldLoadDistantResources: Boolean,
onContactClicked: ((contact: Recipient) -> Unit)?,
onContactClicked: ((contact: Recipient, bimi: Bimi?) -> Unit)?,
onAttachmentClicked: ((attachment: Attachment) -> Unit)?,
onAttachmentOptionsClicked: ((attachment: Attachment) -> Unit)?,
) : ThreadAdapterViewHolder(binding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,10 @@ class ThreadFragment : Fragment() {
override val isCalendarEventExpandedMap by threadState::isCalendarEventExpandedMap
},
threadAdapterCallbacks = ThreadAdapterCallbacks(
onContactClicked = {
onContactClicked = { recipient, bimi ->
safeNavigate(
resId = R.id.detailedContactBottomSheetDialog,
args = DetailedContactBottomSheetDialogArgs(it).toBundle(),
args = DetailedContactBottomSheetDialogArgs(recipient, bimi).toBundle(),
)
},
onDraftClicked = { message ->
Expand Down
Loading
Loading