Respect the active theme when loading sender image

ET-4901
This commit is contained in:
Niccolò Forlini
2026-04-16 09:58:35 +02:00
parent 88b2d72d41
commit 3ace2ea497
13 changed files with 159 additions and 32 deletions
@@ -23,11 +23,13 @@ import ch.protonmail.android.mailcommon.data.repository.UndoRepositoryImpl
import ch.protonmail.android.mailcommon.data.system.BuildVersionProviderImpl
import ch.protonmail.android.mailcommon.data.system.ContentValuesProviderImpl
import ch.protonmail.android.mailcommon.data.system.DeviceCapabilitiesImpl
import ch.protonmail.android.mailcommon.data.system.SenderImageModeProviderImpl
import ch.protonmail.android.mailcommon.domain.network.NetworkManager
import ch.protonmail.android.mailcommon.domain.repository.UndoRepository
import ch.protonmail.android.mailcommon.domain.system.BuildVersionProvider
import ch.protonmail.android.mailcommon.domain.system.ContentValuesProvider
import ch.protonmail.android.mailcommon.domain.system.DeviceCapabilities
import ch.protonmail.android.mailcommon.domain.usecase.SenderImageModeProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -56,5 +58,8 @@ object MailCommonDataModule {
@Binds
fun bindUndoRepository(impl: UndoRepositoryImpl): UndoRepository
@Binds
fun bindSenderImageModeProvider(impl: SenderImageModeProviderImpl): SenderImageModeProvider
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailcommon.data.system
import android.content.Context
import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailcommon.domain.usecase.SenderImageModeProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class SenderImageModeProviderImpl @Inject constructor(
@ApplicationContext private val applicationContext: Context
) : SenderImageModeProvider {
override operator fun invoke(): SenderImageTheme {
val isDark = when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_YES -> true
AppCompatDelegate.MODE_NIGHT_NO -> false
else -> applicationContext.resources.configuration.let {
it.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
}
return if (isDark) SenderImageTheme.Dark else SenderImageTheme.Light
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailcommon.domain.model
enum class SenderImageTheme(val value: String) {
Dark("dark"),
Light("light")
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailcommon.domain.usecase
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
interface SenderImageModeProvider {
operator fun invoke(): SenderImageTheme
}
@@ -25,6 +25,7 @@ import ch.protonmail.android.mailcommon.data.mapper.LocalMessageId
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageMetadata
import ch.protonmail.android.mailcommon.data.mapper.RemoteMessageId
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailcommon.domain.model.UndoSendError
import ch.protonmail.android.mailcommon.domain.model.UndoableOperation
import ch.protonmail.android.mailmessage.domain.model.MessageId
@@ -62,7 +63,8 @@ interface RustMessageDataSource {
suspend fun getSenderImage(
userId: UserId,
address: String,
bimi: String?
bimi: String?,
mode: SenderImageTheme
): String?
suspend fun markRead(userId: UserId, messages: List<LocalMessageId>): Either<DataError, Unit>
@@ -28,6 +28,7 @@ import ch.protonmail.android.mailcommon.data.mapper.RemoteMessageId
import ch.protonmail.android.mailcommon.domain.annotation.MissingRustApi
import ch.protonmail.android.mailcommon.domain.coroutines.IODispatcher
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailcommon.domain.model.UndoSendError
import ch.protonmail.android.mailcommon.domain.model.UndoableOperation
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
@@ -166,7 +167,8 @@ class RustMessageDataSourceImpl @Inject constructor(
override suspend fun getSenderImage(
userId: UserId,
address: String,
bimi: String?
bimi: String?,
mode: SenderImageTheme
): String? = withContext(ioDispatcher) {
val session = userSessionRepository.getUserSession(userId)
if (session == null) {
@@ -174,7 +176,7 @@ class RustMessageDataSourceImpl @Inject constructor(
return@withContext null
}
return@withContext getRustSenderImage(session, address, bimi)
return@withContext getRustSenderImage(session, address, bimi, mode)
.onLeft { Timber.d("rust-message: Failed to get sender image $it") }
.getOrNull()
}
@@ -23,6 +23,7 @@ import arrow.core.Either
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
import ch.protonmail.android.mailcommon.domain.model.CursorId
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailcommon.domain.model.UndoSendError
import ch.protonmail.android.mailcommon.domain.repository.ConversationCursor
import ch.protonmail.android.mailcommon.domain.repository.UndoRepository
@@ -72,9 +73,10 @@ class RustMessageRepositoryImpl @Inject constructor(
override suspend fun getSenderImage(
userId: UserId,
address: String,
bimi: String?
bimi: String?,
mode: SenderImageTheme
): SenderImage? {
return rustMessageDataSource.getSenderImage(userId, address, bimi)?.let { imageString ->
return rustMessageDataSource.getSenderImage(userId, address, bimi, mode)?.let { imageString ->
SenderImage(File(imageString))
}
}
@@ -23,6 +23,7 @@ import arrow.core.left
import arrow.core.right
import ch.protonmail.android.mailcommon.data.mapper.toDataError
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper
import uniffi.mail_uniffi.MailUserSessionImageForSenderResult
import javax.inject.Inject
@@ -32,8 +33,9 @@ class GetRustSenderImage @Inject constructor() {
suspend operator fun invoke(
mailUserSession: MailUserSessionWrapper,
address: String,
bimi: String?
): Either<DataError, String> = when (val result = mailUserSession.imageForSender(address, bimi)) {
bimi: String?,
mode: SenderImageTheme
): Either<DataError, String> = when (val result = mailUserSession.imageForSender(address, bimi, mode)) {
is MailUserSessionImageForSenderResult.Error -> result.v1.toDataError().left()
is MailUserSessionImageForSenderResult.Ok -> {
when (val image = result.v1) {
@@ -24,6 +24,7 @@ import arrow.core.right
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageId
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailcommon.domain.model.UndoSendError
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId
@@ -213,16 +214,17 @@ internal class RustMessageDataSourceImplTest {
val mailSession = mockk<MailUserSessionWrapper>()
val address = "test@example.com"
val bimi = "bimiSelector"
val mode = SenderImageTheme.Light
val expectedImage = "image.png"
coEvery { userSessionRepository.getUserSession(userId) } returns mailSession
coEvery { getRustSenderImage(mailSession, address, bimi) } returns expectedImage.right()
coEvery { getRustSenderImage(mailSession, address, bimi, mode) } returns expectedImage.right()
// When
val result = dataSource.getSenderImage(userId, address, bimi)
val result = dataSource.getSenderImage(userId, address, bimi, mode)
// Then
coVerify { getRustSenderImage(mailSession, address, bimi) }
coVerify { getRustSenderImage(mailSession, address, bimi, mode) }
assertEquals(expectedImage, result)
}
@@ -232,14 +234,15 @@ internal class RustMessageDataSourceImplTest {
val userId = UserIdTestData.userId
val address = "test@example.com"
val bimi = "bimiSelector"
val mode = SenderImageTheme.Light
coEvery { userSessionRepository.getUserSession(userId) } returns null
// When
val result = dataSource.getSenderImage(userId, address, bimi)
val result = dataSource.getSenderImage(userId, address, bimi, mode)
// Then
coVerify(exactly = 0) { getRustSenderImage(any(), any(), any()) }
coVerify(exactly = 0) { getRustSenderImage(any(), any(), any(), any()) }
assertNull(result)
}
@@ -250,18 +253,20 @@ internal class RustMessageDataSourceImplTest {
val mailSession = mockk<MailUserSessionWrapper>()
val address = "test@example.com"
val bimi = "bimiSelector"
val mode = SenderImageTheme.Light
coEvery { userSessionRepository.getUserSession(userId) } returns mailSession
coEvery {
getRustSenderImage(
mailSession,
address,
bimi
bimi,
mode
)
} returns DataError.Local.CryptoError.left()
// When
val result = dataSource.getSenderImage(userId, address, bimi)
val result = dataSource.getSenderImage(userId, address, bimi, mode)
// Then
assertNull(result)
@@ -27,6 +27,7 @@ import ch.protonmail.android.mailcommon.domain.model.ConversationId
import ch.protonmail.android.mailcommon.domain.model.CursorId
import ch.protonmail.android.mailcommon.domain.model.CursorResult
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailcommon.domain.model.UndoableOperation
import ch.protonmail.android.mailcommon.domain.repository.ConversationCursor
import ch.protonmail.android.mailcommon.domain.repository.UndoRepository
@@ -240,16 +241,17 @@ internal class RustMessageRepositoryImplTest {
val userId = UserIdTestData.userId
val address = "test@example.com"
val bimi = "bimiSelector"
val mode = SenderImageTheme.Light
val imagePath = "image.png"
val expectedSenderImage = SenderImage(File(imagePath))
coEvery { rustMessageDataSource.getSenderImage(userId, address, bimi) } returns imagePath
coEvery { rustMessageDataSource.getSenderImage(userId, address, bimi, mode) } returns imagePath
// When
val result = repository.getSenderImage(userId, address, bimi)
val result = repository.getSenderImage(userId, address, bimi, mode)
// Then
coVerify { rustMessageDataSource.getSenderImage(userId, address, bimi) }
coVerify { rustMessageDataSource.getSenderImage(userId, address, bimi, mode) }
assertEquals(expectedSenderImage, result)
}
@@ -259,14 +261,15 @@ internal class RustMessageRepositoryImplTest {
val userId = UserIdTestData.userId
val address = "test@example.com"
val bimi = "bimiSelector"
val mode = SenderImageTheme.Light
coEvery { rustMessageDataSource.getSenderImage(userId, address, bimi) } returns null
coEvery { rustMessageDataSource.getSenderImage(userId, address, bimi, mode) } returns null
// When
val result = repository.getSenderImage(userId, address, bimi)
val result = repository.getSenderImage(userId, address, bimi, mode)
// Then
coVerify { rustMessageDataSource.getSenderImage(userId, address, bimi) }
coVerify { rustMessageDataSource.getSenderImage(userId, address, bimi, mode) }
assertNull(result)
}
@@ -22,6 +22,7 @@ import arrow.core.Either
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
import ch.protonmail.android.mailcommon.domain.model.CursorId
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailcommon.domain.model.UndoSendError
import ch.protonmail.android.mailcommon.domain.repository.ConversationCursor
import ch.protonmail.android.maillabel.domain.model.LabelId
@@ -49,7 +50,8 @@ interface MessageRepository {
suspend fun getSenderImage(
userId: UserId,
address: String,
bimi: String?
bimi: String?,
mode: SenderImageTheme
): SenderImage?
/**
@@ -18,19 +18,22 @@
package ch.protonmail.android.mailmessage.domain.usecase
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
import ch.protonmail.android.mailcommon.domain.usecase.SenderImageModeProvider
import ch.protonmail.android.mailmessage.domain.model.SenderImage
import ch.protonmail.android.mailmessage.domain.repository.MessageRepository
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
class GetSenderImage @Inject constructor(
private val observePrimaryUserId: ObservePrimaryUserId,
private val messageRepository: MessageRepository
private val messageRepository: MessageRepository,
private val senderImageModeProvider: SenderImageModeProvider
) {
suspend operator fun invoke(address: String, bimiSelector: String?): SenderImage? {
val mode = senderImageModeProvider()
return observePrimaryUserId().firstOrNull()?.let { userId ->
messageRepository.getSenderImage(userId, address, bimiSelector)
messageRepository.getSenderImage(userId, address, bimiSelector, mode)
}
}
}
@@ -24,6 +24,7 @@ import arrow.core.right
import ch.protonmail.android.mailcommon.data.mapper.LocalAttachmentId
import ch.protonmail.android.mailcommon.data.mapper.toDataError
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.domain.model.SenderImageTheme
import ch.protonmail.android.mailsession.domain.mapper.toEventLoopError
import ch.protonmail.android.mailsession.domain.model.EventLoopError
import timber.log.Timber
@@ -33,6 +34,7 @@ import uniffi.mail_uniffi.ExecuteWhenOnlineCallbackAsync
import uniffi.mail_uniffi.Fork
import uniffi.mail_uniffi.MailUserSession
import uniffi.mail_uniffi.MailUserSessionForkResult
import uniffi.mail_uniffi.MailUserSessionImageForSenderResult
import uniffi.mail_uniffi.MailUserSessionOverrideUserFeatureFlagResult
import uniffi.mail_uniffi.MailUserSessionUserResult
import uniffi.mail_uniffi.MeasurementEventType
@@ -61,14 +63,20 @@ class MailUserSessionWrapper(private val userSession: MailUserSession) {
VoidEventResult.Ok -> Unit.right()
}
suspend fun imageForSender(address: String, bimi: String?) = userSession.imageForSender(
address,
bimi,
true,
SenderImageSize.S128,
null,
"png"
)
suspend fun imageForSender(
address: String,
bimi: String?,
mode: SenderImageTheme
): MailUserSessionImageForSenderResult {
return userSession.imageForSender(
address,
bimi,
true,
SenderImageSize.S128,
mode.value,
"png"
)
}
suspend fun getAttachment(attachmentId: LocalAttachmentId) = userSession.getAttachment(attachmentId)