From cb5f29aa3007313cf4101f5526e0a5a7a2e4f68e Mon Sep 17 00:00:00 2001 From: stan <26108664+talanov@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:00:52 -0500 Subject: [PATCH] Highlight the open conversation and selected conversations in the two-pane conversation list and archived list so they stand out from the list pane (fixes #379) --- .../unit/measured-composable-sources.txt | 1 + .../ArchivedConversationListViewModelTest.kt | 127 ++++++++++++++ ...edConversationListUiStateMapperImplTest.kt | 129 ++++++++++++++ .../item/ConversationListItemRowTest.kt | 158 ++++++++++++++++++ .../common/pane/ListPaneColorsTest.kt | 74 ++++++++ .../ArchivedConversationListScreen.kt | 5 + .../ArchivedConversationListViewModel.kt | 16 +- .../ArchivedConversationListUiStateMapper.kt | 4 +- .../model/ArchivedConversationListAction.kt | 8 + .../common/item/ConversationListItemRow.kt | 3 +- .../common/pane/ListPaneColors.kt | 10 ++ .../navigation/ConversationListNavEntries.kt | 1 + 12 files changed, 533 insertions(+), 3 deletions(-) create mode 100644 app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModelTest.kt create mode 100644 app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapperImplTest.kt create mode 100644 app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRowTest.kt create mode 100644 app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/pane/ListPaneColorsTest.kt diff --git a/app/jacoco-rules/unit/measured-composable-sources.txt b/app/jacoco-rules/unit/measured-composable-sources.txt index ff723a9bb..87f0b6808 100644 --- a/app/jacoco-rules/unit/measured-composable-sources.txt +++ b/app/jacoco-rules/unit/measured-composable-sources.txt @@ -61,6 +61,7 @@ conversation/screen/ConversationScreenRoute.kt conversation/screen/ConversationSelectionTopAppBar.kt conversation/screen/ConversationSimSheetState.kt conversation/screen/ConversationTargetMessageEffects.kt +conversationlist/common/pane/ListPaneColors.kt recipientselection/component/MultiDestinationContactRow.kt recipientselection/component/RecipientSelectionContactAvatar.kt recipientselection/component/RecipientSelectionContactRow.kt diff --git a/app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModelTest.kt b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModelTest.kt new file mode 100644 index 000000000..1671a8fcc --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModelTest.kt @@ -0,0 +1,127 @@ +package com.android.messaging.ui.conversationlist.archived + +import app.cash.turbine.test +import com.android.messaging.data.conversation.model.ConversationId +import com.android.messaging.data.conversationlist.model.ConversationListSnapshot +import com.android.messaging.domain.conversation.usecase.participant.model.ResolveContactActionResult +import com.android.messaging.testutil.MainDispatcherRule +import com.android.messaging.ui.conversationlist.archived.mapper.ArchivedConversationListUiStateMapper +import com.android.messaging.ui.conversationlist.archived.model.ArchivedConversationListAction as Action +import com.android.messaging.ui.conversationlist.archived.model.ArchivedConversationListUiState as State +import com.android.messaging.ui.conversationlist.conversationItem +import com.android.messaging.ui.conversationlist.delegate.ConversationListOptimisticSnapshotDelegate +import com.android.messaging.ui.conversationlist.delegate.ConversationListSelectionDelegate +import com.android.messaging.ui.conversationlist.snapshotOf +import io.mockk.MockKMatcherScope +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class ArchivedConversationListViewModelTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private val uiStateMapper = mockk() + private val optimisticSnapshotDelegate = + mockk(relaxed = true) + private val selectionDelegate = mockk(relaxed = true) + + private val snapshotFlow = MutableStateFlow( + snapshotOf(conversationItem(conversationId = OPENED_ID, isArchived = true)), + ) + private val selectedIdsFlow = MutableStateFlow>( + persistentListOf(), + ) + + @Test + fun uiState_noOpenedConversation_mapsWithoutOpenedConversation() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + + viewModel.uiState.test { + advanceUntilIdle() + + verify { mapWithOpenedConversation(openedConversationId = null) } + cancelAndIgnoreRemainingEvents() + } + } + } + + @Test + fun openedConversationChanged_toConversation_remapsWithOpenedConversation() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + + viewModel.uiState.test { + viewModel.onAction(Action.OpenedConversationChanged(OPENED_ID)) + advanceUntilIdle() + + verify { mapWithOpenedConversation(openedConversationId = OPENED_ID) } + cancelAndIgnoreRemainingEvents() + } + } + } + + @Test + fun openedConversationChanged_toNull_remapsWithoutOpenedConversation() { + runTest(context = mainDispatcherRule.testDispatcher) { + val viewModel = createViewModel() + + viewModel.uiState.test { + viewModel.onAction(Action.OpenedConversationChanged(OPENED_ID)) + advanceUntilIdle() + + viewModel.onAction(Action.OpenedConversationChanged(null)) + advanceUntilIdle() + + verifyOrder { + mapWithOpenedConversation(openedConversationId = OPENED_ID) + mapWithOpenedConversation(openedConversationId = null) + } + cancelAndIgnoreRemainingEvents() + } + } + } + + private fun MockKMatcherScope.mapWithOpenedConversation( + openedConversationId: ConversationId?, + ): State { + return uiStateMapper.map( + snapshot = any(), + selectedConversationIds = any(), + openedConversationId = openedConversationId, + isDebugEnabled = false, + ) + } + + private fun createViewModel(): ArchivedConversationListViewModel { + every { optimisticSnapshotDelegate.snapshot } returns snapshotFlow + every { selectionDelegate.selectedIds } returns selectedIdsFlow + every { uiStateMapper.map(any(), any(), any(), any()) } returns State() + + return ArchivedConversationListViewModel( + actionsDelegate = mockk(relaxed = true), + optimisticSnapshotDelegate = optimisticSnapshotDelegate, + selectionDelegate = selectionDelegate, + uiStateMapper = uiStateMapper, + resolveContactAction = { _, _, _ -> ResolveContactActionResult.Unavailable }, + debugFeaturesProvider = { false }, + defaultDispatcher = mainDispatcherRule.testDispatcher, + ) + } + + private companion object { + private val OPENED_ID = ConversationId("opened") + } +} diff --git a/app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapperImplTest.kt b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapperImplTest.kt new file mode 100644 index 000000000..ec1541cdf --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapperImplTest.kt @@ -0,0 +1,129 @@ +package com.android.messaging.ui.conversationlist.archived.mapper + +import android.content.Context +import com.android.messaging.data.conversation.model.ConversationId +import com.android.messaging.data.conversationlist.model.ConversationListSnapshot +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter +import com.android.messaging.domain.conversation.usecase.avatar.ResolveAvatarUri +import com.android.messaging.domain.conversation.usecase.participant.CanShowOrAddContact +import com.android.messaging.domain.conversation.usecase.participant.IsContactSaved +import com.android.messaging.domain.conversation.usecase.telephony.CanPlacePhoneCall +import com.android.messaging.ui.conversationlist.archived.model.ArchivedConversationListUiState +import com.android.messaging.ui.conversationlist.conversationItem +import com.android.messaging.ui.conversationlist.mapper.ConversationListContentUiStateMapperImpl +import com.android.messaging.ui.conversationlist.mapper.ConversationListItemUiMapperImpl +import com.android.messaging.ui.conversationlist.model.ConversationListContentUiState +import com.android.messaging.ui.conversationlist.model.ConversationListItemUiModel +import com.android.messaging.ui.conversationlist.snapshotOf +import com.android.messaging.util.OsUtil +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import kotlinx.collections.immutable.persistentListOf +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class ArchivedConversationListUiStateMapperImplTest { + + private val itemUiMapper = ConversationListItemUiMapperImpl( + context = mockk(relaxed = true), + canPlacePhoneCall = mockk(relaxed = true), + canShowOrAddContact = mockk(relaxed = true), + isContactSaved = mockk(relaxed = true), + phoneNumberFormatter = mockk(relaxed = true), + resolveAvatarUri = mockk(relaxed = true), + ) + + private val mapper = ArchivedConversationListUiStateMapperImpl( + contentMapper = ConversationListContentUiStateMapperImpl( + itemUiMapper = itemUiMapper, + ), + ) + + @Before + fun setUp() { + mockkStatic(OsUtil::class) + every { OsUtil.isSecondaryUser() } returns false + } + + @After + fun tearDown() { + unmockkStatic(OsUtil::class) + } + + @Test + fun map_openedConversation_marksOnlyThatItemOpened() { + val state = mapper.map( + snapshot = archivedSnapshot(), + selectedConversationIds = persistentListOf(), + openedConversationId = SECOND_ID, + isDebugEnabled = false, + ) + + assertEquals(listOf(SECOND_ID), openedIds(state)) + } + + @Test + fun map_noOpenedConversation_marksNoItemOpened() { + val state = mapper.map( + snapshot = archivedSnapshot(), + selectedConversationIds = persistentListOf(), + openedConversationId = null, + isDebugEnabled = false, + ) + + assertEquals(emptyList(), openedIds(state)) + } + + @Test + fun map_openedConversationNotArchived_marksNoItemOpened() { + val state = mapper.map( + snapshot = archivedSnapshot(), + selectedConversationIds = persistentListOf(), + openedConversationId = ConversationId("inbox"), + isDebugEnabled = false, + ) + + assertEquals(emptyList(), openedIds(state)) + } + + @Test + fun map_openedAndSelectedConversations_keepsBothFlagsAndCountsSelection() { + val state = mapper.map( + snapshot = archivedSnapshot(), + selectedConversationIds = persistentListOf(FIRST_ID), + openedConversationId = SECOND_ID, + isDebugEnabled = false, + ) + + val items = items(state) + assertEquals(listOf(FIRST_ID), items.filter { it.isSelected }.map { it.conversationId }) + assertEquals(listOf(SECOND_ID), items.filter { it.isOpened }.map { it.conversationId }) + assertEquals(1, state.selectedCount) + } + + private fun archivedSnapshot(): ConversationListSnapshot { + return snapshotOf( + conversationItem(conversationId = FIRST_ID, isArchived = true), + conversationItem(conversationId = SECOND_ID, isArchived = true), + ) + } + + private fun openedIds(state: ArchivedConversationListUiState): List { + return items(state) + .filter(ConversationListItemUiModel::isOpened) + .map(ConversationListItemUiModel::conversationId) + } + + private fun items(state: ArchivedConversationListUiState): List { + return (state.content as ConversationListContentUiState.Items).items + } + + private companion object { + private val FIRST_ID = ConversationId("first") + private val SECOND_ID = ConversationId("second") + } +} diff --git a/app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRowTest.kt b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRowTest.kt new file mode 100644 index 000000000..5d44931f6 --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRowTest.kt @@ -0,0 +1,158 @@ +package com.android.messaging.ui.conversationlist.common.item + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.toPixelMap +import androidx.compose.ui.test.captureToImage +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.dp +import com.android.common.test.helpers.targetContext +import com.android.messaging.FactoryTestAccess +import com.android.messaging.data.conversation.model.ConversationId +import com.android.messaging.testutil.installTestFactory +import com.android.messaging.ui.common.components.LocalIsListDetailPane +import com.android.messaging.ui.conversationlist.common.support.conversationListItemTestTag +import com.android.messaging.ui.conversationlist.common.support.previewConversationListItem +import com.android.messaging.ui.conversationlist.model.ConversationListItemUiModel +import com.android.messaging.ui.core.AppTheme +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@RunWith(RobolectricTestRunner::class) +internal class ConversationListItemRowTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun setUp() { + installTestFactory(context = targetContext) + } + + @After + fun tearDown() { + FactoryTestAccess.reset() + } + + @Test + fun openedRow_inListDetailPane_standsOutFromPaneAndOtherRows() { + val colorScheme = setRow( + item = rowItem().copy(isOpened = true), + isListDetailPane = true, + ) + + val rowColor = rowContainerColor() + assertColorEquals(expected = colorScheme.secondaryContainer, actual = rowColor) + assertNotEquals(colorScheme.surfaceContainerLow.toArgb(), rowColor.toArgb()) + assertNotEquals(colorScheme.background.toArgb(), rowColor.toArgb()) + } + + @Test + @Config(qualifiers = "night") + fun openedRow_inListDetailPaneDarkTheme_standsOutFromPaneAndOtherRows() { + val colorScheme = setRow( + item = rowItem().copy(isOpened = true), + isListDetailPane = true, + ) + + val rowColor = rowContainerColor() + assertColorEquals(expected = colorScheme.secondaryContainer, actual = rowColor) + assertNotEquals(colorScheme.surfaceContainerLow.toArgb(), rowColor.toArgb()) + assertNotEquals(colorScheme.background.toArgb(), rowColor.toArgb()) + } + + @Test + fun selectedRow_inListDetailPane_usesHighlightColor() { + val colorScheme = setRow( + item = rowItem(isSelected = true), + isListDetailPane = true, + ) + + assertColorEquals(expected = colorScheme.secondaryContainer, actual = rowContainerColor()) + } + + @Test + fun plainRow_inListDetailPane_usesBackground() { + val colorScheme = setRow( + item = rowItem(), + isListDetailPane = true, + ) + + assertColorEquals(expected = colorScheme.background, actual = rowContainerColor()) + } + + @Test + fun selectedRow_inSinglePane_keepsSurfaceContainerLow() { + val colorScheme = setRow( + item = rowItem(isSelected = true), + isListDetailPane = false, + ) + + assertColorEquals(expected = colorScheme.surfaceContainerLow, actual = rowContainerColor()) + } + + private fun setRow( + item: ConversationListItemUiModel, + isListDetailPane: Boolean, + ): ColorScheme { + lateinit var colorScheme: ColorScheme + + composeTestRule.setContent { + AppTheme { + colorScheme = MaterialTheme.colorScheme + + CompositionLocalProvider(LocalIsListDetailPane provides isListDetailPane) { + ConversationListItemRow( + item = item, + onClick = {}, + onLongClick = {}, + ) + } + } + } + composeTestRule.waitForIdle() + + return colorScheme + } + + // Samples the row's trailing padding at mid-height, where only the container is drawn. + private fun rowContainerColor(): Color { + val image = composeTestRule + .onNodeWithTag(conversationListItemTestTag(ROW_ID)) + .captureToImage() + val inset = with(composeTestRule.density) { CONTAINER_SAMPLE_INSET.roundToPx() } + + return image.toPixelMap()[image.width - inset, image.height / 2] + } + + private fun assertColorEquals(expected: Color, actual: Color) { + assertEquals(expected.toArgb(), actual.toArgb()) + } + + private fun rowItem(isSelected: Boolean = false): ConversationListItemUiModel { + return previewConversationListItem( + conversationId = ROW_ID, + title = "Ada Lovelace", + snippetText = "Sounds good, thanks!", + isSelected = isSelected, + ) + } + + private companion object { + private val ROW_ID = ConversationId("row") + private val CONTAINER_SAMPLE_INSET = 4.dp + } +} diff --git a/app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/pane/ListPaneColorsTest.kt b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/pane/ListPaneColorsTest.kt new file mode 100644 index 000000000..5dfc24885 --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/ui/conversationlist/common/pane/ListPaneColorsTest.kt @@ -0,0 +1,74 @@ +package com.android.messaging.ui.conversationlist.common.pane + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.v2.createComposeRule +import com.android.messaging.ui.common.components.LocalIsListDetailPane +import com.android.messaging.ui.core.AppTheme +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class ListPaneColorsTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun listPaneContentColor_listDetailPane_usesSurfaceContainerLow() { + val colors = resolveColors(isListDetailPane = true) + + assertEquals(colors.colorScheme.surfaceContainerLow, colors.content) + } + + @Test + fun listPaneContentColor_singlePane_usesBackground() { + val colors = resolveColors(isListDetailPane = false) + + assertEquals(colors.colorScheme.background, colors.content) + } + + @Test + fun listPaneHighlightedItemColor_listDetailPane_usesSecondaryContainer() { + val colors = resolveColors(isListDetailPane = true) + + assertEquals(colors.colorScheme.secondaryContainer, colors.highlightedItem) + } + + @Test + fun listPaneHighlightedItemColor_singlePane_usesSurfaceContainerLow() { + val colors = resolveColors(isListDetailPane = false) + + assertEquals(colors.colorScheme.surfaceContainerLow, colors.highlightedItem) + } + + private fun resolveColors(isListDetailPane: Boolean): ResolvedColors { + lateinit var colors: ResolvedColors + + composeTestRule.setContent { + AppTheme { + CompositionLocalProvider(LocalIsListDetailPane provides isListDetailPane) { + colors = ResolvedColors( + colorScheme = MaterialTheme.colorScheme, + content = listPaneContentColor(), + highlightedItem = listPaneHighlightedItemColor(), + ) + } + } + } + composeTestRule.waitForIdle() + + return colors + } + + private data class ResolvedColors( + val colorScheme: ColorScheme, + val content: Color, + val highlightedItem: Color, + ) +} diff --git a/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListScreen.kt b/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListScreen.kt index 924ead5a2..c0e360114 100644 --- a/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListScreen.kt +++ b/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListScreen.kt @@ -68,6 +68,7 @@ internal fun ArchivedConversationListScreen( onNavigateToConversationSettings: (ConversationId) -> Unit, onCloseConversation: (ConversationId) -> Unit, onCloseArchivedList: () -> Unit, + openedConversationId: ConversationId?, modifier: Modifier = Modifier, ) { val uiState by screenModel.uiState.collectAsStateWithLifecycle() @@ -76,6 +77,10 @@ internal fun ArchivedConversationListScreen( var pendingDelete by remember { mutableStateOf(false) } + LaunchedEffect(openedConversationId) { + screenModel.onAction(Action.OpenedConversationChanged(openedConversationId)) + } + CollectEvents(events = screenModel.navigationEvents) { event -> when (event) { is NavEvent.CloseArchivedList -> { diff --git a/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModel.kt b/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModel.kt index ba5630db7..c46c8d322 100644 --- a/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModel.kt +++ b/src/com/android/messaging/ui/conversationlist/archived/ArchivedConversationListViewModel.kt @@ -26,6 +26,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -64,6 +65,8 @@ internal class ArchivedConversationListViewModel @Inject constructor( private val isDebugEnabled = debugFeaturesProvider.isEnabled() + private val openedConversationId = MutableStateFlow(value = null) + private val _effects = Channel(Channel.BUFFERED) override val effects: Flow = _effects.receiveAsFlow() @@ -73,10 +76,12 @@ internal class ArchivedConversationListViewModel @Inject constructor( override val uiState: StateFlow = combine( snapshot.filterNotNull(), selectionDelegate.selectedIds, - ) { snapshot, selectedIds -> + openedConversationId, + ) { snapshot, selectedIds, openedId -> uiStateMapper.map( snapshot = snapshot, selectedConversationIds = selectedIds, + openedConversationId = openedId, isDebugEnabled = isDebugEnabled, ) } @@ -125,6 +130,7 @@ internal class ArchivedConversationListViewModel @Inject constructor( is Action.NavigationAction -> onNavigationAction(action) is Action.SelectionAction -> onSelectionAction(action) is Action.SnackbarAction -> onSnackbarAction(action) + is Action.PaneStateAction -> onPaneStateAction(action) } } @@ -198,6 +204,14 @@ internal class ArchivedConversationListViewModel @Inject constructor( } } + private fun onPaneStateAction(action: Action.PaneStateAction) { + when (action) { + is Action.OpenedConversationChanged -> { + openedConversationId.value = action.conversationId + } + } + } + private fun onConversationClicked(conversationId: ConversationId) { when { selectionDelegate.selectedIds.value.isNotEmpty() -> { diff --git a/src/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapper.kt b/src/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapper.kt index f55218dfe..ca26c5b7c 100644 --- a/src/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapper.kt +++ b/src/com/android/messaging/ui/conversationlist/archived/mapper/ArchivedConversationListUiStateMapper.kt @@ -11,6 +11,7 @@ internal interface ArchivedConversationListUiStateMapper { fun map( snapshot: ConversationListSnapshot, selectedConversationIds: ImmutableList, + openedConversationId: ConversationId?, isDebugEnabled: Boolean, ): State } @@ -22,12 +23,13 @@ internal class ArchivedConversationListUiStateMapperImpl @Inject constructor( override fun map( snapshot: ConversationListSnapshot, selectedConversationIds: ImmutableList, + openedConversationId: ConversationId?, isDebugEnabled: Boolean, ): State { val content = contentMapper.map( snapshot = snapshot, selectedConversationIds = selectedConversationIds, - openedConversationId = null, + openedConversationId = openedConversationId, ) return State( diff --git a/src/com/android/messaging/ui/conversationlist/archived/model/ArchivedConversationListAction.kt b/src/com/android/messaging/ui/conversationlist/archived/model/ArchivedConversationListAction.kt index 0e3142683..a07447ce2 100644 --- a/src/com/android/messaging/ui/conversationlist/archived/model/ArchivedConversationListAction.kt +++ b/src/com/android/messaging/ui/conversationlist/archived/model/ArchivedConversationListAction.kt @@ -15,6 +15,8 @@ internal sealed interface ArchivedConversationListAction { sealed interface SnackbarAction : ArchivedConversationListAction + sealed interface PaneStateAction : ArchivedConversationListAction + // region ConfirmationAction data object DeleteSelectedConfirmed : ConfirmationAction // endregion @@ -67,4 +69,10 @@ internal sealed interface ArchivedConversationListAction { val conversationIds: List, ) : SnackbarAction // endregion + + // region PaneStateAction + data class OpenedConversationChanged( + val conversationId: ConversationId?, + ) : PaneStateAction + // endregion } diff --git a/src/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRow.kt b/src/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRow.kt index 3a6607e75..e6276d19e 100644 --- a/src/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRow.kt +++ b/src/com/android/messaging/ui/conversationlist/common/item/ConversationListItemRow.kt @@ -33,6 +33,7 @@ import com.android.messaging.R import com.android.messaging.data.conversation.model.ConversationId import com.android.messaging.data.conversationlist.model.ConversationListMessageStatus import com.android.messaging.ui.common.components.TwoLineListItem +import com.android.messaging.ui.conversationlist.common.pane.listPaneHighlightedItemColor import com.android.messaging.ui.conversationlist.common.support.conversationListItemTestTag import com.android.messaging.ui.conversationlist.common.support.previewConversationListItem import com.android.messaging.ui.conversationlist.model.ConversationListItemUiModel @@ -247,7 +248,7 @@ private fun itemContainerColor(item: ConversationListItemUiModel): Color { val isHighlighted = item.isSelected || item.isOpened return when { - isHighlighted -> MaterialTheme.colorScheme.surfaceContainerLow + isHighlighted -> listPaneHighlightedItemColor() else -> MaterialTheme.colorScheme.background } } diff --git a/src/com/android/messaging/ui/conversationlist/common/pane/ListPaneColors.kt b/src/com/android/messaging/ui/conversationlist/common/pane/ListPaneColors.kt index 3deeb7e7c..f0dcfbc5e 100644 --- a/src/com/android/messaging/ui/conversationlist/common/pane/ListPaneColors.kt +++ b/src/com/android/messaging/ui/conversationlist/common/pane/ListPaneColors.kt @@ -12,3 +12,13 @@ internal fun listPaneContentColor(): Color { else -> MaterialTheme.colorScheme.background } } + +// The two-pane list already sits on surfaceContainerLow, so a highlighted row needs a color of +// its own to stand out from both the pane and the unhighlighted rows. +@Composable +internal fun listPaneHighlightedItemColor(): Color { + return when { + LocalIsListDetailPane.current -> MaterialTheme.colorScheme.secondaryContainer + else -> MaterialTheme.colorScheme.surfaceContainerLow + } +} diff --git a/src/com/android/messaging/ui/conversationlist/navigation/ConversationListNavEntries.kt b/src/com/android/messaging/ui/conversationlist/navigation/ConversationListNavEntries.kt index 0360710d2..c4cbe0117 100644 --- a/src/com/android/messaging/ui/conversationlist/navigation/ConversationListNavEntries.kt +++ b/src/com/android/messaging/ui/conversationlist/navigation/ConversationListNavEntries.kt @@ -124,6 +124,7 @@ private fun archivedConversationListRouteContent(): onCloseArchivedList = { navigator.removeDestination(destination = ArchivedConversationListNavKey) }, + openedConversationId = openedConversationId(navigator = navigator), modifier = Modifier .fillMaxSize() .consumeOppositePaneInsets(),