Bug 1814993 - Add inactive tabs to the composified `TabsTray`

fenix/113.0
Noah Bond 1 year ago committed by mergify[bot]
parent e119905271
commit 7150a54cda

@ -11,7 +11,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -24,8 +29,12 @@ import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.tabstray.ext.isNormalTab
import org.mozilla.fenix.tabstray.inactivetabs.InactiveTabsList
import org.mozilla.fenix.theme.FirefoxTheme
/**
@ -38,18 +47,37 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onTabClick Invoked when the user clicks on a tab.
* @param onTabMultiSelectClick Invoked when the user clicks on a tab while in multi-select mode.
* @param onTabLongClick Invoked when the user long clicks a tab.
* @param onInactiveTabsHeaderClick Invoked when the user clicks on the inactive tabs section header.
* @param onDeleteAllInactiveTabsClick Invoked when the user clicks on the delete all inactive tabs button.
* @param onInactiveTabsAutoCloseDialogShown Invoked when the inactive tabs auto close dialog
* is presented to the user.
* @param onInactiveTabAutoCloseDialogCloseButtonClick Invoked when the user clicks on the inactive
* tab auto close dialog's dismiss button.
* @param onEnableInactiveTabAutoCloseClick Invoked when the user clicks on the inactive tab auto
* close dialog's enable button.
* @param onInactiveTabClick Invoked when the user clicks on an inactive tab.
* @param onInactiveTabClose Invoked when the user clicks on an inactive tab's close button.
*/
@OptIn(ExperimentalPagerApi::class, ExperimentalComposeUiApi::class)
@Suppress("LongMethod", "LongParameterList")
@Composable
fun TabsTray(
appStore: AppStore,
tabsTrayStore: TabsTrayStore,
displayTabsInGrid: Boolean,
shouldShowInactiveTabsAutoCloseDialog: (Int) -> Boolean,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
onTabMultiSelectClick: (TabSessionState) -> Unit,
onTabLongClick: (TabSessionState) -> Unit,
onInactiveTabsHeaderClick: (Boolean) -> Unit,
onDeleteAllInactiveTabsClick: () -> Unit,
onInactiveTabsAutoCloseDialogShown: () -> Unit,
onInactiveTabAutoCloseDialogCloseButtonClick: () -> Unit,
onEnableInactiveTabAutoCloseClick: () -> Unit,
onInactiveTabClick: (TabSessionState) -> Unit,
onInactiveTabClose: (TabSessionState) -> Unit,
) {
val multiselectMode = tabsTrayStore
.observeAsComposableState { state -> state.mode }.value ?: TabsTrayState.Mode.Normal
@ -57,6 +85,10 @@ fun TabsTray(
.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 = 0)
val scope = rememberCoroutineScope()
val isInMultiSelectMode = multiselectMode is TabsTrayState.Mode.Select
@ -97,42 +129,56 @@ fun TabsTray(
) { position ->
when (Page.positionToPage(position)) {
Page.NormalTabs -> {
if (displayTabsInGrid) {
TabGrid(
tabs = normalTabs,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
)
val showInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog(inactiveTabs.size)
var showAutoCloseDialog by remember { mutableStateOf(showInactiveTabsAutoCloseDialog) }
val optionalInactiveTabsHeader: (@Composable () -> Unit)? = if (inactiveTabs.isEmpty()) {
null
} else {
TabList(
tabs = normalTabs,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
)
{
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,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
header = optionalInactiveTabsHeader,
)
}
Page.PrivateTabs -> {
if (displayTabsInGrid) {
TabGrid(
tabs = privateTabs,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
)
} else {
TabList(
tabs = privateTabs,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
)
}
TabLayout(
tabs = privateTabs,
displayTabsInGrid = displayTabsInGrid,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
onTabLongClick = onTabLongClick,
)
}
Page.SyncedTabs -> {
Text(
@ -151,48 +197,96 @@ fun TabsTray(
@LightDarkPreview
@Composable
private fun TabsTrayPreview() {
val store = TabsTrayStore(
initialState = TabsTrayState(
normalTabs = generateFakeTabsList(),
privateTabs = generateFakeTabsList(
tabCount = 7,
isPrivate = true,
),
TabsTrayPreviewRoot(
displayTabsInGrid = false,
normalTabs = generateFakeTabsList(),
privateTabs = generateFakeTabsList(
tabCount = 7,
isPrivate = true,
),
)
FirefoxTheme {
TabsTray(
tabsTrayStore = store,
displayTabsInGrid = false,
onTabClose = {},
onTabMediaClick = {},
onTabClick = {},
onTabMultiSelectClick = {},
onTabLongClick = {},
)
}
}
@LightDarkPreview
@Composable
private fun TabsTrayMultiSelectPreview() {
val store = TabsTrayStore(
TabsTrayPreviewRoot(
mode = TabsTrayState.Mode.Select(setOf()),
normalTabs = generateFakeTabsList(),
)
}
@LightDarkPreview
@Composable
private fun TabsTrayInactiveTabsPreview() {
TabsTrayPreviewRoot(
normalTabs = generateFakeTabsList(tabCount = 3),
inactiveTabs = generateFakeTabsList(),
inactiveTabsExpanded = true,
showInactiveTabsAutoCloseDialog = true,
)
}
@Composable
private fun TabsTrayPreviewRoot(
displayTabsInGrid: Boolean = true,
mode: TabsTrayState.Mode = TabsTrayState.Mode.Normal,
normalTabs: List<TabSessionState> = emptyList(),
inactiveTabs: List<TabSessionState> = emptyList(),
privateTabs: List<TabSessionState> = emptyList(),
inactiveTabsExpanded: Boolean = false,
showInactiveTabsAutoCloseDialog: Boolean = false,
) {
val normalTabsState = remember { normalTabs.toMutableStateList() }
val inactiveTabsState = remember { inactiveTabs.toMutableStateList() }
val privateTabsState = remember { privateTabs.toMutableStateList() }
var inactiveTabsExpandedState by remember { mutableStateOf(inactiveTabsExpanded) }
var showInactiveTabsAutoCloseDialogState by remember { mutableStateOf(showInactiveTabsAutoCloseDialog) }
val appStore = AppStore(
initialState = AppState(
inactiveTabsExpanded = inactiveTabsExpandedState,
),
)
val tabsTrayStore = TabsTrayStore(
initialState = TabsTrayState(
mode = TabsTrayState.Mode.Select(setOf()),
normalTabs = generateFakeTabsList(),
mode = mode,
inactiveTabs = inactiveTabsState,
normalTabs = normalTabsState,
privateTabs = privateTabs,
),
)
FirefoxTheme {
TabsTray(
tabsTrayStore = store,
displayTabsInGrid = true,
onTabClose = {},
appStore = appStore,
tabsTrayStore = tabsTrayStore,
displayTabsInGrid = displayTabsInGrid,
shouldShowInactiveTabsAutoCloseDialog = { true },
onTabClose = { tab ->
if (tab.isNormalTab()) {
normalTabsState.remove(tab)
} else {
privateTabsState.remove(tab)
}
},
onTabMediaClick = {},
onTabClick = {},
onTabMultiSelectClick = {},
onTabLongClick = {},
onInactiveTabsHeaderClick = {
inactiveTabsExpandedState = !inactiveTabsExpandedState
},
onDeleteAllInactiveTabsClick = inactiveTabsState::clear,
onInactiveTabsAutoCloseDialogShown = {},
onInactiveTabAutoCloseDialogCloseButtonClick = {
showInactiveTabsAutoCloseDialogState = !showInactiveTabsAutoCloseDialogState
},
onEnableInactiveTabAutoCloseClick = {
showInactiveTabsAutoCloseDialogState = !showInactiveTabsAutoCloseDialogState
},
onInactiveTabClick = {},
onInactiveTabClose = inactiveTabsState::remove,
)
}
}

@ -219,8 +219,11 @@ class TabsTrayFragment : AppCompatDialogFragment() {
tabsTrayComposeBinding.root.setContent {
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
TabsTray(
appStore = requireComponents.appStore,
tabsTrayStore = tabsTrayStore,
displayTabsInGrid = requireContext().settings().gridTabView,
shouldShowInactiveTabsAutoCloseDialog =
requireContext().settings()::shouldShowInactiveTabsAutoCloseDialog,
onTabClose = { tab ->
tabsTrayInteractor.onTabClosed(tab, TABS_TRAY_FEATURE_NAME)
},
@ -232,6 +235,19 @@ class TabsTrayFragment : AppCompatDialogFragment() {
tabsTrayInteractor.onMultiSelectClicked(tab, TABS_TRAY_FEATURE_NAME)
},
onTabLongClick = tabsTrayInteractor::onTabLongClicked,
onInactiveTabsHeaderClick = tabsTrayInteractor::onInactiveTabsHeaderClicked,
onDeleteAllInactiveTabsClick = tabsTrayInteractor::onDeleteAllInactiveTabsClicked,
onInactiveTabsAutoCloseDialogShown = {
TabsTray.autoCloseSeen.record(NoExtras())
},
onInactiveTabAutoCloseDialogCloseButtonClick =
tabsTrayInteractor::onAutoCloseDialogCloseButtonClicked,
onEnableInactiveTabAutoCloseClick = {
tabsTrayInteractor.onEnableAutoCloseClicked()
showInactiveTabsAutoCloseConfirmationSnackbar()
},
onInactiveTabClick = tabsTrayInteractor::onInactiveTabClicked,
onInactiveTabClose = tabsTrayInteractor::onInactiveTabClosed,
)
}
}
@ -664,6 +680,17 @@ class TabsTrayFragment : AppCompatDialogFragment() {
}
}
private fun showInactiveTabsAutoCloseConfirmationSnackbar() {
val text = getString(R.string.inactive_tabs_auto_close_message_snackbar)
val snackbar = FenixSnackbar.make(
view = tabsTrayComposeBinding.root,
duration = FenixSnackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true,
).setText(text)
snackbar.view.elevation = ELEVATION
snackbar.show()
}
companion object {
private const val DOWNLOAD_CANCEL_DIALOG_FRAGMENT_TAG = "DOWNLOAD_CANCEL_DIALOG_FRAGMENT_TAG"

@ -18,6 +18,8 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
@ -34,31 +36,73 @@ import org.mozilla.fenix.theme.FirefoxTheme
* Top-level UI for displaying a list of tabs.
*
* @param tabs The list of [TabSessionState] to display.
* @param displayTabsInGrid Whether the tabs should be displayed in a grid.
* @param onTabClose Invoked when the user clicks to close a tab.
* @param onTabMediaClick Invoked when the user interacts with a tab's media controls.
* @param onTabClick Invoked when the user clicks on a tab.
* @param onTabLongClick Invoked when the user long clicks a tab.
* @param header Optional layout to display before [tabs].
*/
@Suppress("LongParameterList")
@Composable
fun TabList(
fun TabLayout(
tabs: List<TabSessionState>,
displayTabsInGrid: Boolean,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
onTabLongClick: (TabSessionState) -> Unit,
header: (@Composable () -> Unit)? = null,
) {
if (displayTabsInGrid) {
TabGrid(
tabs = tabs,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = onTabClick,
onTabLongClick = onTabLongClick,
header = header,
)
} else {
TabList(
tabs = tabs,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = onTabClick,
onTabLongClick = onTabLongClick,
header = header,
)
}
}
@Composable
private fun TabGrid(
tabs: List<TabSessionState>,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
onTabLongClick: (TabSessionState) -> Unit,
header: (@Composable () -> Unit)? = null,
) {
val state = rememberLazyGridState()
val tabListBottomPadding = dimensionResource(id = R.dimen.tab_tray_list_bottom_padding)
val state = rememberLazyListState()
LazyColumn(
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = MIN_COLUMN_WIDTH_DP.dp),
modifier = Modifier.fillMaxSize(),
state = state,
) {
header?.let {
item(span = { GridItemSpan(maxLineSpan) }) {
header()
}
}
items(
items = tabs,
key = { tab -> tab.id },
) { tab ->
TabListItem(
TabGridItem(
tab = tab,
onCloseClick = onTabClose,
onMediaClick = onTabMediaClick,
@ -67,42 +111,39 @@ fun TabList(
)
}
item {
item(span = { GridItemSpan(maxLineSpan) }) {
Spacer(modifier = Modifier.height(tabListBottomPadding))
}
}
}
/**
* Top-level UI for displaying a grid of tabs.
*
* @param tabs The list of [TabSessionState] to display.
* @param onTabClose Invoked when the user clicks to close a tab.
* @param onTabMediaClick Invoked when the user interacts with a tab's media controls.
* @param onTabClick Invoked when the user clicks on a tab.
* @param onTabLongClick Invoked when the user long clicks a tab.
*/
@Composable
fun TabGrid(
private fun TabList(
tabs: List<TabSessionState>,
onTabClose: (tab: TabSessionState) -> Unit,
onTabMediaClick: (tab: TabSessionState) -> Unit,
onTabClick: (tab: TabSessionState) -> Unit,
onTabLongClick: (tab: TabSessionState) -> Unit,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
onTabLongClick: (TabSessionState) -> Unit,
header: (@Composable () -> Unit)? = null,
) {
val state = rememberLazyListState()
val tabListBottomPadding = dimensionResource(id = R.dimen.tab_tray_list_bottom_padding)
val state = rememberLazyGridState()
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = MIN_COLUMN_WIDTH_DP.dp),
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = state,
) {
header?.let {
item {
header()
}
}
items(
items = tabs,
key = { tab -> tab.id },
) { tab ->
TabGridItem(
TabListItem(
tab = tab,
onCloseClick = onTabClose,
onMediaClick = onTabMediaClick,
@ -111,7 +152,7 @@ fun TabGrid(
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
item {
Spacer(modifier = Modifier.height(tabListBottomPadding))
}
}
@ -120,15 +161,18 @@ fun TabGrid(
@LightDarkPreview
@Composable
private fun TabListPreview() {
val tabs = remember { generateFakeTabsList().toMutableStateList() }
FirefoxTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layer1),
) {
TabList(
tabs = generateFakeTabsList(),
onTabClose = {},
TabLayout(
tabs = tabs,
displayTabsInGrid = false,
onTabClose = tabs::remove,
onTabMediaClick = {},
onTabClick = {},
onTabLongClick = {},
@ -140,15 +184,18 @@ private fun TabListPreview() {
@LightDarkPreview
@Composable
private fun TabGridPreview() {
val tabs = remember { generateFakeTabsList().toMutableStateList() }
FirefoxTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layer1),
) {
TabGrid(
tabs = generateFakeTabsList(),
onTabClose = {},
TabLayout(
tabs = tabs,
displayTabsInGrid = true,
onTabClose = tabs::remove,
onTabMediaClick = {},
onTabClick = {},
onTabLongClick = {},
@ -157,13 +204,13 @@ private fun TabGridPreview() {
}
}
private fun generateFakeTabsList(tabCount: Int = 10): List<TabSessionState> {
val fakeTab = TabSessionState(
id = "tabId",
content = ContentState(
url = "www.mozilla.com",
),
)
return List(tabCount) { fakeTab }
}
private fun generateFakeTabsList(tabCount: Int = 10, isPrivate: Boolean = false): List<TabSessionState> =
List(tabCount) { index ->
TabSessionState(
id = "tabId$index-$isPrivate",
content = ContentState(
url = "www.mozilla.com",
private = isPrivate,
),
)
}

@ -65,7 +65,7 @@ fun InactiveTabsList(
inactiveTabs: List<TabSessionState>,
expanded: Boolean,
showAutoCloseDialog: Boolean,
onHeaderClick: () -> Unit,
onHeaderClick: (Boolean) -> Unit,
onDeleteAllButtonClick: () -> Unit,
onAutoCloseDismissClick: () -> Unit,
onEnableAutoCloseClick: () -> Unit,
@ -86,7 +86,7 @@ fun InactiveTabsList(
) {
InactiveTabsHeader(
expanded = expanded,
onClick = onHeaderClick,
onClick = { onHeaderClick(!expanded) },
onDeleteAllClick = onDeleteAllButtonClick,
)

Loading…
Cancel
Save