Skip to content

Commit 94ff105

Browse files
committed
fix(message-list): auto-exit selection mode when no messages selected
1 parent da93732 commit 94ff105

9 files changed

Lines changed: 88 additions & 29 deletions

File tree

feature/mail/message/list/api/src/main/kotlin/net/thunderbird/feature/mail/message/list/ui/effect/MessageListEffect.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ sealed interface MessageListEffect {
9595
val isAllSelected: Boolean,
9696
) : MessageListEffect
9797

98+
data object ResetToolbarActionMode : MessageListEffect
99+
98100
data class ScrollToMessage(val message: MessageItemUi) : MessageListEffect
99101

100102
data class OpenMessage(val message: MessageItemUi) : MessageListEffect

feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/MessageListViewModel.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,17 @@ internal class MessageListViewModel(
3434
is MessageListState.SelectingMessages -> {
3535
val selectedCount = state.messages.count { it.selected }
3636
emitEffect(
37-
MessageListEffect.UpdateToolbarActionMode(
38-
title = stringsResourceManager.stringResource(
39-
MessageListApiR.string.actionbar_selected,
40-
selectedCount,
41-
),
42-
isAllSelected = selectedCount == state.messages.size,
43-
),
37+
if (selectedCount > 0) {
38+
MessageListEffect.UpdateToolbarActionMode(
39+
title = stringsResourceManager.stringResource(
40+
MessageListApiR.string.actionbar_selected,
41+
selectedCount,
42+
),
43+
isAllSelected = selectedCount == state.messages.size,
44+
)
45+
} else {
46+
MessageListEffect.ResetToolbarActionMode
47+
},
4448
)
4549
}
4650

feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/state/machine/MessageListStateMachine.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class MessageListStateMachine(
6161
globalState()
6262
loadingMessagesState(dispatch)
6363
loadedMessagesState()
64-
selectingMessagesState()
64+
selectingMessagesState(dispatch, dispatchUiEffect)
6565
searchingMessagesState()
6666
},
6767
) : StateMachine<MessageListState, MessageListEvent> by stateMachine {

feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/state/machine/SetupSelectingMessagesState.kt

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package net.thunderbird.feature.mail.message.list.internal.ui.state.machine
22

33
import kotlinx.collections.immutable.toPersistentList
44
import net.thunderbird.core.common.state.builder.StateMachineBuilder
5+
import net.thunderbird.feature.mail.message.list.ui.effect.MessageListEffect
56
import net.thunderbird.feature.mail.message.list.ui.event.MessageItemEvent
67
import net.thunderbird.feature.mail.message.list.ui.event.MessageListEvent
8+
import net.thunderbird.feature.mail.message.list.ui.state.MessageItemUi
79
import net.thunderbird.feature.mail.message.list.ui.state.MessageListState
810

911
/**
@@ -16,29 +18,49 @@ import net.thunderbird.feature.mail.message.list.ui.state.MessageListState
1618
* - [MessageListEvent.ExitSelectionMode]: Exits selection mode, deselecting all messages and returning
1719
* to the [MessageListState.LoadedMessages] state.
1820
*/
19-
internal fun StateMachineBuilder<MessageListState, MessageListEvent>.selectingMessagesState() {
21+
internal fun StateMachineBuilder<MessageListState, MessageListEvent>.selectingMessagesState(
22+
dispatch: (MessageListEvent) -> Unit,
23+
dispatchUiEffect: (MessageListEffect) -> Unit,
24+
) {
2025
state<MessageListState.SelectingMessages> {
2126
transition<MessageItemEvent.ToggleSelectMessages> { state, event ->
22-
state.copy(
23-
messages = state.messages.map { message ->
24-
if (message in event.messages) message.copy(selected = !message.selected) else message
25-
}.toPersistentList(),
26-
)
27+
toggleSelectMessages(state, event.messages, dispatch, dispatchUiEffect)
2728
}
29+
2830
transition<MessageListEvent.ExitSelectionMode> { state, _ ->
31+
dispatchUiEffect(MessageListEffect.ResetToolbarActionMode)
2932
MessageListState.LoadedMessages(
3033
metadata = state.metadata,
3134
preferences = state.preferences,
3235
messages = state.messages.map { message -> message.copy(selected = false) }.toPersistentList(),
3336
)
3437
}
3538

36-
transition<MessageItemEvent.OnMessageClick> { currentState, event ->
37-
currentState.copy(
38-
messages = currentState.messages.map { message ->
39-
if (message == event.message) message.copy(selected = !message.selected) else message
40-
}.toPersistentList(),
41-
)
39+
transition<MessageItemEvent.OnMessageClick> { state, event ->
40+
toggleSelectMessages(state, listOf(event.message), dispatch, dispatchUiEffect)
4241
}
4342
}
4443
}
44+
45+
private fun toggleSelectMessages(
46+
state: MessageListState.SelectingMessages,
47+
messages: List<MessageItemUi>,
48+
dispatch: (MessageListEvent) -> Unit,
49+
dispatchUiEffect: (MessageListEffect) -> Unit,
50+
): MessageListState.SelectingMessages {
51+
var selectedCount = 0
52+
val newMessages = state.messages.map { message ->
53+
if (message in messages) {
54+
message.copy(selected = !message.selected)
55+
} else {
56+
message
57+
}.also { selectedCount += if (it.selected) 1 else 0 }
58+
}.toPersistentList()
59+
return if (selectedCount == 0) {
60+
dispatch(MessageListEvent.ExitSelectionMode)
61+
dispatchUiEffect(MessageListEffect.ResetToolbarActionMode)
62+
state
63+
} else {
64+
state.copy(messages = newMessages)
65+
}
66+
}

feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/state/sideeffect/inject/MessageListSideEffectsModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.Lo
77
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.LoadPreferencesSideEffect
88
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.LoadSortCriteriaStateSideEffectHandler
99
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.LoadSwipeActionsStateSideEffectHandler
10-
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.ToggleMessageSideEffect
1110
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.legacy.LoadMessagesLegacySideEffect
1211
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.ui.OpenMessageSideEffect
1312
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.ui.SetMessageActiveSideEffect
13+
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.ui.ToggleMessageSideEffect
1414
import net.thunderbird.feature.mail.message.list.ui.MessageListContract
1515
import net.thunderbird.feature.mail.message.list.ui.state.sideeffect.MessageListStateSideEffectHandlerFactory
1616
import org.koin.dsl.module

feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/state/sideeffect/ui/OpenMessageSideEffect.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ internal class OpenMessageSideEffect(
1515
dispatchUiEffect: suspend (MessageListEffect) -> Unit,
1616
) : MessageListStateSideEffectHandler(logger, dispatch, dispatchUiEffect) {
1717
override fun accept(event: MessageListEvent, oldState: MessageListState, newState: MessageListState): Boolean =
18-
event is MessageItemEvent.OnMessageClick && newState is MessageListState.LoadedMessages
18+
event is MessageItemEvent.OnMessageClick &&
19+
oldState is MessageListState.LoadedMessages &&
20+
newState is MessageListState.LoadedMessages
1921

2022
override suspend fun consume(
2123
event: MessageListEvent,

feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/state/sideeffect/ToggleMessageSideEffect.kt renamed to feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/state/sideeffect/ui/ToggleMessageSideEffect.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect
1+
package net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.ui
22

33
import kotlinx.coroutines.CoroutineScope
44
import net.thunderbird.core.logging.Logger
@@ -15,7 +15,9 @@ internal class ToggleMessageSideEffect(
1515
) : MessageListStateSideEffectHandler(logger, dispatch) {
1616

1717
override fun accept(event: MessageListEvent, oldState: MessageListState, newState: MessageListState): Boolean =
18-
event is MessageItemEvent.OnMessageClick && newState is MessageListState.SelectingMessages
18+
event is MessageItemEvent.OnMessageClick &&
19+
oldState is MessageListState.SelectingMessages &&
20+
newState is MessageListState.SelectingMessages
1921

2022
override suspend fun consume(
2123
event: MessageListEvent,

feature/mail/message/list/internal/src/test/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/state/sideeffect/ToggleMessageSideEffectTest.kt

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,32 @@ import kotlin.test.Test
99
import kotlinx.coroutines.test.runTest
1010
import net.thunderbird.core.common.state.sideeffect.StateSideEffectHandler
1111
import net.thunderbird.core.logging.testing.TestLogger
12+
import net.thunderbird.feature.mail.message.list.internal.ui.state.sideeffect.ui.ToggleMessageSideEffect
1213
import net.thunderbird.feature.mail.message.list.ui.event.MessageItemEvent
1314
import net.thunderbird.feature.mail.message.list.ui.event.MessageListEvent
1415

1516
class ToggleMessageSideEffectTest : BaseSideEffectHandlerTest() {
1617

1718
@Test
18-
fun `handle() should return Consumed when event is OnMessageClick and newState is SelectingMessages`() = runTest {
19+
fun `handle() should return Consumed when event is OnMessageClick and both states are SelectingMessages`() =
20+
runTest {
21+
// Arrange
22+
val message = createMessageItemUi()
23+
val testSubject = createTestSubject()
24+
25+
// Act
26+
val result = testSubject.handle(
27+
event = MessageItemEvent.OnMessageClick(message),
28+
oldState = createSelectingMessagesState(),
29+
newState = createSelectingMessagesState(),
30+
)
31+
32+
// Assert
33+
assertThat(result).isEqualTo(StateSideEffectHandler.ConsumeResult.Consumed)
34+
}
35+
36+
@Test
37+
fun `handle() should return Ignored when event is OnMessageClick but oldState is LoadedMessages`() = runTest {
1938
// Arrange
2039
val message = createMessageItemUi()
2140
val testSubject = createTestSubject()
@@ -28,7 +47,7 @@ class ToggleMessageSideEffectTest : BaseSideEffectHandlerTest() {
2847
)
2948

3049
// Assert
31-
assertThat(result).isEqualTo(StateSideEffectHandler.ConsumeResult.Consumed)
50+
assertThat(result).isEqualTo(StateSideEffectHandler.ConsumeResult.Ignored)
3251
}
3352

3453
@Test
@@ -40,7 +59,7 @@ class ToggleMessageSideEffectTest : BaseSideEffectHandlerTest() {
4059
// Act
4160
val result = testSubject.handle(
4261
event = MessageItemEvent.OnMessageClick(message),
43-
oldState = createLoadedMessagesState(),
62+
oldState = createSelectingMessagesState(),
4463
newState = createLoadedMessagesState(),
4564
)
4665

@@ -56,7 +75,7 @@ class ToggleMessageSideEffectTest : BaseSideEffectHandlerTest() {
5675
// Act
5776
val result = testSubject.handle(
5877
event = MessageListEvent.EnterSelectionMode,
59-
oldState = createLoadedMessagesState(),
78+
oldState = createSelectingMessagesState(),
6079
newState = createSelectingMessagesState(),
6180
)
6281

@@ -74,7 +93,7 @@ class ToggleMessageSideEffectTest : BaseSideEffectHandlerTest() {
7493
// Act
7594
testSubject.handle(
7695
event = MessageItemEvent.OnMessageClick(message),
77-
oldState = createLoadedMessagesState(),
96+
oldState = createSelectingMessagesState(),
7897
newState = createSelectingMessagesState(),
7998
)
8099

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,6 +2412,9 @@ class MessageListFragment :
24122412
when (effect) {
24132413
is MessageListEffect.ScrollToMessage -> scrollToMessage(effect.message)
24142414
is MessageListEffect.UpdateToolbarActionMode -> {
2415+
println(
2416+
"[MessageList] UpdateToolbarActionMode called with title: ${effect.title}, state = $stateSnapshot",
2417+
)
24152418
if (actionMode == null) {
24162419
startAndPrepareActionMode()
24172420
}
@@ -2422,6 +2425,11 @@ class MessageListFragment :
24222425
}
24232426
}
24242427

2428+
is MessageListEffect.ResetToolbarActionMode -> {
2429+
println("[MessageList] ResetToolbarActionMode called with state = $stateSnapshot")
2430+
resetActionMode()
2431+
}
2432+
24252433
is MessageListEffect.RefreshMessageList -> {
24262434
val (primarySortType, secondarySortType) = effect.currentState.metadata.currentSortCriteria
24272435
val (sortType, sortAscending) = primarySortType.toDomainSortType()

0 commit comments

Comments
 (0)