diff --git a/mail-label/dagger/src/main/kotlin/ch/protonmail/android/maillabel/dagger/MailLabelModule.kt b/mail-label/dagger/src/main/kotlin/ch/protonmail/android/maillabel/dagger/MailLabelModule.kt index 2025a66c4d..9ed4a8b20c 100644 --- a/mail-label/dagger/src/main/kotlin/ch/protonmail/android/maillabel/dagger/MailLabelModule.kt +++ b/mail-label/dagger/src/main/kotlin/ch/protonmail/android/maillabel/dagger/MailLabelModule.kt @@ -22,17 +22,19 @@ import ch.protonmail.android.mailcommon.domain.coroutines.AppScope import ch.protonmail.android.mailcommon.domain.coroutines.IODispatcher import ch.protonmail.android.maillabel.data.MailLabelRustCoroutineScope import ch.protonmail.android.maillabel.data.local.LabelDataSource +import ch.protonmail.android.maillabel.data.local.RustGetLabelIdBySystemLabel +import ch.protonmail.android.maillabel.data.local.RustGetSystemLabelById import ch.protonmail.android.maillabel.data.local.RustLabelDataSource -import ch.protonmail.android.maillabel.data.repository.InMemorySelectedMailLabelIdRepositoryImpl import ch.protonmail.android.maillabel.data.local.RustMailboxFactory +import ch.protonmail.android.maillabel.data.repository.InMemorySelectedMailLabelIdRepositoryImpl import ch.protonmail.android.maillabel.data.repository.RustLabelRepository import ch.protonmail.android.maillabel.data.repository.ViewModeRepositoryImpl import ch.protonmail.android.maillabel.data.usecase.CreateRustSidebar import ch.protonmail.android.maillabel.data.usecase.RustGetAllMailLabelId import ch.protonmail.android.maillabel.domain.repository.LabelRepository import ch.protonmail.android.maillabel.domain.repository.SelectedMailLabelIdRepository -import ch.protonmail.android.maillabel.domain.usecase.FindLocalSystemLabelId import ch.protonmail.android.maillabel.domain.repository.ViewModeRepository +import ch.protonmail.android.maillabel.domain.usecase.FindLocalSystemLabelId import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId import dagger.Module @@ -54,18 +56,23 @@ object MailLabelModule { @MailLabelRustCoroutineScope fun provideLabelRustCoroutineScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + @Suppress("LongParameterList") @Provides @Singleton fun provideRustLabelDataSource( userSessionRepository: UserSessionRepository, createRustSidebar: CreateRustSidebar, rustGetAllMailLabelId: RustGetAllMailLabelId, + rustGetSystemLabelById: RustGetSystemLabelById, + rustGetLabelIdBySystemLabel: RustGetLabelIdBySystemLabel, @MailLabelRustCoroutineScope coroutineScope: CoroutineScope, @IODispatcher ioDispatcher: CoroutineDispatcher ): LabelDataSource = RustLabelDataSource( userSessionRepository, createRustSidebar, rustGetAllMailLabelId, + rustGetSystemLabelById, + rustGetLabelIdBySystemLabel, coroutineScope, ioDispatcher ) diff --git a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/LabelDataSource.kt b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/LabelDataSource.kt index ce4fba26e2..a75794b696 100644 --- a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/LabelDataSource.kt +++ b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/LabelDataSource.kt @@ -20,12 +20,14 @@ package ch.protonmail.android.maillabel.data.local import arrow.core.Either import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId +import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel import ch.protonmail.android.mailcommon.domain.model.DataError import kotlinx.coroutines.flow.Flow import me.proton.core.domain.entity.UserId import uniffi.proton_mail_uniffi.SidebarCustomFolder import uniffi.proton_mail_uniffi.SidebarCustomLabel import uniffi.proton_mail_uniffi.SidebarSystemLabel +import uniffi.proton_mail_uniffi.SystemLabel interface LabelDataSource { @@ -37,4 +39,7 @@ interface LabelDataSource { suspend fun getAllMailLabelId(userId: UserId): Either + suspend fun resolveSystemLabelByLocalId(userId: UserId, labelId: LocalLabelId): Either + + suspend fun resolveLocalIdBySystemLabel(userId: UserId, systemLabel: SystemLabel): Either } diff --git a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustGetLabelIdBySystemLabel.kt b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustGetLabelIdBySystemLabel.kt new file mode 100644 index 0000000000..12ff740c64 --- /dev/null +++ b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustGetLabelIdBySystemLabel.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 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 . + */ + +package ch.protonmail.android.maillabel.data.local + +import arrow.core.Either +import arrow.core.raise.either +import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId +import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel +import ch.protonmail.android.mailcommon.data.mapper.toDataError +import ch.protonmail.android.mailcommon.domain.model.DataError +import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper +import uniffi.proton_mail_uniffi.ResolveSystemLabelIdResult +import uniffi.proton_mail_uniffi.resolveSystemLabelId +import javax.inject.Inject + +class RustGetLabelIdBySystemLabel @Inject constructor() { + + suspend operator fun invoke( + mailUserSession: MailUserSessionWrapper, + labelId: LocalSystemLabel + ): Either = either { + when (val result = resolveSystemLabelId(mailUserSession.getRustUserSession(), labelId)) { + is ResolveSystemLabelIdResult.Error -> raise(result.v1.toDataError()) + is ResolveSystemLabelIdResult.Ok -> result.v1 ?: raise(DataError.Local.NoDataCached) + } + } +} diff --git a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustGetSystemLabelById.kt b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustGetSystemLabelById.kt new file mode 100644 index 0000000000..d09e5a91e3 --- /dev/null +++ b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustGetSystemLabelById.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 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 . + */ + +package ch.protonmail.android.maillabel.data.local + +import arrow.core.Either +import arrow.core.raise.either +import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId +import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel +import ch.protonmail.android.mailcommon.data.mapper.toDataError +import ch.protonmail.android.mailcommon.domain.model.DataError +import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper +import uniffi.proton_mail_uniffi.ResolveSystemLabelByIdResult +import uniffi.proton_mail_uniffi.resolveSystemLabelById +import javax.inject.Inject + +class RustGetSystemLabelById @Inject constructor() { + + suspend operator fun invoke( + mailUserSession: MailUserSessionWrapper, + labelId: LocalLabelId + ): Either = either { + when (val result = resolveSystemLabelById(mailUserSession.getRustUserSession(), labelId)) { + is ResolveSystemLabelByIdResult.Error -> raise(result.v1.toDataError()) + is ResolveSystemLabelByIdResult.Ok -> result.v1 ?: raise(DataError.Local.NoDataCached) + } + } +} diff --git a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSource.kt b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSource.kt index 498af72c3b..c6c5f159d9 100644 --- a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSource.kt +++ b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSource.kt @@ -21,6 +21,7 @@ package ch.protonmail.android.maillabel.data.local import arrow.core.Either import arrow.core.left import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId +import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel import ch.protonmail.android.mailcommon.domain.coroutines.IODispatcher import ch.protonmail.android.mailcommon.domain.model.DataError import ch.protonmail.android.maillabel.data.MailLabelRustCoroutineScope @@ -51,6 +52,8 @@ class RustLabelDataSource @Inject constructor( private val userSessionRepository: UserSessionRepository, private val createRustSidebar: CreateRustSidebar, private val rustGetAllMailLabelId: RustGetAllMailLabelId, + private val rustGetSystemLabelById: RustGetSystemLabelById, + private val rustGetLabelIdBySystemLabel: RustGetLabelIdBySystemLabel, @MailLabelRustCoroutineScope private val coroutineScope: CoroutineScope, @IODispatcher private val ioDispatcher: CoroutineDispatcher ) : LabelDataSource { @@ -152,4 +155,36 @@ class RustLabelDataSource @Inject constructor( } return@withContext rustGetAllMailLabelId(session) } + + override suspend fun resolveSystemLabelByLocalId( + userId: UserId, + labelId: LocalLabelId + ): Either { + return withContext(ioDispatcher) { + val session = userSessionRepository.getUserSession(userId) + + if (session == null) { + Timber.e("rust-label: trying to resolve system label by local id with null session.") + return@withContext DataError.Local.NoUserSession.left() + } + + rustGetSystemLabelById(session, labelId) + } + } + + override suspend fun resolveLocalIdBySystemLabel( + userId: UserId, + systemLabel: LocalSystemLabel + ): Either { + return withContext(ioDispatcher) { + val session = userSessionRepository.getUserSession(userId) + + if (session == null) { + Timber.e("rust-label: trying to resolve local id by system label with null session.") + return@withContext DataError.Local.NoUserSession.left() + } + + rustGetLabelIdBySystemLabel(session, systemLabel) + } + } } diff --git a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/mapper/LabelMapper.kt b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/mapper/LabelMapper.kt index 419b1ead7d..743696b820 100644 --- a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/mapper/LabelMapper.kt +++ b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/mapper/LabelMapper.kt @@ -125,6 +125,23 @@ fun LocalSystemLabel.toSystemLabel() = when (this) { } } +fun SystemLabelId.toLocalSystemLabel() = when (this) { + SystemLabelId.Inbox -> LocalSystemLabel.INBOX + SystemLabelId.AllDrafts -> LocalSystemLabel.ALL_DRAFTS + SystemLabelId.AllSent -> LocalSystemLabel.ALL_SENT + SystemLabelId.Trash -> LocalSystemLabel.TRASH + SystemLabelId.Spam -> LocalSystemLabel.SPAM + SystemLabelId.AllMail -> LocalSystemLabel.ALL_MAIL + SystemLabelId.Archive -> LocalSystemLabel.ARCHIVE + SystemLabelId.Sent -> LocalSystemLabel.SENT + SystemLabelId.Drafts -> LocalSystemLabel.DRAFTS + SystemLabelId.Outbox -> LocalSystemLabel.OUTBOX + SystemLabelId.Starred -> LocalSystemLabel.STARRED + SystemLabelId.AllScheduled -> LocalSystemLabel.SCHEDULED + SystemLabelId.AlmostAllMail -> LocalSystemLabel.ALMOST_ALL_MAIL + SystemLabelId.Snoozed -> LocalSystemLabel.SNOOZED +} + fun MovableSystemFolder.toSystemLabel() = when (this) { MovableSystemFolder.INBOX -> SystemLabelId.Inbox MovableSystemFolder.TRASH -> SystemLabelId.Trash diff --git a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/repository/RustLabelRepository.kt b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/repository/RustLabelRepository.kt index 365fd540eb..afd354bf2b 100644 --- a/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/repository/RustLabelRepository.kt +++ b/mail-label/data/src/main/kotlin/ch/protonmail/android/maillabel/data/repository/RustLabelRepository.kt @@ -18,15 +18,21 @@ package ch.protonmail.android.maillabel.data.repository +import arrow.core.Either +import ch.protonmail.android.mailcommon.domain.model.DataError import ch.protonmail.android.maillabel.data.local.LabelDataSource import ch.protonmail.android.maillabel.data.mapper.toLabel import ch.protonmail.android.maillabel.data.mapper.toLabelId import ch.protonmail.android.maillabel.data.mapper.toLabelWithSystemLabelId +import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId +import ch.protonmail.android.maillabel.data.mapper.toLocalSystemLabel +import ch.protonmail.android.maillabel.data.mapper.toSystemLabel import ch.protonmail.android.maillabel.domain.model.Label import ch.protonmail.android.maillabel.domain.model.LabelId import ch.protonmail.android.maillabel.domain.model.LabelType import ch.protonmail.android.maillabel.domain.model.LabelWithSystemLabelId import ch.protonmail.android.maillabel.domain.model.NewLabel +import ch.protonmail.android.maillabel.domain.model.SystemLabelId import ch.protonmail.android.maillabel.domain.repository.LabelRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull @@ -147,6 +153,20 @@ class RustLabelRepository @Inject constructor( TODO("Not yet implemented") } + override suspend fun resolveSystemLabel(userId: UserId, labelId: LabelId): Either { + return labelDataSource.resolveSystemLabelByLocalId(userId = userId, labelId = labelId.toLocalLabelId()).map { + it.toSystemLabel() + } + } + + override suspend fun resolveLocalIdBySystemLabel( + userId: UserId, + labelId: SystemLabelId + ): Either { + return labelDataSource.resolveLocalIdBySystemLabel(userId = userId, systemLabel = labelId.toLocalSystemLabel()) + .map { it.toLabelId() } + } + private fun Flow>.convertToDataResultFlow(): Flow>> { return this.map { labels -> if (labels.isNotEmpty()) { diff --git a/mail-label/data/src/test/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSourceTest.kt b/mail-label/data/src/test/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSourceTest.kt index 21aa982c11..1e2d499012 100644 --- a/mail-label/data/src/test/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSourceTest.kt +++ b/mail-label/data/src/test/kotlin/ch/protonmail/android/maillabel/data/local/RustLabelDataSourceTest.kt @@ -20,9 +20,13 @@ package ch.protonmail.android.maillabel.data.local import app.cash.turbine.test import arrow.core.right +import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId +import ch.protonmail.android.mailcommon.data.mapper.LocalSystemLabel +import ch.protonmail.android.maillabel.data.mapper.toLocalLabelId import ch.protonmail.android.maillabel.data.usecase.CreateRustSidebar import ch.protonmail.android.maillabel.data.usecase.RustGetAllMailLabelId import ch.protonmail.android.maillabel.data.wrapper.SidebarWrapper +import ch.protonmail.android.maillabel.domain.model.LabelId import ch.protonmail.android.mailsession.domain.repository.UserSessionRepository import ch.protonmail.android.mailsession.domain.wrapper.MailUserSessionWrapper import ch.protonmail.android.test.utils.rule.LoggingTestRule @@ -32,6 +36,7 @@ import ch.protonmail.android.testdata.user.UserIdTestData import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.confirmVerified import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -44,7 +49,7 @@ import uniffi.proton_mail_uniffi.LiveQueryCallback import uniffi.proton_mail_uniffi.WatchHandle import kotlin.test.assertEquals -class RustLabelDataSourceTest { +internal class RustLabelDataSourceTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @@ -56,11 +61,15 @@ class RustLabelDataSourceTest { private val testCoroutineScope = CoroutineScope(mainDispatcherRule.testDispatcher) private val createRustSidebar = mockk() private val rustGetAllMailLabelId = mockk() + private val rustGetSystemLabelById = mockk() + private val rustGetLabelIdBySystemLabel = mockk() private val labelDataSource = RustLabelDataSource( userSessionRepository, createRustSidebar, rustGetAllMailLabelId, + rustGetSystemLabelById, + rustGetLabelIdBySystemLabel, testCoroutineScope, mainDispatcherRule.testDispatcher ) @@ -348,4 +357,45 @@ class RustLabelDataSourceTest { coVerify { sidebarMock.destroy() } } } + + @Test + fun `resolve system label by local id calls the UC with the user session and label`() = runTest { + // Given + val userId = UserIdTestData.userId + val userSessionMock = mockk() + val labelId = LabelId("1").toLocalLabelId() + coEvery { userSessionRepository.getUserSession(userId) } returns userSessionMock + coEvery { + rustGetSystemLabelById(userSessionMock, labelId) + } returns LocalSystemLabel.INBOX.right() + + // When + val actual = labelDataSource.resolveSystemLabelByLocalId(userId, labelId) + + // Then + assertEquals(LocalSystemLabel.INBOX.right(), actual) + coVerify(exactly = 1) { rustGetSystemLabelById(userSessionMock, labelId) } + confirmVerified(rustGetLabelIdBySystemLabel) + } + + @Test + fun `resolve local id by system label calls the UC with the user session and system label`() = runTest { + // Given + val userId = UserIdTestData.userId + val userSessionMock = mockk() + val systemLabel = LocalSystemLabel.INBOX + val expectedLocalLabelId = LocalLabelId(1u) + coEvery { userSessionRepository.getUserSession(userId) } returns userSessionMock + coEvery { + rustGetLabelIdBySystemLabel(userSessionMock, systemLabel) + } returns expectedLocalLabelId.right() + + // When + val actual = labelDataSource.resolveLocalIdBySystemLabel(userId, systemLabel) + + // Then + assertEquals(expectedLocalLabelId.right(), actual) + coVerify(exactly = 1) { rustGetLabelIdBySystemLabel(userSessionMock, systemLabel) } + confirmVerified(rustGetLabelIdBySystemLabel) + } } diff --git a/mail-label/domain/src/main/kotlin/ch/protonmail/android/maillabel/domain/repository/LabelRepository.kt b/mail-label/domain/src/main/kotlin/ch/protonmail/android/maillabel/domain/repository/LabelRepository.kt index 55c5deb8d1..0ab413180c 100644 --- a/mail-label/domain/src/main/kotlin/ch/protonmail/android/maillabel/domain/repository/LabelRepository.kt +++ b/mail-label/domain/src/main/kotlin/ch/protonmail/android/maillabel/domain/repository/LabelRepository.kt @@ -18,11 +18,14 @@ package ch.protonmail.android.maillabel.domain.repository +import arrow.core.Either +import ch.protonmail.android.mailcommon.domain.model.DataError import ch.protonmail.android.maillabel.domain.model.Label import ch.protonmail.android.maillabel.domain.model.LabelId import ch.protonmail.android.maillabel.domain.model.LabelType import ch.protonmail.android.maillabel.domain.model.LabelWithSystemLabelId import ch.protonmail.android.maillabel.domain.model.NewLabel +import ch.protonmail.android.maillabel.domain.model.SystemLabelId import kotlinx.coroutines.flow.Flow import me.proton.core.domain.arch.DataResult import me.proton.core.domain.entity.UserId @@ -103,6 +106,11 @@ interface LabelRepository { labelId: LabelId ) + suspend fun resolveSystemLabel(userId: UserId, labelId: LabelId): Either + + suspend fun resolveLocalIdBySystemLabel(userId: UserId, labelId: SystemLabelId): Either + + /** * Mark local data as stale for [userId], by [type]. * diff --git a/mail-label/domain/src/main/kotlin/ch/protonmail/android/maillabel/domain/usecase/ResolveSystemLabelId.kt b/mail-label/domain/src/main/kotlin/ch/protonmail/android/maillabel/domain/usecase/ResolveSystemLabelId.kt new file mode 100644 index 0000000000..9b6003cd9d --- /dev/null +++ b/mail-label/domain/src/main/kotlin/ch/protonmail/android/maillabel/domain/usecase/ResolveSystemLabelId.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 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 . + */ + +package ch.protonmail.android.maillabel.domain.usecase + +import ch.protonmail.android.maillabel.domain.model.LabelId +import ch.protonmail.android.maillabel.domain.repository.LabelRepository +import me.proton.core.domain.entity.UserId +import javax.inject.Inject + +class ResolveSystemLabelId @Inject constructor(private val labelRepository: LabelRepository) { + + suspend operator fun invoke(userId: UserId, labelId: LabelId) = labelRepository.resolveSystemLabel(userId, labelId) +}