Enable switching active category and updating mailbox content accordingly.

- Allow selecting a new active category from mailbox
- Propagate category selection through UI, vm and domain layers
- Update Rust scroller to reflect active category change
- Handle errors and provide user feedback on failure

ET-6046
This commit is contained in:
Serdar Ozturk
2026-04-28 15:54:16 +01:00
parent 693aeab3fa
commit 601131afb0
25 changed files with 257 additions and 5 deletions
@@ -51,6 +51,8 @@ fun LocalCategoryLabel.toCategoryLabel(): CategoryLabel {
fun LocalCategoryLabelId.toCategoryLabelId(): CategoryLabelId = CategoryLabelId(this.value.toString())
fun CategoryLabelId.toLocalCategoryLabelId(): LocalCategoryLabelId = LocalCategoryLabelId(this.id.toULong())
fun ConversationScrollerCategoryViewResult.toCategoryViewStatus(): CategoryViewStatus = when (this) {
is ConversationScrollerCategoryViewResult.Ok -> {
v1.toCategoryViewStatus()
@@ -49,3 +49,5 @@ fun CategoryLabel.toUiModel(): CategoryItemUiModel {
}
fun CategoryLabelId.toUiModel(): CategoryLabelIdUiModel = CategoryLabelIdUiModel(id = id)
fun CategoryLabelIdUiModel.toDomainModel(): CategoryLabelId = CategoryLabelId(id = id)
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailconversation.data.local
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalConversation
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
@@ -132,4 +133,6 @@ interface RustConversationDataSource {
fun observeScrollerFetchNewStatus(): Flow<ConversationScrollerFetchNewStatus>
fun observeCategoryViewStatus(): Flow<CategoryViewStatus>
fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit>
}
@@ -21,6 +21,7 @@ package ch.protonmail.android.mailconversation.data.local
import arrow.core.Either
import arrow.core.left
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalConversation
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
@@ -345,4 +346,6 @@ class RustConversationDataSourceImpl @Inject constructor(
override fun observeCategoryViewStatus(): Flow<CategoryViewStatus> =
rustConversationsQuery.observeCategoryViewStatus()
override fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit> =
rustConversationsQuery.setActiveCategoryLabel(categoryLabelId)
}
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailconversation.data.local
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalConversation
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
import ch.protonmail.android.mailconversation.data.wrapper.ConversationCursorWrapper
@@ -53,4 +54,5 @@ interface RustConversationsQuery {
fun observeScrollerFetchNewStatus(): Flow<ConversationScrollerStatusUpdate>
fun observeCategoryViewStatus(): Flow<CategoryViewStatus>
fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit>
}
@@ -22,6 +22,7 @@ import arrow.core.Either
import arrow.core.left
import ch.protonmail.android.mailcategory.data.mapper.toCategoryViewStatus
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalConversation
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
import ch.protonmail.android.mailcommon.domain.model.DataError
@@ -298,6 +299,10 @@ class RustConversationsQueryImpl @Inject constructor(
override fun observeCategoryViewStatus(): Flow<CategoryViewStatus> = categoryViewStatusFlow.filterNotNull()
override fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit> =
paginatorState?.paginatorWrapper?.changeCategoryView(categoryLabelId)
?: PaginationError.Other(DataError.Local.IllegalStateError).left()
private fun destroy() {
if (paginatorState == null) {
Timber.d("rust-conversation-query: no paginator to destroy")
@@ -21,6 +21,8 @@ package ch.protonmail.android.mailconversation.data.repository
import arrow.core.Either
import arrow.core.flatMap
import arrow.core.right
import ch.protonmail.android.mailcategory.data.mapper.toLocalCategoryLabelId
import ch.protonmail.android.mailcategory.domain.model.CategoryLabelId
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
import ch.protonmail.android.mailcommon.domain.model.ConversationId
@@ -245,4 +247,7 @@ class RustConversationRepositoryImpl @Inject constructor(
override fun observeCategoryViewStatus(): Flow<CategoryViewStatus> =
rustConversationDataSource.observeCategoryViewStatus()
override fun setActiveCategoryLabel(categoryLabelId: CategoryLabelId): Either<PaginationError, Unit> =
rustConversationDataSource.setActiveCategoryLabel(categoryLabelId.toLocalCategoryLabelId())
}
@@ -23,11 +23,13 @@ import arrow.core.left
import arrow.core.right
import ch.protonmail.android.mailcategory.data.mapper.toCategoryViewStatus
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
import ch.protonmail.android.mailpagination.data.mapper.toPaginationError
import ch.protonmail.android.mailpagination.domain.model.PaginationError
import timber.log.Timber
import uniffi.mail_uniffi.ConversationScroller
import uniffi.mail_uniffi.ConversationScrollerChangeCategoryViewResult
import uniffi.mail_uniffi.ConversationScrollerCursorResult
import uniffi.mail_uniffi.ConversationScrollerFetchMoreResult
import uniffi.mail_uniffi.ConversationScrollerGetItemsResult
@@ -83,4 +85,10 @@ class ConversationPaginatorWrapper(private val rustPaginator: ConversationScroll
fun getScrollerId(): String = rustPaginator.id()
suspend fun getCategoryViewStatus(): CategoryViewStatus = rustPaginator.categoryView().toCategoryViewStatus()
fun changeCategoryView(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit> =
when (val result = rustPaginator.changeCategoryView(categoryLabelId)) {
is ConversationScrollerChangeCategoryViewResult.Error -> result.v1.toPaginationError().left()
is ConversationScrollerChangeCategoryViewResult.Ok -> Unit.right()
}
}
@@ -19,6 +19,7 @@
package ch.protonmail.android.mailconversation.domain.repository
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryLabelId
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
import ch.protonmail.android.mailcommon.domain.model.ConversationId
@@ -124,4 +125,6 @@ interface ConversationRepository {
fun observeScrollerFetchNewStatus(): Flow<ConversationScrollerFetchNewStatus>
fun observeCategoryViewStatus(): Flow<CategoryViewStatus>
fun setActiveCategoryLabel(categoryLabelId: CategoryLabelId): Either<PaginationError, Unit>
}
@@ -107,6 +107,7 @@ import ch.protonmail.android.mailattachments.presentation.model.FileContent
import ch.protonmail.android.mailattachments.presentation.ui.OpenAttachmentInput
import ch.protonmail.android.mailattachments.presentation.ui.fileOpener
import ch.protonmail.android.mailattachments.presentation.ui.fileSaver
import ch.protonmail.android.mailcategory.presentation.model.CategoryItemUiModel
import ch.protonmail.android.mailcommon.presentation.AdaptivePreviews
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
import ch.protonmail.android.mailcommon.presentation.ConsumableTextEffect
@@ -288,7 +289,8 @@ fun MailboxScreen(
onClearAllConfirmed = { viewModel.submit(MailboxViewAction.ClearAllConfirmed) },
onClearAllDismissed = { viewModel.submit(MailboxViewAction.ClearAllDismissed) },
onSnooze = { viewModel.submit(MailboxViewAction.RequestSnoozeBottomSheet) },
validateUserSession = { viewModel.submit(MailboxViewAction.ValidateUserSession) }
validateUserSession = { viewModel.submit(MailboxViewAction.ValidateUserSession) },
onCategoryItemClicked = { viewModel.submit(MailboxViewAction.OnCategoryItemClicked(it)) }
)
val lifecycle = LocalLifecycleOwner.current
@@ -458,7 +460,7 @@ fun MailboxScreen(
onSpamTrashFilterDisabled = actions.onDisableSpamTrashFilter,
onSelectAllClicked = actions.onSelectAllClicked,
onDeselectAllClicked = actions.onDeselectAllClicked,
onCategoryItemClicked = {}
onCategoryItemClicked = actions.onCategoryItemClicked
)
val fileSaver = fileSaver(
@@ -1211,7 +1213,8 @@ object MailboxScreen {
val onSnooze: () -> Unit,
val onCustomizeToolbar: () -> Unit,
val validateUserSession: () -> Unit,
val onShowRatingBooster: () -> Unit
val onShowRatingBooster: () -> Unit,
val onCategoryItemClicked: (CategoryItemUiModel) -> Unit
) {
companion object {
@@ -1275,7 +1278,8 @@ object MailboxScreen {
onSnooze = {},
onCustomizeToolbar = {},
validateUserSession = {},
onShowRatingBooster = {}
onShowRatingBooster = {},
onCategoryItemClicked = {}
)
}
}
@@ -36,6 +36,7 @@ import ch.protonmail.android.mailattachments.domain.model.AttachmentId
import ch.protonmail.android.mailattachments.domain.model.AttachmentOpenMode
import ch.protonmail.android.mailattachments.domain.usecase.GetAttachmentIntentValues
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcategory.presentation.mapper.toDomainModel
import ch.protonmail.android.mailcategory.presentation.model.CategoryViewState
import ch.protonmail.android.mailcommon.domain.coroutines.AppScope
import ch.protonmail.android.mailcommon.domain.model.Action
@@ -104,6 +105,7 @@ import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ObserveCat
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ObserveValidSenderAddress
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ObserveViewModeChanged
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.RecordRatingBoosterTriggered
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.SetActiveCategoryLabel
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ShouldShowRatingBooster
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.UpdateShowSpamTrashFilter
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.UpdateUnreadFilter
@@ -228,6 +230,7 @@ class MailboxViewModel @Inject constructor(
private val shouldShowRatingBooster: ShouldShowRatingBooster,
private val recordRatingBoosterTriggered: RecordRatingBoosterTriggered,
private val observeCategoryViewStatus: ObserveCategoryViewStatus,
private val setActiveCategoryLabel: SetActiveCategoryLabel,
@IsCategoryViewEnabled private val categoryViewEnabled: FeatureFlag<Boolean>
) : ViewModel() {
@@ -469,10 +472,30 @@ class MailboxViewModel @Inject constructor(
is MailboxViewAction.SignalLabelAsCompleted -> handleLabelAsCompleted(viewAction)
is MailboxViewAction.ValidateUserSession -> handleValidateUserSession()
is MailboxViewAction.NavigateToComposer -> handleNavigateToComposer()
is MailboxViewAction.OnCategoryItemClicked -> handleCategoryItemClicked(viewAction)
}
}
}
private suspend fun handleCategoryItemClicked(viewAction: MailboxViewAction.OnCategoryItemClicked) {
val categoryItem = viewAction.categoryItem
if (categoryItem.isActive) {
Timber.d("Category ${categoryItem.id} is already active, ignoring click")
return
}
val viewMode = getViewModeForCurrentLocation(getSelectedMailLabelId())
setActiveCategoryLabel(
categoryLabelId = categoryItem.id.toDomainModel(),
viewMode = viewMode
).onLeft { error ->
Timber.e("Failed to set active category label: $error")
emitNewStateFrom(MailboxEvent.ErrorChangingCategory)
}
}
private fun handleNavigateToComposer() {
if (state.value.composerNavigationState is MailboxComposerNavigationState.Disabled) {
emitNewStateFrom(MailboxEvent.ErrorComposing)
@@ -21,6 +21,7 @@ package ch.protonmail.android.mailmailbox.presentation.mailbox.model
import ch.protonmail.android.mailattachments.domain.model.OpenAttachmentIntentValues
import ch.protonmail.android.mailattachments.presentation.model.AttachmentIdUiModel
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcategory.presentation.model.CategoryItemUiModel
import ch.protonmail.android.mailcommon.presentation.model.BottomBarEvent
import ch.protonmail.android.mailcommon.presentation.model.BottomSheetOperation
import ch.protonmail.android.maillabel.domain.model.LabelId
@@ -182,6 +183,8 @@ internal sealed interface MailboxViewAction : MailboxOperation {
AffectingBottomAppBar
object ValidateUserSession : MailboxViewAction
data class OnCategoryItemClicked(val categoryItem: CategoryItemUiModel) : MailboxViewAction
}
internal sealed interface MailboxEvent : MailboxOperation {
@@ -351,6 +354,7 @@ internal sealed interface MailboxEvent : MailboxOperation {
object ErrorRetrievingFolderColorSettings : MailboxEvent, AffectingErrorBar, AffectingBottomSheet
object ErrorMoving : MailboxEvent, AffectingErrorBar
object ErrorRetrievingDestinationMailFolders : MailboxEvent, AffectingErrorBar, AffectingBottomSheet
object ErrorChangingCategory : MailboxEvent, AffectingErrorBar
data class AttachmentDownloadStartedEvent(
val attachmentId: AttachmentIdUiModel
@@ -210,6 +210,7 @@ class MailboxReducer @Inject constructor(
is MailboxEvent.ErrorDeleting -> R.string.mailbox_action_delete_failed
is MailboxEvent.ErrorComposing -> R.string.mailbox_action_no_sender_addresses
is MailboxEvent.ErrorChangingCategory -> R.string.mailbox_action_change_category_failed
}
Effect.of(TextUiModel(textResource))
} else {
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2026 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.mailmailbox.presentation.mailbox.usecase
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryLabelId
import ch.protonmail.android.mailconversation.domain.repository.ConversationRepository
import ch.protonmail.android.maillabel.domain.model.ViewMode
import ch.protonmail.android.mailmessage.domain.repository.MessageRepository
import ch.protonmail.android.mailpagination.domain.model.PaginationError
import javax.inject.Inject
class SetActiveCategoryLabel @Inject constructor(
private val conversationRepository: ConversationRepository,
private val messageRepository: MessageRepository
) {
operator fun invoke(categoryLabelId: CategoryLabelId, viewMode: ViewMode): Either<PaginationError, Unit> =
when (viewMode) {
ViewMode.ConversationGrouping ->
conversationRepository.setActiveCategoryLabel(categoryLabelId)
ViewMode.NoConversationGrouping ->
messageRepository.setActiveCategoryLabel(categoryLabelId)
}
}
@@ -112,6 +112,8 @@
<!-- Storage Quota Over Warning-->
<string name="mailbox_action_delete_failed">Failed to delete message</string>
<string name="mailbox_action_change_category_failed">Failed to change the active category</string>
<string name="select_all">Select All</string>
<string name="deselect_all">Deselect All</string>
@@ -30,7 +30,9 @@ import ch.protonmail.android.mailattachments.domain.model.OpenAttachmentIntentVa
import ch.protonmail.android.mailattachments.domain.usecase.GetAttachmentIntentValues
import ch.protonmail.android.mailattachments.presentation.model.AttachmentIdUiModel
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcategory.presentation.mapper.toDomainModel
import ch.protonmail.android.mailcategory.presentation.model.CategoryViewState
import ch.protonmail.android.mailcategory.presentation.sample.CategoryItemUiModelSample
import ch.protonmail.android.mailcommon.domain.model.Action
import ch.protonmail.android.mailcommon.domain.model.AllBottomBarActions
import ch.protonmail.android.mailcommon.domain.model.ConversationId
@@ -44,6 +46,7 @@ import ch.protonmail.android.mailcommon.presentation.model.BottomBarState
import ch.protonmail.android.mailcommon.presentation.model.BottomBarTarget
import ch.protonmail.android.mailcommon.presentation.model.BottomSheetState
import ch.protonmail.android.mailcommon.presentation.model.CappedNumberUiModel
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.mailcommon.presentation.model.toCappedNumberUiModel
import ch.protonmail.android.mailcommon.presentation.sample.ActionUiModelSample
import ch.protonmail.android.mailcommon.presentation.ui.delete.DeleteDialogState
@@ -111,6 +114,7 @@ import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ObserveCat
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ObserveValidSenderAddress
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ObserveViewModeChanged
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.RecordRatingBoosterTriggered
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.SetActiveCategoryLabel
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.ShouldShowRatingBooster
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.UpdateShowSpamTrashFilter
import ch.protonmail.android.mailmailbox.presentation.mailbox.usecase.UpdateUnreadFilter
@@ -130,6 +134,7 @@ import ch.protonmail.android.mailmessage.domain.usecase.UnStarMessages
import ch.protonmail.android.mailmessage.presentation.model.bottomsheet.MailboxMoreActionsBottomSheetState
import ch.protonmail.android.mailmessage.presentation.model.bottomsheet.SnoozeSheetState
import ch.protonmail.android.mailpagination.domain.model.PageInvalidationEvent
import ch.protonmail.android.mailpagination.domain.model.PaginationError
import ch.protonmail.android.mailpagination.domain.usecase.ObservePageInvalidationEvents
import ch.protonmail.android.mailsession.domain.repository.EventLoopRepository
import ch.protonmail.android.mailsession.domain.usecase.HasValidUserSession
@@ -364,6 +369,8 @@ internal class MailboxViewModelTest {
} returns categoryViewStatusFlow
}
private val setActiveCategoryLabel = mockk<SetActiveCategoryLabel>()
private val scope = TestScope(UnconfinedTestDispatcher())
private val mailboxViewModel by lazy {
@@ -421,7 +428,8 @@ internal class MailboxViewModelTest {
shouldShowRatingBooster = shouldShowRatingBooster,
recordRatingBoosterTriggered = recordRatingBoosterTriggered,
categoryViewEnabled = isCategoryViewEnabled,
observeCategoryViewStatus = observeCategoryViewStatus
observeCategoryViewStatus = observeCategoryViewStatus,
setActiveCategoryLabel = setActiveCategoryLabel
)
}
@@ -3881,6 +3889,97 @@ internal class MailboxViewModelTest {
}
}
@Test
fun `when inactive category item is clicked, active category is changed`() = runTest {
// Given
val categoryItem = CategoryItemUiModelSample.social
expectViewModeForCurrentLocation(NoConversationGrouping)
every {
setActiveCategoryLabel(
categoryLabelId = categoryItem.id.toDomainModel(),
viewMode = NoConversationGrouping
)
} returns Unit.right()
// When
mailboxViewModel.submit(MailboxViewAction.OnCategoryItemClicked(categoryItem))
advanceUntilIdle()
// Then
verify(exactly = 1) {
setActiveCategoryLabel(
categoryLabelId = categoryItem.id.toDomainModel(),
viewMode = NoConversationGrouping
)
}
verify(exactly = 0) {
mailboxReducer.newStateFrom(any(), MailboxEvent.ErrorChangingCategory)
}
}
@Test
fun `when active category item is clicked, the request is ignored`() = runTest {
// Given
val categoryItem = CategoryItemUiModelSample.primary.copy(isActive = true)
// When
mailboxViewModel.submit(MailboxViewAction.OnCategoryItemClicked(categoryItem))
advanceUntilIdle()
// Then
verify(exactly = 0) {
setActiveCategoryLabel(any(), any())
}
verify(exactly = 0) {
mailboxReducer.newStateFrom(any(), MailboxEvent.ErrorChangingCategory)
}
}
@Test
fun `when changing active category fails, error event is emitted`() = runTest {
// Given
val categoryItem = CategoryItemUiModelSample.social
val expectedState = MailboxStateSampleData.Loading.copy(
error = Effect.of(TextUiModel(R.string.mailbox_action_change_category_failed))
)
expectViewModeForCurrentLocation(NoConversationGrouping)
every {
setActiveCategoryLabel(
categoryLabelId = categoryItem.id.toDomainModel(),
viewMode = NoConversationGrouping
)
} returns PaginationError.Other(DataError.Local.IllegalStateError).left()
every {
mailboxReducer.newStateFrom(
any(),
MailboxEvent.ErrorChangingCategory
)
} returns expectedState
mailboxViewModel.state.test {
awaitItem()
// When
mailboxViewModel.submit(MailboxViewAction.OnCategoryItemClicked(categoryItem))
advanceUntilIdle()
// Then
assertEquals(expectedState, awaitItem())
verify(exactly = 1) {
mailboxReducer.newStateFrom(any(), MailboxEvent.ErrorChangingCategory)
}
cancelAndIgnoreRemainingEvents()
}
}
private fun returnExpectedStateForBottomBarEvent(
intermediateState: MailboxState? = null,
expectedState: MailboxState
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailmessage.data.local
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelAsAction
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageId
@@ -142,4 +143,7 @@ interface RustMessageDataSource {
fun observeScrollerFetchNewStatus(): Flow<MessageScrollerFetchNewStatus>
fun observeCategoryViewStatus(): Flow<CategoryViewStatus>
fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit>
}
@@ -21,6 +21,7 @@ package ch.protonmail.android.mailmessage.data.local
import arrow.core.Either
import arrow.core.left
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelAsAction
import ch.protonmail.android.mailcommon.data.mapper.LocalLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalMessageId
@@ -456,4 +457,7 @@ class RustMessageDataSourceImpl @Inject constructor(
override fun observeCategoryViewStatus(): Flow<CategoryViewStatus> =
rustMessageListQuery.observeCategoryViewStatus()
override fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit> =
rustMessageListQuery.setActiveCategoryLabel(categoryLabelId)
}
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailmessage.data.local
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalItemId
import ch.protonmail.android.maillabel.domain.model.LabelId
import ch.protonmail.android.mailmessage.data.wrapper.MailMessageCursorWrapper
@@ -49,4 +50,6 @@ interface RustMessageListQuery {
fun observeScrollerFetchNewStatus(): Flow<MessageScrollerStatusUpdate>
fun observeCategoryViewStatus(): Flow<CategoryViewStatus>
fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit>
}
@@ -22,6 +22,7 @@ import arrow.core.Either
import arrow.core.left
import ch.protonmail.android.mailcategory.data.mapper.toCategoryViewStatus
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalConversationId
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.maillabel.data.local.RustMailboxFactory
@@ -188,6 +189,12 @@ class RustMessageListQueryImpl @Inject constructor(
override suspend fun supportsIncludeFilter() = paginatorState?.paginatorWrapper?.supportsIncludeFilter() == true
override fun setActiveCategoryLabel(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit> =
paginatorState?.paginatorWrapper?.changeCategoryView(categoryLabelId) ?: run {
Timber.w("rust-message-query: No paginator to change category view")
PaginationError.Other(DataError.Local.IllegalStateError).left()
}
override suspend fun updateUnreadFilter(filterUnread: Boolean) {
paginatorState?.paginatorWrapper?.filterUnread(filterUnread)
?: Timber.w("rust-message-query: No paginator to update unread filter")
@@ -20,6 +20,8 @@ package ch.protonmail.android.mailmessage.data.repository
import java.io.File
import arrow.core.Either
import ch.protonmail.android.mailcategory.data.mapper.toLocalCategoryLabelId
import ch.protonmail.android.mailcategory.domain.model.CategoryLabelId
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
import ch.protonmail.android.mailcommon.domain.model.CursorId
@@ -187,4 +189,8 @@ class RustMessageRepositoryImpl @Inject constructor(
override suspend fun isMessageSenderBlocked(userId: UserId, messageId: MessageId): Either<DataError, Boolean> =
rustMessageDataSource.isMessageSenderBlocked(userId, messageId.toLocalMessageId())
override fun setActiveCategoryLabel(categoryLabelId: CategoryLabelId): Either<PaginationError, Unit> =
rustMessageDataSource.setActiveCategoryLabel(categoryLabelId.toLocalCategoryLabelId())
}
@@ -23,12 +23,14 @@ import arrow.core.left
import arrow.core.right
import ch.protonmail.android.mailcategory.data.mapper.toCategoryViewStatus
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalItemId
import ch.protonmail.android.mailpagination.data.mapper.toPaginationError
import ch.protonmail.android.mailpagination.domain.model.PaginationError
import timber.log.Timber
import uniffi.mail_uniffi.IncludeSwitch
import uniffi.mail_uniffi.MessageScroller
import uniffi.mail_uniffi.MessageScrollerChangeCategoryViewResult
import uniffi.mail_uniffi.MessageScrollerCursorResult
import uniffi.mail_uniffi.MessageScrollerFetchMoreResult
import uniffi.mail_uniffi.MessageScrollerGetItemsResult
@@ -94,4 +96,10 @@ class MailboxMessagePaginatorWrapper(
override suspend fun getCategoryViewStatus(): CategoryViewStatus =
rustPaginator.categoryView().toCategoryViewStatus()
override fun changeCategoryView(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit> =
when (val result = rustPaginator.changeCategoryView(categoryLabelId)) {
is MessageScrollerChangeCategoryViewResult.Error -> result.v1.toPaginationError().left()
is MessageScrollerChangeCategoryViewResult.Ok -> Unit.right()
}
}
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailmessage.data.wrapper
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalItemId
import ch.protonmail.android.mailpagination.domain.model.PaginationError
@@ -44,4 +45,6 @@ interface MessagePaginatorWrapper {
fun getScrollerId(): String
suspend fun getCategoryViewStatus(): CategoryViewStatus
fun changeCategoryView(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit>
}
@@ -22,6 +22,7 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.data.mapper.LocalCategoryLabelId
import ch.protonmail.android.mailcommon.data.mapper.LocalItemId
import ch.protonmail.android.mailpagination.data.mapper.toPaginationError
import ch.protonmail.android.mailpagination.domain.model.PaginationError
@@ -89,4 +90,9 @@ class SearchMessagePaginatorWrapper(
override suspend fun getCategoryViewStatus(): CategoryViewStatus = CategoryViewStatus.NotAvailable
override fun changeCategoryView(categoryLabelId: LocalCategoryLabelId): Either<PaginationError, Unit> {
Timber.w("search-paginator: Called change category on a search paginator, which is illegal. No-op.")
return Unit.right()
}
}
@@ -19,6 +19,7 @@
package ch.protonmail.android.mailmessage.domain.repository
import arrow.core.Either
import ch.protonmail.android.mailcategory.domain.model.CategoryLabelId
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcommon.domain.model.ConversationCursorError
import ch.protonmail.android.mailcommon.domain.model.CursorId
@@ -168,4 +169,6 @@ interface MessageRepository {
fun observeScrollerFetchNewStatus(): Flow<MessageScrollerFetchNewStatus>
fun observeCategoryViewStatus(): Flow<CategoryViewStatus>
fun setActiveCategoryLabel(categoryLabelId: CategoryLabelId): Either<PaginationError, Unit>
}