Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d1cf173
feat(message-list): add messageReference to MessageItemUi
rafaeltonholo Mar 2, 2026
78c06bc
feat(message-list): implement legacy message list loading bridge
rafaeltonholo Mar 2, 2026
492fcdf
refactor(message-list): remove manual refresh on sort criteria change…
rafaeltonholo Mar 2, 2026
56108a7
feat(message-list): enable new state feature alongside Compose for me…
rafaeltonholo Mar 10, 2026
29170bd
fix: use account-specific sort criteria in MessageListFragment
rafaeltonholo Apr 6, 2026
08171d6
feat(message-list): implement Select All and Deselect All using state…
rafaeltonholo Apr 6, 2026
6f8edd9
feat(message-list): implement `selectedMessages` in `MessageListFragm…
rafaeltonholo Apr 6, 2026
8880a94
feat(message-list): track and handle focused message in message list
rafaeltonholo Apr 6, 2026
0b1b4a7
chore(message-list): MessageListFragment clean up
rafaeltonholo Apr 6, 2026
69edeb2
refactor(message-list): track active message in view model state
rafaeltonholo Apr 7, 2026
fe006d9
feat(message-list): implement open message functionality
rafaeltonholo Apr 8, 2026
b588b06
feat(message-list): implement message selection toggling in selecting…
rafaeltonholo Apr 8, 2026
595489e
refactor(message-list): move OpenMessageSideEffect to sideeffect.ui p…
rafaeltonholo Apr 8, 2026
5bd3544
refactor(message-list): use viewModel delegate instead of inject
rafaeltonholo Apr 8, 2026
6bda989
feat(message-list): add scroll-to-message side effect for active mess…
rafaeltonholo Apr 8, 2026
5f5b6dd
refactor(message-list): mark internal classes and functions as internal
rafaeltonholo Apr 13, 2026
aa6c66d
test(message-list): add tests for message active/open/toggle side eff…
rafaeltonholo Apr 13, 2026
29ed6f4
fix(message-list): auto-exit selection mode when no messages selected
rafaeltonholo Apr 13, 2026
affec6c
feat(message-list): mark message as read when clicked
rafaeltonholo Apr 13, 2026
a4c8de6
feat(message-list): add support for unified folder
rafaeltonholo Mar 3, 2026
21c1811
feat(message-list): add account indicator visibility based on folder'…
rafaeltonholo Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ private fun PreviewDefault(
state = MessageItemUi(
state = MessageItemUi.State.New,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -209,6 +210,7 @@ private fun PreviewCompact(
state = MessageItemUi(
state = MessageItemUi.State.New,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -257,6 +259,7 @@ private fun PreviewRelaxed(
state = MessageItemUi(
state = MessageItemUi.State.New,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -305,6 +308,7 @@ private fun PreviewDefaultWithoutAccountIndicator(
state = MessageItemUi(
state = MessageItemUi.State.New,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ private fun PreviewDefault(
state = MessageItemUi(
state = MessageItemUi.State.Read,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -240,6 +241,7 @@ private fun PreviewCompact(
state = MessageItemUi(
state = MessageItemUi.State.Read,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -288,6 +290,7 @@ private fun PreviewRelaxed(
state = MessageItemUi(
state = MessageItemUi.State.Read,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -336,6 +339,7 @@ private fun PreviewDefaultWithoutAccountIndicator(
state = MessageItemUi(
state = MessageItemUi.State.Read,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ private fun PreviewDefault(
state = MessageItemUi(
state = MessageItemUi.State.Unread,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -209,6 +210,7 @@ private fun PreviewCompact(
state = MessageItemUi(
state = MessageItemUi.State.Unread,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -257,6 +259,7 @@ private fun PreviewRelaxed(
state = MessageItemUi(
state = MessageItemUi.State.Unread,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down Expand Up @@ -305,6 +308,7 @@ private fun PreviewDefaultWithoutIndicator(
state = MessageItemUi(
state = MessageItemUi.State.Unread,
id = "",
messageReference = "reference",
account = Account(id = AccountIdFactory.create(), color = params.accountColor),
senders = ComposedAddressUi(
displayName = params.sender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import net.thunderbird.feature.mail.message.list.ui.component.MessageListScope
import net.thunderbird.feature.mail.message.list.ui.component.rememberMessageListScope
import net.thunderbird.feature.mail.message.list.ui.effect.MessageListEffect
import net.thunderbird.feature.mail.message.list.ui.event.MessageListEvent
import net.thunderbird.feature.mail.message.list.ui.legacy.LegacyMessageListBridge
import net.thunderbird.feature.mail.message.list.ui.state.MessageListState
import net.thunderbird.feature.mail.message.list.ui.state.sideeffect.MessageListStateSideEffectHandlerFactory
import net.thunderbird.feature.notification.api.content.InAppNotification
Expand Down Expand Up @@ -48,6 +49,8 @@ interface MessageListContract {
data class Args(
val accountIds: Set<AccountId>,
val folderId: Long?,
// Temporary argument just to allow using the current legacy implementation.
val legacyMessageListBridge: LegacyMessageListBridge,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.thunderbird.feature.mail.message.list.ui.effect

import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.mail.message.list.ui.state.MessageItemUi
import net.thunderbird.feature.mail.message.list.ui.state.MessageListState

/**
Expand Down Expand Up @@ -81,4 +82,22 @@ sealed interface MessageListEffect {
* that have been discarded for that account.
*/
data class DraftsDiscarded(val messagesIdByAccountId: Map<AccountId, List<Long>>) : MessageListEffect

/**
* Effect to update the state and appearance of the contextual action mode toolbar.
*
* @param title The text to be displayed in the action mode toolbar, usually indicating
* the number of currently selected messages.
* @param isAllSelected Whether all available messages in the current list are selected.
*/
data class UpdateToolbarActionMode(
val title: String,
val isAllSelected: Boolean,
) : MessageListEffect

data object ResetToolbarActionMode : MessageListEffect

data class ScrollToMessage(val message: MessageItemUi) : MessageListEffect

data class OpenMessage(val message: MessageItemUi) : MessageListEffect
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ sealed interface MessageItemEvent : MessageListEvent.UserEvent {
*/
data class OnMessageClick(val message: MessageItemUi) : MessageItemEvent

data object SelectAll : MessageItemEvent
data object DeselectAll : MessageItemEvent

/**
* Event to toggle the selection state of one or more messages.
*
Expand Down Expand Up @@ -60,4 +63,9 @@ sealed interface MessageItemEvent : MessageListEvent.UserEvent {
* @property message The message item that was swiped.
*/
data class OnSwipeMessage(val message: MessageItemUi, val swipeAction: SwipeAction) : MessageItemEvent

data class OnFocusEnter(val message: MessageItemUi) : MessageItemEvent
data class OnFocusExit(val message: MessageItemUi) : MessageItemEvent

data class SetMessageActive(val message: MessageItemUi?) : MessageItemEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ sealed interface MessageListEvent {
*
* @param progress A float value between 0.0 and 1.0 representing the loading completion percentage.
*/
data class UpdateLoadingProgress(val progress: Float) : SystemEvent
data class UpdateLoadingProgress(val progress: Float, val messages: List<MessageItemUi> = emptyList()) : SystemEvent

/**
* A system event indicating that a list of messages has been successfully loaded.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.thunderbird.feature.mail.message.list.ui.legacy

import kotlinx.coroutines.flow.Flow
import net.thunderbird.feature.mail.message.list.preferences.MessageListPreferences
import net.thunderbird.feature.mail.message.list.ui.state.MessageItemUi
import net.thunderbird.feature.mail.message.list.ui.state.MessageListMetadata

/**
* Bridge interface for accessing functionalities implemented in the legacy message list system.
*
* @see MessageListPreferences for available display and behavior customization options
* @see MessageListMetadata for contextual information about the message list state
* @see MessageItemUi for the structure of individual message items in the returned list
*/
interface LegacyMessageListBridge {
/**
* Loads messages for the current mailbox stored in the database.
*
* @param preferences Display and behaviour settings that influence how messages are loaded.
* @param metadata Contextual information about the message list (e.g. folder, account).
* @returns a [Flow] that emits updated [MessageItemUi] lists as the underlying data changes.
*/
fun loadMessages(
preferences: MessageListPreferences,
metadata: MessageListMetadata,
): Flow<List<MessageItemUi>>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.thunderbird.feature.mail.message.list.ui.state

import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.toPersistentList

/**
* Represents the UI state of a single message item in a message list.
Expand All @@ -12,6 +13,7 @@ import androidx.compose.runtime.Immutable
*
* @property state The current display state of the message (e.g., Read, Unread, Selected).
* @property id The unique identifier for the message.
* @param messageReference The reference to the message in the underlying data source.
* @property account The account to which this message belongs.
* @property senders The composed representation of the message sender(s) with display name,
* styling, and avatar.
Expand All @@ -32,6 +34,7 @@ import androidx.compose.runtime.Immutable
data class MessageItemUi(
val state: State,
val id: String,
val messageReference: String,
val account: Account,
val senders: ComposedAddressUi,
val subject: String,
Expand Down Expand Up @@ -63,3 +66,29 @@ data class MessageItemUi(
Unread,
}
}

/**
* Create a copy of this message item with the updated [state] and refreshed sender
* display name styling.
*
* @param state The new state to apply.
* @return A new [MessageItemUi] instance with the updated state and styling.
*/
fun MessageItemUi.withState(state: MessageItemUi.State): MessageItemUi {
val styles = buildList {
when (val separatorIndex = senders.displayName.indexOf(',')) {
-1 if state != MessageItemUi.State.Read -> add(ComposedAddressStyle.Bold(start = 0))
in 0..Int.MAX_VALUE if state != MessageItemUi.State.Read -> {
add(ComposedAddressStyle.Bold(start = 0, end = separatorIndex))
add(ComposedAddressStyle.Regular(start = separatorIndex))
}

else -> add(ComposedAddressStyle.Regular(start = 0))
}
}.toPersistentList()

return copy(
senders = senders.copy(displayNameStyles = styles),
state = state,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ data class MessageListMetadata(
val folder: Folder?,
val swipeActions: ImmutableMap<AccountId, SwipeActions>,
val sortCriteriaPerAccount: ImmutableMap<AccountId?, SortCriteria>,
val activeMessage: MessageItemUi?,
val isActive: Boolean,
val activeMessage: MessageItemUi? = null,
val focusedMessage: MessageItemUi? = null,
val availablePrimarySortTypes: ImmutableSet<SortType> = SortType.entries.toPersistentSet(),
val availableSecondarySortTypes: ImmutableSet<SortType> = SortCriteria.DateSortTypeOnly.toPersistentSet(),
val footer: MessageListFooter = MessageListFooter(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import net.thunderbird.feature.mail.message.list.preferences.MessageListPreferences

/**
Expand Down Expand Up @@ -74,6 +75,26 @@ sealed interface MessageListState {
is WarmingUp -> copy(preferences = preferences?.transform())
}

/**
* Creates a copy of the current state with an updated list of [messages], preserving all other
* properties.
*
* @param transform A lambda function that receives the current [ImmutableList] of [MessageItemUi]
* and returns a new, transformed list.
* @return A new [MessageListState] instance of the same type as the original, containing the
* transformed messages.
*/
fun mapMessages(transform: (MessageItemUi) -> MessageItemUi): MessageListState {
val messages = messages.map(transform).toImmutableList()
return when (this) {
is LoadedMessages -> copy(messages = messages)
is LoadingMessages -> copy(messages = messages)
is SearchingMessages -> copy(messages = messages)
is SelectingMessages -> copy(messages = messages)
is WarmingUp -> copy(messages = messages)
}
}

/**
* Represents the initial state of the message list screen before any messages are loaded.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ internal object MessagePreviewHelper {
forwarded = forwarded,
selected = selected,
threadCount = threadCount,
messageReference = "reference",
)

val sampleMessages = persistentListOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package net.thunderbird.feature.mail.message.list.internal
import net.thunderbird.core.android.account.LegacyAccountDto
import net.thunderbird.feature.mail.message.list.LocalDeleteOperationDecider

class DefaultLocalDeleteOperationDecider : LocalDeleteOperationDecider {
internal class DefaultLocalDeleteOperationDecider : LocalDeleteOperationDecider {
override fun isDeleteImmediately(
account: LegacyAccountDto,
folderId: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ val featureMessageListModule = module {
logger = get(),
messageListStateMachineFactory = get(),
stateSideEffectHandlersFactories = getList { parameters },
stringsResourceManager = get(),
)
}
single<LocalDeleteOperationDecider> { DefaultLocalDeleteOperationDecider() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import net.thunderbird.feature.mail.message.list.R as ApiR
* swipe actions are available.
*/
@Stable
class MessageListScreenAccessibilityState(
internal class MessageListScreenAccessibilityState(
private val stateDescription: Map<MessageListStateDescription, String>,
val swipeDirectionAccessibilityAction: ImmutableList<SwipeDirectionAccessibilityAction> = persistentListOf(),
) {
Expand Down Expand Up @@ -97,7 +97,9 @@ enum class MessageListStateDescription {
* description strings or swipe actions change.
*/
@Composable
fun rememberMessageListScreenAccessibilityState(swipeActions: SwipeActions?): MessageListScreenAccessibilityState {
internal fun rememberMessageListScreenAccessibilityState(
swipeActions: SwipeActions?,
): MessageListScreenAccessibilityState {
val stateDescription = mapOf(
MessageListStateDescription.NewMessage to stringResource(
id = R.string.message_list_state_new_message_description,
Expand Down
Loading
Loading