Reset category header scroll state on account switch

- ensure category bar returns to default position after account change
    - added PrimaryAccountChanged event

ET-6232
This commit is contained in:
Serdar Ozturk
2026-05-22 08:03:58 +00:00
committed by MargeBot
parent 4078cbe3d8
commit dfb907cd82
8 changed files with 60 additions and 8 deletions
@@ -18,13 +18,16 @@
package ch.protonmail.android.mailcategory.presentation.model
import ch.protonmail.android.mailcommon.presentation.Effect
sealed interface CategoryViewState {
sealed interface Available : CategoryViewState {
data object Loading : Available
data class Data(
val categories: List<CategoryItemUiModel>
val categories: List<CategoryItemUiModel>,
val resetScrollEffect: Effect<Unit> = Effect.empty()
) : Available
}
@@ -36,6 +36,7 @@ import ch.protonmail.android.design.compose.theme.ProtonDimens
import ch.protonmail.android.mailcategory.presentation.CategoryViewMenu
import ch.protonmail.android.mailcategory.presentation.model.CategoryItemUiModel
import ch.protonmail.android.mailcategory.presentation.model.CategoryViewState
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
import ch.protonmail.android.mailcommon.presentation.compose.MailDimens
import ch.protonmail.android.mailmailbox.presentation.mailbox.model.MailboxListState
import ch.protonmail.android.mailmailbox.presentation.mailbox.model.MailboxState
@@ -50,6 +51,13 @@ fun MailboxStickyHeader(
actions: MailboxStickyHeader.Actions,
isCategoryViewEnabled: Boolean
) {
val horizontalScrollState = rememberScrollState()
(state.categoryViewState as? CategoryViewState.Available.Data)?.let { categoryViewData ->
ConsumableLaunchedEffect(effect = categoryViewData.resetScrollEffect) {
horizontalScrollState.animateScrollTo(0)
}
}
val isCategoryViewVisible =
isCategoryViewEnabled &&
@@ -78,7 +86,7 @@ fun MailboxStickyHeader(
bottom = bottomPadding,
top = 0.dp
)
.horizontalScroll(rememberScrollState()),
.horizontalScroll(horizontalScrollState),
horizontalArrangement = Arrangement.Start
) {
if (state.mailboxListState is MailboxListState.Data.SelectionMode) {
@@ -347,6 +347,14 @@ class MailboxViewModel @Inject constructor(
}
.launchIn(viewModelScope)
primaryUserId
.distinctUntilChanged()
.drop(1)
.onEach {
emitNewStateFrom(MailboxEvent.PrimaryAccountChanged)
}
.launchIn(viewModelScope)
observePrimaryAccountAvatarItem().onEach { item ->
emitNewStateFrom(MailboxEvent.PrimaryAccountAvatarChanged(item))
}.launchIn(viewModelScope)
@@ -299,6 +299,8 @@ internal sealed interface MailboxEvent : MailboxOperation {
data class PrimaryAccountAvatarChanged(val item: CoreAccountAvatarItem?) : MailboxEvent, AffectingTopAppBar
data object PrimaryAccountChanged : MailboxEvent, AffectingCategoryView
data class LoadingBarStateUpdated(val state: LoadingBarUiState) : MailboxEvent, AffectingMailboxList
data class PaginatorInvalidated(val event: PageInvalidationEvent) : MailboxEvent, AffectingMailboxList
@@ -19,8 +19,8 @@
package ch.protonmail.android.mailmailbox.presentation.mailbox.reducer
import ch.protonmail.android.mailcategory.presentation.mapper.CategoryViewUiModelMapper
import ch.protonmail.android.mailcategory.presentation.mapper.toUiModel
import ch.protonmail.android.mailcategory.presentation.model.CategoryViewState
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.mailmailbox.presentation.mailbox.model.MailboxEvent
import ch.protonmail.android.mailmailbox.presentation.mailbox.model.MailboxOperation
import javax.inject.Inject
@@ -29,12 +29,24 @@ class MailboxCategoryViewReducer @Inject constructor(
private val categoryViewUiModelMapper: CategoryViewUiModelMapper
) {
internal fun newStateFrom(operation: MailboxOperation.AffectingCategoryView): CategoryViewState {
internal fun newStateFrom(
currentState: CategoryViewState,
operation: MailboxOperation.AffectingCategoryView
): CategoryViewState {
return when (operation) {
is MailboxEvent.CategoryViewStatusChanged -> {
categoryViewUiModelMapper.toUiModel(operation.categoryViewStatus)
}
MailboxEvent.PrimaryAccountChanged -> {
when (currentState) {
is CategoryViewState.Available.Data -> {
currentState.copy(resetScrollEffect = Effect.of(Unit))
}
else -> currentState
}
}
}
}
}
@@ -76,7 +76,7 @@ class MailboxReducer @Inject constructor(
private fun MailboxState.toNewCategoryViewStateFrom(operation: MailboxOperation): CategoryViewState {
return if (operation is MailboxOperation.AffectingCategoryView) {
categoryViewReducer.newStateFrom(operation)
categoryViewReducer.newStateFrom(this.categoryViewState, operation)
} else {
categoryViewState
}
@@ -21,7 +21,9 @@ package ch.protonmail.android.mailmailbox.presentation.mailbox.reducer
import ch.protonmail.android.mailcategory.domain.model.CategoryViewStatus
import ch.protonmail.android.mailcategory.presentation.mapper.CategoryViewUiModelMapper
import ch.protonmail.android.mailcategory.presentation.mapper.toUiModel
import ch.protonmail.android.mailcategory.presentation.sample.CategoryItemUiModelSample
import ch.protonmail.android.mailcategory.presentation.model.CategoryViewState
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.mailmailbox.presentation.mailbox.model.MailboxEvent
import io.mockk.every
import io.mockk.mockk
@@ -40,6 +42,7 @@ class MailboxCategoryViewReducerTest {
fun `should map category view status changed event to category view state`() {
// Given
val categoryViewStatus = mockk<CategoryViewStatus>()
val currentState = mockk<CategoryViewState>()
val expectedState = mockk<CategoryViewState>()
every {
@@ -51,9 +54,25 @@ class MailboxCategoryViewReducerTest {
)
// When
val actual = reducer.newStateFrom(operation)
val actual = reducer.newStateFrom(currentState, operation)
// Then
assertEquals(expectedState, actual)
}
@Test
fun `should emit reset scroll effect when primary account changes and category view is available data`() {
// Given
val currentState = CategoryViewState.Available.Data(
categories = CategoryItemUiModelSample.all,
resetScrollEffect = Effect.empty()
)
// When
val actual = reducer.newStateFrom(currentState, MailboxEvent.PrimaryAccountChanged)
// Then
assertEquals(CategoryItemUiModelSample.all, (actual as CategoryViewState.Available.Data).categories)
assertEquals(Unit, actual.resetScrollEffect.consume())
}
}
@@ -93,7 +93,7 @@ internal class MailboxReducerTest(
}
private val categoryViewReducer: MailboxCategoryViewReducer = mockk {
every { newStateFrom(any()) } returns reducedState.categoryViewState
every { newStateFrom(any(), any()) } returns reducedState.categoryViewState
}
private val mailboxReducer = MailboxReducer(