From 8973d3ef60cfe6c51b2d91a8bcd637abdf87d8dd Mon Sep 17 00:00:00 2001 From: Noah Bond Date: Wed, 26 Apr 2023 10:35:58 -0700 Subject: [PATCH] Bug 1822192 - Add empty state UI for Normal and Private tab pages --- .../org/mozilla/fenix/tabstray/TabsTray.kt | 274 ++++++++++++------ 1 file changed, 188 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt index 2ac1b90613..0f46716276 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -17,21 +18,25 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState +import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.sync.TabEntry import mozilla.components.lib.state.ext.observeAsComposableState +import org.mozilla.fenix.R import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.compose.Divider @@ -93,20 +98,12 @@ fun TabsTray( onInactiveTabClose: (TabSessionState) -> Unit, onSyncedTabClick: (SyncTab) -> Unit, ) { - val selectedTabId = browserStore - .observeAsComposableState { state -> state.selectedTabId }.value + val normalTabCount = browserStore + .observeAsComposableState { state -> state.normalTabs.size }.value ?: 0 val multiselectMode = tabsTrayStore .observeAsComposableState { state -> state.mode }.value ?: TabsTrayState.Mode.Normal val selectedPage = tabsTrayStore .observeAsComposableState { state -> state.selectedPage }.value ?: Page.NormalTabs - val normalTabs = tabsTrayStore - .observeAsComposableState { state -> state.normalTabs }.value ?: emptyList() - val privateTabs = tabsTrayStore - .observeAsComposableState { state -> state.privateTabs }.value ?: emptyList() - val inactiveTabsExpanded = appStore - .observeAsComposableState { state -> state.inactiveTabsExpanded }.value ?: false - val inactiveTabs = tabsTrayStore - .observeAsComposableState { state -> state.inactiveTabs }.value ?: emptyList() val pagerState = rememberPagerState(initialPage = selectedPage.ordinal) val isInMultiSelectMode = multiselectMode is TabsTrayState.Mode.Select @@ -138,7 +135,7 @@ fun TabsTray( TabsTrayBanner( selectMode = multiselectMode, selectedPage = selectedPage, - normalTabCount = normalTabs.size + inactiveTabs.size, + normalTabCount = normalTabCount, isInDebugMode = isInDebugMode, onTabPageIndicatorClicked = onTabPageClick, ) @@ -155,85 +152,43 @@ fun TabsTray( ) { position -> when (Page.positionToPage(position)) { Page.NormalTabs -> { - if (normalTabs.isNotEmpty() || inactiveTabs.isNotEmpty()) { - val showInactiveTabsAutoCloseDialog = - shouldShowInactiveTabsAutoCloseDialog(inactiveTabs.size) - var showAutoCloseDialog by remember { mutableStateOf(showInactiveTabsAutoCloseDialog) } - - val optionalInactiveTabsHeader: (@Composable () -> Unit)? = if (inactiveTabs.isEmpty()) { - null - } else { - { - InactiveTabsList( - inactiveTabs = inactiveTabs, - expanded = inactiveTabsExpanded, - showAutoCloseDialog = showAutoCloseDialog, - onHeaderClick = onInactiveTabsHeaderClick, - onDeleteAllButtonClick = onDeleteAllInactiveTabsClick, - onAutoCloseDismissClick = { - onInactiveTabAutoCloseDialogCloseButtonClick() - showAutoCloseDialog = !showAutoCloseDialog - }, - onEnableAutoCloseClick = { - onEnableInactiveTabAutoCloseClick() - showAutoCloseDialog = !showAutoCloseDialog - }, - onTabClick = onInactiveTabClick, - onTabCloseClick = onInactiveTabClose, - ) - } - } - - if (showInactiveTabsAutoCloseDialog) { - onInactiveTabsAutoCloseDialogShown() - } - - TabLayout( - tabs = normalTabs, - displayTabsInGrid = displayTabsInGrid, - selectedTabId = selectedTabId, - selectionMode = multiselectMode, - onTabClose = onTabClose, - onTabMediaClick = onTabMediaClick, - onTabClick = handleTabClick, - onTabLongClick = onTabLongClick, - header = optionalInactiveTabsHeader, - ) - } else { - Text( - text = "Empty state", - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.body1, - ) - } + NormalTabsPage( + appStore = appStore, + browserStore = browserStore, + tabsTrayStore = tabsTrayStore, + displayTabsInGrid = displayTabsInGrid, + selectionMode = multiselectMode, + onTabClose = onTabClose, + onTabMediaClick = onTabMediaClick, + onTabClick = handleTabClick, + onTabLongClick = onTabLongClick, + shouldShowInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog, + onInactiveTabsHeaderClick = onInactiveTabsHeaderClick, + onDeleteAllInactiveTabsClick = onDeleteAllInactiveTabsClick, + onInactiveTabsAutoCloseDialogShown = onInactiveTabsAutoCloseDialogShown, + onInactiveTabAutoCloseDialogCloseButtonClick = onInactiveTabAutoCloseDialogCloseButtonClick, + onEnableInactiveTabAutoCloseClick = onEnableInactiveTabAutoCloseClick, + onInactiveTabClick = onInactiveTabClick, + onInactiveTabClose = onInactiveTabClose, + ) } + Page.PrivateTabs -> { - if (privateTabs.isNotEmpty()) { - TabLayout( - tabs = privateTabs, - displayTabsInGrid = displayTabsInGrid, - selectedTabId = selectedTabId, - selectionMode = multiselectMode, - onTabClose = onTabClose, - onTabMediaClick = onTabMediaClick, - onTabClick = handleTabClick, - onTabLongClick = onTabLongClick, - ) - } else { - Text( - text = "Empty state", - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.body1, - ) - } + PrivateTabsPage( + browserStore = browserStore, + tabsTrayStore = tabsTrayStore, + displayTabsInGrid = displayTabsInGrid, + selectionMode = multiselectMode, + onTabClose = onTabClose, + onTabMediaClick = onTabMediaClick, + onTabClick = handleTabClick, + onTabLongClick = onTabLongClick, + ) } - Page.SyncedTabs -> { - val syncedTabs = tabsTrayStore - .observeAsComposableState { state -> state.syncedTabs }.value ?: emptyList() - SyncedTabsList( - syncedTabs = syncedTabs, - taskContinuityEnabled = true, + Page.SyncedTabs -> { + SyncedTabsPage( + tabsTrayStore = tabsTrayStore, onTabClick = onSyncedTabClick, ) } @@ -243,6 +198,153 @@ fun TabsTray( } } +@Composable +@Suppress("LongParameterList") +private fun NormalTabsPage( + appStore: AppStore, + browserStore: BrowserStore, + tabsTrayStore: TabsTrayStore, + displayTabsInGrid: Boolean, + selectionMode: TabsTrayState.Mode, + onTabClose: (TabSessionState) -> Unit, + onTabMediaClick: (TabSessionState) -> Unit, + onTabClick: (TabSessionState) -> Unit, + onTabLongClick: (TabSessionState) -> Unit, + shouldShowInactiveTabsAutoCloseDialog: (Int) -> Boolean, + onInactiveTabsHeaderClick: (Boolean) -> Unit, + onDeleteAllInactiveTabsClick: () -> Unit, + onInactiveTabsAutoCloseDialogShown: () -> Unit, + onInactiveTabAutoCloseDialogCloseButtonClick: () -> Unit, + onEnableInactiveTabAutoCloseClick: () -> Unit, + onInactiveTabClick: (TabSessionState) -> Unit, + onInactiveTabClose: (TabSessionState) -> Unit, +) { + val inactiveTabsExpanded = appStore + .observeAsComposableState { state -> state.inactiveTabsExpanded }.value ?: false + val selectedTabId = browserStore + .observeAsComposableState { state -> state.selectedTabId }.value + val normalTabs = tabsTrayStore + .observeAsComposableState { state -> state.normalTabs }.value ?: emptyList() + val inactiveTabs = tabsTrayStore + .observeAsComposableState { state -> state.inactiveTabs }.value ?: emptyList() + + if (normalTabs.isNotEmpty() || inactiveTabs.isNotEmpty()) { + val showInactiveTabsAutoCloseDialog = + shouldShowInactiveTabsAutoCloseDialog(inactiveTabs.size) + var showAutoCloseDialog by remember { mutableStateOf(showInactiveTabsAutoCloseDialog) } + + val optionalInactiveTabsHeader: (@Composable () -> Unit)? = if (inactiveTabs.isEmpty()) { + null + } else { + { + InactiveTabsList( + inactiveTabs = inactiveTabs, + expanded = inactiveTabsExpanded, + showAutoCloseDialog = showAutoCloseDialog, + onHeaderClick = onInactiveTabsHeaderClick, + onDeleteAllButtonClick = onDeleteAllInactiveTabsClick, + onAutoCloseDismissClick = { + onInactiveTabAutoCloseDialogCloseButtonClick() + showAutoCloseDialog = !showAutoCloseDialog + }, + onEnableAutoCloseClick = { + onEnableInactiveTabAutoCloseClick() + showAutoCloseDialog = !showAutoCloseDialog + }, + onTabClick = onInactiveTabClick, + onTabCloseClick = onInactiveTabClose, + ) + } + } + + if (showInactiveTabsAutoCloseDialog) { + onInactiveTabsAutoCloseDialogShown() + } + + TabLayout( + tabs = normalTabs, + displayTabsInGrid = displayTabsInGrid, + selectedTabId = selectedTabId, + selectionMode = selectionMode, + onTabClose = onTabClose, + onTabMediaClick = onTabMediaClick, + onTabClick = onTabClick, + onTabLongClick = onTabLongClick, + header = optionalInactiveTabsHeader, + ) + } else { + EmptyTabPage(isPrivate = false) + } +} + +@Composable +@Suppress("LongParameterList") +private fun PrivateTabsPage( + browserStore: BrowserStore, + tabsTrayStore: TabsTrayStore, + displayTabsInGrid: Boolean, + selectionMode: TabsTrayState.Mode, + onTabClose: (TabSessionState) -> Unit, + onTabMediaClick: (TabSessionState) -> Unit, + onTabClick: (TabSessionState) -> Unit, + onTabLongClick: (TabSessionState) -> Unit, +) { + val selectedTabId = browserStore + .observeAsComposableState { state -> state.selectedTabId }.value + val privateTabs = tabsTrayStore + .observeAsComposableState { state -> state.privateTabs }.value ?: emptyList() + + if (privateTabs.isNotEmpty()) { + TabLayout( + tabs = privateTabs, + displayTabsInGrid = displayTabsInGrid, + selectedTabId = selectedTabId, + selectionMode = selectionMode, + onTabClose = onTabClose, + onTabMediaClick = onTabMediaClick, + onTabClick = onTabClick, + onTabLongClick = onTabLongClick, + ) + } else { + EmptyTabPage(isPrivate = true) + } +} + +@Composable +private fun SyncedTabsPage( + tabsTrayStore: TabsTrayStore, + onTabClick: (SyncTab) -> Unit, +) { + val syncedTabs = tabsTrayStore + .observeAsComposableState { state -> state.syncedTabs }.value ?: emptyList() + + SyncedTabsList( + syncedTabs = syncedTabs, + taskContinuityEnabled = true, + onTabClick = onTabClick, + ) +} + +@Composable +private fun EmptyTabPage(isPrivate: Boolean) { + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = stringResource( + id = if (isPrivate) { + R.string.no_private_tabs_description + } else { + R.string.no_open_tabs_description + }, + ), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 80.dp), + color = FirefoxTheme.colors.textSecondary, + style = FirefoxTheme.typography.body1, + ) + } +} + @LightDarkPreview @Composable private fun TabsTrayPreview() {