From a9b4975adab4c687817f44705656b8da1739e87a Mon Sep 17 00:00:00 2001 From: 0nko Date: Mon, 3 Nov 2025 12:08:22 +0100 Subject: [PATCH 1/4] Add TabManager mode to the navigation bar --- .../app/appearance/AppearanceActivity.kt | 3 +- .../duckduckgo/app/browser/BrowserActivity.kt | 3 +- .../app/browser/BrowserTabFragment.kt | 17 +- .../bar/view/BrowserNavigationBarObserver.kt | 12 - .../bar/view/BrowserNavigationBarView.kt | 9 +- .../bar/view/BrowserNavigationBarViewModel.kt | 18 +- .../app/tabs/ui/TabSwitcherActivity.kt | 43 ++- .../app/tabs/ui/TabSwitcherMenuExt.kt | 17 +- .../app/tabs/ui/TabSwitcherViewModel.kt | 86 +++-- .../duckduckgo/app/tabs/ui/TabTouchHelper.kt | 2 +- .../main/res/layout/activity_tab_switcher.xml | 8 +- .../app/tabs/ui/TabSwitcherViewModelTest.kt | 348 ++++++++++++++++-- 12 files changed, 435 insertions(+), 131 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt b/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt index 41d25ea2f432..61dc0eb487a4 100644 --- a/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt @@ -200,8 +200,9 @@ class AppearanceActivity : DuckDuckGoActivity() { val subtitle = getString( when (omnibarType) { - OmnibarType.SINGLE_TOP, OmnibarType.SPLIT -> R.string.settingsAddressBarPositionTop + OmnibarType.SINGLE_TOP -> R.string.settingsAddressBarPositionTop OmnibarType.SINGLE_BOTTOM -> R.string.settingsAddressBarPositionBottom + OmnibarType.SPLIT -> throw IllegalStateException("Split omnibar not supported in the position selection settings") }, ) binding.addressBarPositionSetting.setSecondaryText(subtitle) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index d6d3d735773b..f1a752e612d1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -765,8 +765,9 @@ open class BrowserActivity : DuckDuckGoActivity() { val anchorView = when (settingsDataStore.omnibarType) { - OmnibarType.SINGLE_TOP, OmnibarType.SPLIT -> null + OmnibarType.SINGLE_TOP -> null OmnibarType.SINGLE_BOTTOM -> currentTab?.getOmnibar()?.omnibarView?.toolbar ?: binding.fragmentContainer + OmnibarType.SPLIT -> currentTab?.getBottomNavigationBar() ?: binding.fragmentContainer } DefaultSnackbar( parentView = binding.fragmentContainer, diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 734ecd534588..ca924b4c6631 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -142,6 +142,7 @@ import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget import com.duckduckgo.app.browser.navigation.bar.BrowserNavigationBarViewIntegration import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarObserver +import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarView import com.duckduckgo.app.browser.newtab.NewTabPageProvider import com.duckduckgo.app.browser.omnibar.Omnibar import com.duckduckgo.app.browser.omnibar.Omnibar.FindInPageListener @@ -1190,18 +1191,6 @@ class BrowserTabFragment : onBrowserMenuButtonPressed() } - override fun onBackButtonClicked() { - onBackArrowClicked() - } - - override fun onBackButtonLongClicked() { - onBackArrowLongClicked() - } - - override fun onForwardButtonClicked() { - onForwardArrowClicked() - } - override fun onNewTabButtonClicked() { viewModel.onNavigationBarNewTabButtonClicked() } @@ -2012,6 +2001,10 @@ class BrowserTabFragment : } } + fun getBottomNavigationBar(): BrowserNavigationBarView { + return binding.navigationBar + } + private fun processCommand(it: Command?) { if (it is NavigationCommand) { omnibar.cancelTrackersAnimation() diff --git a/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarObserver.kt b/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarObserver.kt index 1a0aa658db2d..8cb5249ef436 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarObserver.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarObserver.kt @@ -35,18 +35,6 @@ interface BrowserNavigationBarObserver { fun onMenuButtonClicked() { } - @EmptySuper - fun onBackButtonClicked() { - } - - @EmptySuper - fun onBackButtonLongClicked() { - } - - @EmptySuper - fun onForwardButtonClicked() { - } - @EmptySuper fun onNewTabButtonClicked() { } diff --git a/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarView.kt b/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarView.kt index b58fc85ab966..bfd9c9c1b03e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarView.kt @@ -36,11 +36,8 @@ import com.duckduckgo.app.browser.PulseAnimation import com.duckduckgo.app.browser.databinding.ViewBrowserNavigationBarBinding import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyAutofillButtonClicked -import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyBackButtonClicked -import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyBackButtonLongClicked import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyBookmarksButtonClicked import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyFireButtonClicked -import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyForwardButtonClicked import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyMenuButtonClicked import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyNewTabButtonClicked import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarViewModel.Command.NotifyTabsButtonClicked @@ -208,9 +205,6 @@ class BrowserNavigationBarView @JvmOverloads constructor( NotifyTabsButtonClicked -> browserNavigationBarObserver?.onTabsButtonClicked() NotifyTabsButtonLongClicked -> browserNavigationBarObserver?.onTabsButtonLongClicked() NotifyMenuButtonClicked -> browserNavigationBarObserver?.onMenuButtonClicked() - NotifyBackButtonClicked -> browserNavigationBarObserver?.onBackButtonClicked() - NotifyBackButtonLongClicked -> browserNavigationBarObserver?.onBackButtonLongClicked() - NotifyForwardButtonClicked -> browserNavigationBarObserver?.onForwardButtonClicked() NotifyBookmarksButtonClicked -> browserNavigationBarObserver?.onBookmarksButtonClicked() NotifyNewTabButtonClicked -> browserNavigationBarObserver?.onNewTabButtonClicked() NotifyAutofillButtonClicked -> browserNavigationBarObserver?.onAutofillButtonClicked() @@ -232,12 +226,13 @@ class BrowserNavigationBarView @JvmOverloads constructor( enum class ViewMode { NewTab, Browser, + TabManager, } /** * Behavior that offsets the navigation bar proportionally to the offset of the top omnibar. */ - private class BottomViewBehavior( + inner class BottomViewBehavior( context: Context, attrs: AttributeSet?, ) : Behavior(context, attrs) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarViewModel.kt index 408bdcf9a51f..f464236dd45e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/navigation/bar/view/BrowserNavigationBarViewModel.kt @@ -122,8 +122,6 @@ class BrowserNavigationBarViewModel @Inject constructor( it.copy( newTabButtonVisible = false, autofillButtonVisible = true, - fireButtonVisible = true, - tabsButtonVisible = true, ) } } @@ -133,8 +131,17 @@ class BrowserNavigationBarViewModel @Inject constructor( it.copy( newTabButtonVisible = true, autofillButtonVisible = false, - fireButtonVisible = true, - tabsButtonVisible = true, + ) + } + } + + ViewMode.TabManager -> { + _viewState.update { + it.copy( + newTabButtonVisible = true, + autofillButtonVisible = false, + tabsButtonVisible = false, + bookmarksButtonVisible = false, ) } } @@ -154,9 +161,6 @@ class BrowserNavigationBarViewModel @Inject constructor( data object NotifyTabsButtonClicked : Command() data object NotifyTabsButtonLongClicked : Command() data object NotifyMenuButtonClicked : Command() - data object NotifyBackButtonClicked : Command() - data object NotifyBackButtonLongClicked : Command() - data object NotifyForwardButtonClicked : Command() data object NotifyNewTabButtonClicked : Command() data object NotifyAutofillButtonClicked : Command() data object NotifyBookmarksButtonClicked : Command() diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt index c4a16778d58e..c05bee5b75d6 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt @@ -42,6 +42,8 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.ActivityTabSwitcherBinding import com.duckduckgo.app.browser.databinding.PopupTabsMenuBinding import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarObserver +import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarView import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.downloads.DownloadsActivity @@ -50,7 +52,6 @@ import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.view.ClearDataAction import com.duckduckgo.app.global.view.FireDialog import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentManager -import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.settings.SettingsActivity import com.duckduckgo.app.settings.clear.OnboardingExperimentFireAnimationHelper import com.duckduckgo.app.settings.db.SettingsDataStore @@ -72,8 +73,8 @@ import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShareLinks import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShowAnimatedTileDismissalDialog import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShowUndoBookmarkMessage import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShowUndoDeleteTabsMessage -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode.Selection +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode.Selection import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.browser.ui.omnibar.OmnibarType import com.duckduckgo.common.ui.DuckDuckGoActivity @@ -224,8 +225,7 @@ class TabSwitcherActivity : null } OmnibarType.SPLIT -> { - null - // TODO: add bottom bar + binding.navigationBar } } } @@ -244,6 +244,7 @@ class TabSwitcherActivity : configureViewReferences() setupToolbar(toolbar) configureRecycler() + configureNavigationBar() configureObservers() configureOnBackPressedListener() @@ -251,6 +252,24 @@ class TabSwitcherActivity : initMenuClickListeners() } + private fun configureNavigationBar() { + binding.navigationBar.browserNavigationBarObserver = + object : BrowserNavigationBarObserver { + override fun onMenuButtonClicked() { + showPopupMenu(binding.navigationBar.popupMenuAnchor.id) + } + + override fun onNewTabButtonClicked() { + viewModel.onNewTabRequested() + } + + override fun onFireButtonClicked() { + viewModel.onFireButtonTapped() + } + } + binding.navigationBar.setViewMode(BrowserNavigationBarView.ViewMode.TabManager) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) @@ -267,11 +286,9 @@ class TabSwitcherActivity : when (settingsDataStore.omnibarType) { OmnibarType.SINGLE_TOP -> { binding.root.removeView(binding.tabSwitcherToolbarBottom.root) - // TODO: remove bottom bar } OmnibarType.SINGLE_BOTTOM -> { binding.root.removeView(binding.tabSwitcherToolbarTop.root) - // TODO: remove bottom bar } OmnibarType.SPLIT -> { binding.root.removeView(binding.tabSwitcherToolbarBottom.root) @@ -392,7 +409,7 @@ class TabSwitcherActivity : private fun configureObservers() { lifecycleScope.launch { - viewModel.selectionViewState.flowWithLifecycle(lifecycle).collectLatest { + viewModel.viewState.flowWithLifecycle(lifecycle).collectLatest { tabsRecycler.invalidateItemDecorations() tabsAdapter.updateData(it.tabSwitcherItems) @@ -546,6 +563,7 @@ class TabSwitcherActivity : is ShowUndoDeleteTabsMessage -> showTabsDeletedSnackbar(command.tabIds) ShowAnimatedTileDismissalDialog -> showAnimatedTileDismissalDialog() DismissAnimatedTileDismissalDialog -> tabSwitcherAnimationTileRemovalDialog!!.dismiss() + Command.ShowFireBottomSheet -> onFireButtonClicked() } } @@ -566,14 +584,15 @@ class TabSwitcherActivity : menuInflater.inflate(R.menu.menu_tab_switcher_activity, menu) val popupBinding = PopupTabsMenuBinding.bind(popupMenu.contentView) - val viewState = viewModel.selectionViewState.value + val viewState = viewModel.viewState.value - val numSelectedTabs = viewModel.selectionViewState.value.numSelectedTabs + val numSelectedTabs = viewModel.viewState.value.numSelectedTabs menu.createDynamicInterface( numSelectedTabs = numSelectedTabs, popupMenu = popupBinding, toolbar = toolbar, dynamicMenu = viewState.dynamicInterface, + navigationBar = binding.navigationBar, ) return true @@ -598,7 +617,7 @@ class TabSwitcherActivity : override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.fireToolbarButton -> onFireButtonClicked() + R.id.fireToolbarButton -> viewModel.onFireButtonTapped() R.id.popupMenuToolbarButton -> showPopupMenu(item.itemId) R.id.newTabToolbarButton -> onNewTabRequested(fromOverflowMenu = false) R.id.duckAIToolbarButton -> viewModel.onDuckAIButtonClicked() @@ -627,7 +646,6 @@ class TabSwitcherActivity : } private fun onFireButtonClicked() { - pixel.fire(AppPixelName.FORGET_ALL_PRESSED_TABSWITCHING) val dialog = FireDialog( context = this, @@ -932,5 +950,6 @@ class TabSwitcherActivity : private const val KEY_FIRST_TIME_LOADING = "FIRST_TIME_LOADING" private const val FAB_SCROLL_THRESHOLD = 7 private const val TABS_CONTENT_PADDING_DP = 56 + private const val NAVIGATION_BAR_HEIGHT_DP = 60 } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherMenuExt.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherMenuExt.kt index a5d6a6664452..0ea265c667fc 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherMenuExt.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherMenuExt.kt @@ -22,12 +22,13 @@ import androidx.appcompat.widget.Toolbar import androidx.core.view.isVisible import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.PopupTabsMenuBinding -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.BackButtonType.ARROW -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.BackButtonType.CLOSE -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.DynamicInterface -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.LayoutMode.GRID -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.LayoutMode.HIDDEN -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.LayoutMode.LIST +import com.duckduckgo.app.browser.navigation.bar.view.BrowserNavigationBarView +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.BackButtonType.ARROW +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.BackButtonType.CLOSE +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.DynamicInterface +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.LayoutMode.GRID +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.LayoutMode.HIDDEN +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.LayoutMode.LIST import com.duckduckgo.mobile.android.R as commonR fun Menu.createDynamicInterface( @@ -35,6 +36,7 @@ fun Menu.createDynamicInterface( popupMenu: PopupTabsMenuBinding, toolbar: Toolbar, dynamicMenu: DynamicInterface, + navigationBar: BrowserNavigationBarView, ) { popupMenu.selectAllMenuItem.isVisible = dynamicMenu.isSelectAllVisible popupMenu.deselectAllMenuItem.isVisible = dynamicMenu.isDeselectAllVisible @@ -94,4 +96,7 @@ fun Menu.createDynamicInterface( findItem(R.id.fireToolbarButton).isVisible = dynamicMenu.isFireButtonVisible findItem(R.id.duckAIToolbarButton).isVisible = dynamicMenu.isDuckAIButtonVisible findItem(R.id.newTabToolbarButton).isVisible = dynamicMenu.isNewTabButtonVisible + findItem(R.id.popupMenuToolbarButton).isVisible = dynamicMenu.isMenuButtonVisible + + navigationBar.isVisible = dynamicMenu.isBottomBarVisible } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index b1497acfd358..6ca8264fba10 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -22,13 +22,13 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.omnibar.OmnibarFeatureRepository import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.TAB_MANAGER_GRID_VIEW_BUTTON_CLICKED import com.duckduckgo.app.pixels.AppPixelName.TAB_MANAGER_LIST_VIEW_BUTTON_CLICKED import com.duckduckgo.app.pixels.duckchat.createWasUsedBeforePixelParams import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily -import com.duckduckgo.app.tabs.TabManagerFeatureFlags import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType @@ -46,9 +46,9 @@ import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShareLink import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShareLinks import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShowAnimatedTileDismissalDialog import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShowUndoBookmarkMessage -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode.Normal -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode.Selection +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode.Normal +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode.Selection import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository import com.duckduckgo.common.ui.tabs.SwipingTabsFeatureProvider import com.duckduckgo.common.utils.DispatcherProvider @@ -76,7 +76,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject -import kotlin.Boolean import kotlin.time.Duration.Companion.milliseconds @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @@ -88,12 +87,12 @@ class TabSwitcherViewModel @Inject constructor( private val swipingTabsFeature: SwipingTabsFeatureProvider, private val duckChat: DuckChat, private val duckAiFeatureState: DuckAiFeatureState, - private val tabManagerFeatureFlags: TabManagerFeatureFlags, private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository, private val tabSwitcherDataStore: TabSwitcherDataStore, private val faviconManager: FaviconManager, private val savedSitesRepository: SavedSitesRepository, private val trackersAnimationInfoPanelPixels: TrackersAnimationInfoPanelPixels, + private val omnibarFeatureRepository: OmnibarFeatureRepository, ) : ViewModel() { val deletableTabs: LiveData> = tabRepository.flowDeletableTabs.asLiveData( context = viewModelScope.coroutineContext, @@ -107,7 +106,7 @@ class TabSwitcherViewModel @Inject constructor( .flatMapLatest { tabEntities -> combine( tabRepository.flowSelectedTab, - _selectionViewState, + _viewState, tabSwitcherDataStore.isTrackersAnimationInfoTileHidden(), ) { activeTab, viewState, isAnimationTileDismissed -> getTabItems(tabEntities, activeTab, isAnimationTileDismissed, viewState.mode) @@ -116,9 +115,13 @@ class TabSwitcherViewModel @Inject constructor( val tabSwitcherItemsLiveData: LiveData> = tabSwitcherItemsFlow.asLiveData() - private val _selectionViewState = MutableStateFlow(SelectionViewState()) - val selectionViewState = combine( - _selectionViewState, + private val _viewState = MutableStateFlow( + ViewState( + isSplitOmnibarEnabled = omnibarFeatureRepository.isSplitOmnibarEnabled, + ), + ) + val viewState = combine( + _viewState, tabSwitcherItemsFlow, tabRepository.tabSwitcherData, duckAiFeatureState.showOmnibarShortcutOnNtpAndOnFocus, @@ -128,7 +131,13 @@ class TabSwitcherViewModel @Inject constructor( layoutType = tabSwitcherData.layoutType, isDuckAIButtonVisible = showDuckAiButton, ) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SelectionViewState()) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = ViewState( + isSplitOmnibarEnabled = omnibarFeatureRepository.isSplitOmnibarEnabled, + ), + ) val layoutType = tabRepository.tabSwitcherData .map { it.layoutType } @@ -136,7 +145,7 @@ class TabSwitcherViewModel @Inject constructor( // all tab items, including the animated tile val tabSwitcherItems: List - get() = selectionViewState.value.tabSwitcherItems + get() = viewState.value.tabSwitcherItems // only the actual browser tabs val tabs: List @@ -146,7 +155,7 @@ class TabSwitcherViewModel @Inject constructor( // to be used in places where selection mode is required private val selectionMode: Selection - get() = requireNotNull(selectionViewState.value.mode as Selection) + get() = requireNotNull(viewState.value.mode as Selection) sealed class Command { data object Close : Command() @@ -168,6 +177,7 @@ class TabSwitcherViewModel @Inject constructor( data class BookmarkTabsRequest(val tabIds: List) : Command() data class ShowUndoBookmarkMessage(val numBookmarks: Int) : Command() data class ShowUndoDeleteTabsMessage(val tabIds: List) : Command() + data object ShowFireBottomSheet : Command() } fun onNewTabRequested(fromOverflowMenu: Boolean = false) = viewModelScope.launch { @@ -190,8 +200,13 @@ class TabSwitcherViewModel @Inject constructor( } } + fun onFireButtonTapped() { + pixel.fire(AppPixelName.FORGET_ALL_PRESSED_TABSWITCHING) + command.value = Command.ShowFireBottomSheet + } + suspend fun onTabSelected(tabId: String) { - val mode = selectionViewState.value.mode as? Selection ?: Normal + val mode = viewState.value.mode as? Selection ?: Normal if (mode is Selection) { if (tabId in mode.selectedTabs) { pixel.fire(AppPixelName.TAB_MANAGER_TAB_DESELECTED) @@ -217,18 +232,18 @@ class TabSwitcherViewModel @Inject constructor( } private fun triggerEmptySelectionMode() { - _selectionViewState.update { it.copy(mode = Selection(emptyList())) } + _viewState.update { it.copy(mode = Selection(emptyList())) } } private fun triggerNormalMode() { - _selectionViewState.update { it.copy(mode = Normal) } + _viewState.update { it.copy(mode = Normal) } } fun onSelectAllTabs() { pixel.fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_SELECT_ALL) pixel.fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_SELECT_ALL_DAILY, type = Daily()) - _selectionViewState.update { it.copy(mode = Selection(selectedTabs = tabs.map { tab -> tab.id })) } + _viewState.update { it.copy(mode = Selection(selectedTabs = tabs.map { tab -> tab.id })) } } fun onDeselectAllTabs() { @@ -262,7 +277,7 @@ class TabSwitcherViewModel @Inject constructor( pixel.fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_BOOKMARK_TABS) pixel.fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_BOOKMARK_TABS_DAILY, type = Daily()) - (selectionViewState.value.mode as? Selection)?.let { mode -> + (viewState.value.mode as? Selection)?.let { mode -> command.value = BookmarkTabsRequest(mode.selectedTabs) } } @@ -346,7 +361,7 @@ class TabSwitcherViewModel @Inject constructor( // user has confirmed they want to close tabs fun onCloseTabsConfirmed(tabIds: List) { viewModelScope.launch { - if (selectionViewState.value.mode is Selection) { + if (viewState.value.mode is Selection) { unselectTabs(tabIds) } @@ -400,7 +415,7 @@ class TabSwitcherViewModel @Inject constructor( pixel.fire(AppPixelName.TAB_MANAGER_CLOSE_TAB_CLICKED) } - if (selectionViewState.value.mode is Selection) { + if (viewState.value.mode is Selection) { unselectTab(tab.id) } } @@ -425,19 +440,19 @@ class TabSwitcherViewModel @Inject constructor( private fun selectTab(tabId: String) = selectTabs(listOf(tabId)) private fun unselectTabs(tabIds: List) { - _selectionViewState.update { + _viewState.update { it.copy(mode = Selection(selectedTabs = selectionMode.selectedTabs - tabIds.toSet())) } } private fun selectTabs(tabIds: List) { - _selectionViewState.update { + _viewState.update { it.copy(mode = Selection(selectedTabs = selectionMode.selectedTabs + tabIds.toSet())) } } fun onEmptyAreaClicked() { - if (selectionViewState.value.mode is Selection) { + if (viewState.value.mode is Selection) { triggerNormalMode() } } @@ -445,7 +460,7 @@ class TabSwitcherViewModel @Inject constructor( fun onUpButtonPressed() { pixel.fire(AppPixelName.TAB_MANAGER_UP_BUTTON_PRESSED) - if (selectionViewState.value.mode is Selection) { + if (viewState.value.mode is Selection) { triggerNormalMode() } else { command.value = Command.Close @@ -455,7 +470,7 @@ class TabSwitcherViewModel @Inject constructor( fun onBackButtonPressed() { pixel.fire(AppPixelName.TAB_MANAGER_BACK_BUTTON_PRESSED) - if (selectionViewState.value.mode is Selection) { + if (viewState.value.mode is Selection) { triggerNormalMode() } else { command.value = Command.Close @@ -463,7 +478,7 @@ class TabSwitcherViewModel @Inject constructor( } fun onMenuOpened() { - if (selectionViewState.value.mode is Selection) { + if (viewState.value.mode is Selection) { pixel.fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_PRESSED) } else { pixel.fire(AppPixelName.TAB_MANAGER_MENU_PRESSED) @@ -595,11 +610,12 @@ class TabSwitcherViewModel @Inject constructor( } } - data class SelectionViewState( + data class ViewState( val tabSwitcherItems: List = emptyList(), val mode: Mode = Normal, val layoutType: LayoutType? = null, val isDuckAIButtonVisible: Boolean = false, + val isSplitOmnibarEnabled: Boolean = false, ) { val tabs: List = tabSwitcherItems.filterIsInstance() val numSelectedTabs: Int = (mode as? Selection)?.selectedTabs?.size ?: 0 @@ -607,9 +623,10 @@ class TabSwitcherViewModel @Inject constructor( val dynamicInterface = when (mode) { is Normal -> { DynamicInterface( - isFireButtonVisible = true, - isNewTabButtonVisible = true, + isFireButtonVisible = !isSplitOmnibarEnabled, + isNewTabButtonVisible = !isSplitOmnibarEnabled, isDuckAIButtonVisible = isDuckAIButtonVisible, + isMenuButtonVisible = !isSplitOmnibarEnabled, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -622,11 +639,12 @@ class TabSwitcherViewModel @Inject constructor( isCloseAllTabsDividerVisible = true, isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, - layoutMenuMode = when { - layoutType == GRID -> LayoutMode.LIST - layoutType == LIST -> LayoutMode.GRID + layoutMenuMode = when (layoutType) { + GRID -> LayoutMode.LIST + LIST -> LayoutMode.GRID else -> LayoutMode.HIDDEN }, + isBottomBarVisible = isSplitOmnibarEnabled, ) } @@ -640,6 +658,7 @@ class TabSwitcherViewModel @Inject constructor( isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = !areAllTabsSelected, isDeselectAllVisible = areAllTabsSelected, isSelectionActionsDividerVisible = isSelectionActionable, @@ -653,6 +672,7 @@ class TabSwitcherViewModel @Inject constructor( isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) } } @@ -661,6 +681,7 @@ class TabSwitcherViewModel @Inject constructor( val isFireButtonVisible: Boolean, val isNewTabButtonVisible: Boolean, val isDuckAIButtonVisible: Boolean, + val isMenuButtonVisible: Boolean, val isSelectAllVisible: Boolean, val isDeselectAllVisible: Boolean, val isSelectionActionsDividerVisible: Boolean, @@ -674,6 +695,7 @@ class TabSwitcherViewModel @Inject constructor( val isCloseAllTabsVisible: Boolean, val backButtonType: BackButtonType, val layoutMenuMode: LayoutMode, + val isBottomBarVisible: Boolean, ) enum class BackButtonType { diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabTouchHelper.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabTouchHelper.kt index d3a6b67a16d3..6a3752693621 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabTouchHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabTouchHelper.kt @@ -30,7 +30,7 @@ import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode import kotlin.math.abs class TabTouchHelper( diff --git a/app/src/main/res/layout/activity_tab_switcher.xml b/app/src/main/res/layout/activity_tab_switcher.xml index 44e6ff0e9243..a536acbb8697 100644 --- a/app/src/main/res/layout/activity_tab_switcher.xml +++ b/app/src/main/res/layout/activity_tab_switcher.xml @@ -19,6 +19,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" + android:animateLayoutChanges="true" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -69,4 +70,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - + + + \ No newline at end of file diff --git a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt index b8981003145a..d15a4350ce4d 100644 --- a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt @@ -23,11 +23,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Observer import androidx.lifecycle.liveData import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.browser.omnibar.OmnibarFeatureRepository import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily import com.duckduckgo.app.statistics.store.StatisticsDataStore -import com.duckduckgo.app.tabs.TabManagerFeatureFlags import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.tabs.model.TabSwitcherData @@ -42,12 +42,12 @@ import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore import com.duckduckgo.app.tabs.ui.TabSwitcherItem.Tab.NormalTab import com.duckduckgo.app.tabs.ui.TabSwitcherItem.Tab.SelectableTab import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.BackButtonType -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.DynamicInterface -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.LayoutMode -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode.Normal -import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode.Selection +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.BackButtonType +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.DynamicInterface +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.LayoutMode +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode.Normal +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.ViewState.Mode.Selection import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository import com.duckduckgo.browser.api.UserBrowserProperties import com.duckduckgo.common.test.CoroutineTestRule @@ -91,6 +91,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.Date +import kotlin.Boolean @SuppressLint("DenyListedApi") @OptIn(ExperimentalCoroutinesApi::class) @@ -129,7 +130,8 @@ class TabSwitcherViewModelTest { private val mockTrackersAnimationInfoPanelPixels: TrackersAnimationInfoPanelPixels = mock() - private val tabManagerFeatureFlags = FakeFeatureToggleFactory.create(TabManagerFeatureFlags::class.java) + private val mockOmnibarFeatureRepository: OmnibarFeatureRepository = mock() + private val swipingTabsFeature = FakeFeatureToggleFactory.create(SwipingTabsFeature::class.java) private val swipingTabsFeatureProvider = SwipingTabsFeatureProvider(swipingTabsFeature) @@ -182,12 +184,12 @@ class TabSwitcherViewModelTest { swipingTabsFeatureProvider, duckChatMock, duckAiFeatureState = duckAiFeatureStateMock, - tabManagerFeatureFlags, mockWebTrackersBlockedAppRepository, tabSwitcherDataStore, faviconManager, savedSitesRepository, mockTrackersAnimationInfoPanelPixels, + mockOmnibarFeatureRepository, ) testee.command.observeForever(mockCommandObserver) testee.tabSwitcherItemsLiveData.observeForever(mockTabSwitcherItemsObserver) @@ -258,15 +260,15 @@ class TabSwitcherViewModelTest { prepareSelectionMode() testee.onSelectionModeRequested() - assertEquals(testee.selectionViewState.value.mode, Selection()) + assertEquals(testee.viewState.value.mode, Selection()) val selectedTabId = tabList[1].tabId testee.onTabSelected(selectedTabId) verify(mockPixel).fire(AppPixelName.TAB_MANAGER_TAB_SELECTED) - assertEquals(testee.selectionViewState.value.mode, Selection(selectedTabs = listOf(selectedTabId))) + assertEquals(testee.viewState.value.mode, Selection(selectedTabs = listOf(selectedTabId))) testee.onTabSelected(selectedTabId) verify(mockPixel).fire(AppPixelName.TAB_MANAGER_TAB_DESELECTED) - assertEquals(testee.selectionViewState.value.mode, Selection()) + assertEquals(testee.viewState.value.mode, Selection()) } @Test @@ -287,13 +289,13 @@ class TabSwitcherViewModelTest { testee.onSelectionModeRequested() testee.onSelectAllTabs() - assertEquals(testee.selectionViewState.value.mode, Selection(tabList.map { it.tabId })) + assertEquals(testee.viewState.value.mode, Selection(tabList.map { it.tabId })) verify(mockPixel).fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_SELECT_ALL) verify(mockPixel).fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_SELECT_ALL_DAILY, type = Daily()) testee.onDeselectAllTabs() - assertEquals(testee.selectionViewState.value.mode, Selection()) + assertEquals(testee.viewState.value.mode, Selection()) verify(mockPixel).fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_DESELECT_ALL) verify(mockPixel).fire(AppPixelName.TAB_MANAGER_SELECT_MODE_MENU_DESELECT_ALL_DAILY, type = Daily()) } @@ -750,11 +752,12 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndNoTabsThenVerifyDynamicInterface() { - val viewState = SelectionViewState(tabSwitcherItems = emptyList(), mode = Normal, layoutType = null, isDuckAIButtonVisible = true) + val viewState = ViewState(tabSwitcherItems = emptyList(), mode = Normal, layoutType = null, isDuckAIButtonVisible = true) val expected = DynamicInterface( isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -768,6 +771,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -775,11 +779,12 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndOneNewTabPageThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true)) - val viewState = SelectionViewState(tabSwitcherItems = tabItems, mode = Normal, layoutType = null, isDuckAIButtonVisible = true) + val viewState = ViewState(tabSwitcherItems = tabItems, mode = Normal, layoutType = null, isDuckAIButtonVisible = true) val expected = DynamicInterface( isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -793,6 +798,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -800,7 +806,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndMultipleTabsThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = null, @@ -810,6 +816,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -823,6 +830,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -830,7 +838,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndNewVisualDesignEnabledAndDuckChatDisabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = null, @@ -840,6 +848,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -853,6 +862,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -860,7 +870,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndNewVisualDesignDisabledAndDuckChatDisabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = null, @@ -870,6 +880,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -883,6 +894,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -890,11 +902,12 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndMultipleTabsAndLayoutIsGridThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState(tabSwitcherItems = tabItems, mode = Normal, layoutType = GRID, isDuckAIButtonVisible = true) + val viewState = ViewState(tabSwitcherItems = tabItems, mode = Normal, layoutType = GRID, isDuckAIButtonVisible = true) val expected = DynamicInterface( isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -908,6 +921,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.LIST, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -915,7 +929,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndMultipleTabsAndLayoutIsListThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1", "http://cnn.com"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = LIST, @@ -925,6 +939,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -938,6 +953,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.GRID, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -948,11 +964,12 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1", "http://cnn.com"), false), SelectableTab(TabEntity("2"), false), ) - val viewState = SelectionViewState(tabSwitcherItems = tabItems, mode = Selection(emptyList()), layoutType = null) + val viewState = ViewState(tabSwitcherItems = tabItems, mode = Selection(emptyList()), layoutType = null) val expected = DynamicInterface( isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = true, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -966,6 +983,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -976,11 +994,12 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1", "http://cnn.com"), true), SelectableTab(TabEntity("2"), false), ) - val viewState = SelectionViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1")), layoutType = null) + val viewState = ViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1")), layoutType = null) val expected = DynamicInterface( isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = true, isDeselectAllVisible = false, isSelectionActionsDividerVisible = true, @@ -994,6 +1013,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1004,11 +1024,12 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1"), true), SelectableTab(TabEntity("2", url = "cnn.com"), false), ) - val viewState = SelectionViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1")), layoutType = null) + val viewState = ViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1")), layoutType = null) val expected = DynamicInterface( isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = true, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1022,6 +1043,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1033,11 +1055,12 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("2", "http://cnn.com"), true), SelectableTab(TabEntity("3"), false), ) - val viewState = SelectionViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1", "2")), layoutType = null) + val viewState = ViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1", "2")), layoutType = null) val expected = DynamicInterface( isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = true, isDeselectAllVisible = false, isSelectionActionsDividerVisible = true, @@ -1051,6 +1074,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1061,11 +1085,12 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1", "http://cnn.com"), true), SelectableTab(TabEntity("2"), true), ) - val viewState = SelectionViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1", "2")), layoutType = null) + val viewState = ViewState(tabSwitcherItems = tabItems, mode = Selection(listOf("1", "2")), layoutType = null) val expected = DynamicInterface( isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = true, isSelectionActionsDividerVisible = true, @@ -1079,6 +1104,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1087,7 +1113,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndNoTabsAndNewToolbarEnabledThenVerifyDynamicInterface() { - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = emptyList(), mode = Normal, layoutType = null, @@ -1097,6 +1123,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1110,6 +1137,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1117,7 +1145,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndOneNewTabPageAndNewToolbarEnabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = null, @@ -1127,6 +1155,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1140,6 +1169,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1147,7 +1177,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndMultipleTabsAndNewToolbarEnabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = null, @@ -1157,6 +1187,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1170,6 +1201,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1177,7 +1209,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndNewVisualDesignEnabledAndDuckChatDisabledAndNewToolbarEnabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = null, @@ -1187,6 +1219,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1200,6 +1233,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1207,7 +1241,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndNewVisualDesignDisabledAndDuckChatDisabledAndNewToolbarEnabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = null, @@ -1217,6 +1251,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1230,6 +1265,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1237,7 +1273,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndMultipleTabsAndLayoutIsGridAndNewToolbarEnabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = GRID, @@ -1247,6 +1283,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1260,6 +1297,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.LIST, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1267,7 +1305,7 @@ class TabSwitcherViewModelTest { @Test fun whenNormalModeAndMultipleTabsAndLayoutIsListAndNewToolbarEnabledThenVerifyDynamicInterface() { val tabItems = listOf(NormalTab(TabEntity("1", "http://cnn.com"), true), NormalTab(TabEntity("2"), false)) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Normal, layoutType = LIST, @@ -1277,6 +1315,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = true, isNewTabButtonVisible = true, isDuckAIButtonVisible = true, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1290,6 +1329,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = true, backButtonType = BackButtonType.ARROW, layoutMenuMode = LayoutMode.GRID, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1300,7 +1340,7 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1", "http://cnn.com"), false), SelectableTab(TabEntity("2"), false), ) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Selection(emptyList()), layoutType = null, @@ -1310,6 +1350,7 @@ class TabSwitcherViewModelTest { isNewTabButtonVisible = false, isDuckAIButtonVisible = false, isSelectAllVisible = true, + isMenuButtonVisible = true, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, isShareSelectedLinksVisible = false, @@ -1322,6 +1363,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1332,7 +1374,7 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1", "http://cnn.com"), true), SelectableTab(TabEntity("2"), false), ) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Selection(listOf("1")), layoutType = null, @@ -1341,6 +1383,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = true, isDeselectAllVisible = false, isSelectionActionsDividerVisible = true, @@ -1354,6 +1397,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1364,7 +1408,7 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1"), true), SelectableTab(TabEntity("2", url = "cnn.com"), false), ) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Selection(listOf("1")), layoutType = null, @@ -1373,6 +1417,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = true, isDeselectAllVisible = false, isSelectionActionsDividerVisible = false, @@ -1386,6 +1431,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1396,7 +1442,7 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1", "http://cnn.com"), true), SelectableTab(TabEntity("2"), true), ) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Selection(listOf("1", "2")), layoutType = null, @@ -1405,6 +1451,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = true, isSelectionActionsDividerVisible = true, @@ -1418,6 +1465,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1428,7 +1476,7 @@ class TabSwitcherViewModelTest { SelectableTab(TabEntity("1", "http://cnn.com"), true), SelectableTab(TabEntity("2"), true), ) - val viewState = SelectionViewState( + val viewState = ViewState( tabSwitcherItems = tabItems, mode = Selection(listOf("1", "2")), layoutType = null, @@ -1437,6 +1485,7 @@ class TabSwitcherViewModelTest { isFireButtonVisible = false, isNewTabButtonVisible = false, isDuckAIButtonVisible = false, + isMenuButtonVisible = true, isSelectAllVisible = false, isDeselectAllVisible = true, isSelectionActionsDividerVisible = true, @@ -1450,6 +1499,7 @@ class TabSwitcherViewModelTest { isCloseAllTabsVisible = false, backButtonType = BackButtonType.CLOSE, layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, ) assertEquals(expected, viewState.dynamicInterface) } @@ -1560,6 +1610,226 @@ class TabSwitcherViewModelTest { verify(mockTrackersAnimationInfoPanelPixels).fireInfoPanelDismissed() } + @Test + fun whenNormalModeAndSplitOmnibarEnabledThenMenuButtonHiddenAndBottomBarVisible() { + val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) + val viewState = ViewState( + tabSwitcherItems = tabItems, + mode = Normal, + layoutType = null, + isDuckAIButtonVisible = true, + isSplitOmnibarEnabled = true, + ) + val expected = DynamicInterface( + isFireButtonVisible = false, + isNewTabButtonVisible = false, + isDuckAIButtonVisible = true, + isMenuButtonVisible = false, + isSelectAllVisible = false, + isDeselectAllVisible = false, + isSelectionActionsDividerVisible = false, + isShareSelectedLinksVisible = false, + isBookmarkSelectedTabsVisible = false, + isSelectTabsDividerVisible = true, + isSelectTabsVisible = true, + isCloseSelectedTabsVisible = false, + isCloseOtherTabsVisible = false, + isCloseAllTabsDividerVisible = true, + isCloseAllTabsVisible = true, + backButtonType = BackButtonType.ARROW, + layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = true, + ) + assertEquals(expected, viewState.dynamicInterface) + } + + @Test + fun whenNormalModeAndSplitOmnibarEnabledAndDuckAIDisabledThenMenuButtonHiddenAndBottomBarVisible() { + val tabItems = listOf(NormalTab(TabEntity("1"), true), NormalTab(TabEntity("2"), false)) + val viewState = ViewState( + tabSwitcherItems = tabItems, + mode = Normal, + layoutType = null, + isDuckAIButtonVisible = false, + isSplitOmnibarEnabled = true, + ) + val expected = DynamicInterface( + isFireButtonVisible = false, + isNewTabButtonVisible = false, + isDuckAIButtonVisible = false, + isMenuButtonVisible = false, + isSelectAllVisible = false, + isDeselectAllVisible = false, + isSelectionActionsDividerVisible = false, + isShareSelectedLinksVisible = false, + isBookmarkSelectedTabsVisible = false, + isSelectTabsDividerVisible = true, + isSelectTabsVisible = true, + isCloseSelectedTabsVisible = false, + isCloseOtherTabsVisible = false, + isCloseAllTabsDividerVisible = true, + isCloseAllTabsVisible = true, + backButtonType = BackButtonType.ARROW, + layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = true, + ) + assertEquals(expected, viewState.dynamicInterface) + } + + @Test + fun whenSelectionModeAndSplitOmnibarEnabledThenMenuButtonVisibleAndBottomBarHidden() { + val tabItems = listOf( + SelectableTab(TabEntity("1", "http://cnn.com"), true), + SelectableTab(TabEntity("2"), false), + ) + val viewState = ViewState( + tabSwitcherItems = tabItems, + mode = Selection(listOf("1")), + layoutType = null, + isDuckAIButtonVisible = false, + isSplitOmnibarEnabled = true, + ) + val expected = DynamicInterface( + isFireButtonVisible = false, + isNewTabButtonVisible = false, + isDuckAIButtonVisible = false, + isMenuButtonVisible = true, + isSelectAllVisible = true, + isDeselectAllVisible = false, + isSelectionActionsDividerVisible = true, + isShareSelectedLinksVisible = true, + isBookmarkSelectedTabsVisible = true, + isSelectTabsDividerVisible = false, + isSelectTabsVisible = false, + isCloseSelectedTabsVisible = true, + isCloseOtherTabsVisible = true, + isCloseAllTabsDividerVisible = true, + isCloseAllTabsVisible = false, + backButtonType = BackButtonType.CLOSE, + layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, + ) + assertEquals(expected, viewState.dynamicInterface) + } + + @Test + fun whenSelectionModeAndSplitOmnibarEnabledAndNoTabsSelectedThenMenuButtonVisibleAndBottomBarHidden() { + val tabItems = listOf( + SelectableTab(TabEntity("1", "http://cnn.com"), false), + SelectableTab(TabEntity("2"), false), + ) + val viewState = ViewState( + tabSwitcherItems = tabItems, + mode = Selection(emptyList()), + layoutType = null, + isDuckAIButtonVisible = false, + isSplitOmnibarEnabled = true, + ) + val expected = DynamicInterface( + isFireButtonVisible = false, + isNewTabButtonVisible = false, + isDuckAIButtonVisible = false, + isMenuButtonVisible = true, + isSelectAllVisible = true, + isDeselectAllVisible = false, + isSelectionActionsDividerVisible = false, + isShareSelectedLinksVisible = false, + isBookmarkSelectedTabsVisible = false, + isSelectTabsDividerVisible = false, + isSelectTabsVisible = false, + isCloseSelectedTabsVisible = false, + isCloseOtherTabsVisible = false, + isCloseAllTabsDividerVisible = false, + isCloseAllTabsVisible = false, + backButtonType = BackButtonType.CLOSE, + layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, + ) + assertEquals(expected, viewState.dynamicInterface) + } + + @Test + fun whenSelectionModeAndSplitOmnibarEnabledAndAllTabsSelectedThenMenuButtonVisibleAndBottomBarHidden() { + val tabItems = listOf( + SelectableTab(TabEntity("1", "http://cnn.com"), true), + SelectableTab(TabEntity("2"), true), + ) + val viewState = ViewState( + tabSwitcherItems = tabItems, + mode = Selection(listOf("1", "2")), + layoutType = null, + isDuckAIButtonVisible = false, + isSplitOmnibarEnabled = true, + ) + val expected = DynamicInterface( + isFireButtonVisible = false, + isNewTabButtonVisible = false, + isDuckAIButtonVisible = false, + isMenuButtonVisible = true, + isSelectAllVisible = false, + isDeselectAllVisible = true, + isSelectionActionsDividerVisible = true, + isShareSelectedLinksVisible = true, + isBookmarkSelectedTabsVisible = true, + isSelectTabsDividerVisible = false, + isSelectTabsVisible = false, + isCloseSelectedTabsVisible = true, + isCloseOtherTabsVisible = false, + isCloseAllTabsDividerVisible = true, + isCloseAllTabsVisible = false, + backButtonType = BackButtonType.CLOSE, + layoutMenuMode = LayoutMode.HIDDEN, + isBottomBarVisible = false, + ) + assertEquals(expected, viewState.dynamicInterface) + } + + // Tests for ViewState.isSplitOmnibarEnabled based on OmnibarFeatureRepository + + @Test + fun whenOmnibarFeatureRepositoryHasSplitOmnibarDisabledThenViewStateReflectsIt() = runTest { + whenever(mockOmnibarFeatureRepository.isSplitOmnibarEnabled).thenReturn(false) + + initializeViewModel() + + assertFalse(testee.viewState.value.isSplitOmnibarEnabled) + } + + @Test + fun whenOmnibarFeatureRepositoryHasSplitOmnibarEnabledThenViewStateReflectsIt() = runTest { + whenever(mockOmnibarFeatureRepository.isSplitOmnibarEnabled).thenReturn(true) + + initializeViewModel() + + assertTrue(testee.viewState.value.isSplitOmnibarEnabled) + } + + @Test + fun whenSplitOmnibarDisabledThenDynamicInterfaceShowsMenuButtonAndHidesBottomBar() = runTest { + whenever(mockOmnibarFeatureRepository.isSplitOmnibarEnabled).thenReturn(false) + + initializeViewModel() + + val viewState = testee.viewState.value + assertTrue(viewState.dynamicInterface.isMenuButtonVisible) + assertFalse(viewState.dynamicInterface.isBottomBarVisible) + assertTrue(viewState.dynamicInterface.isFireButtonVisible) + assertTrue(viewState.dynamicInterface.isNewTabButtonVisible) + } + + @Test + fun whenSplitOmnibarEnabledThenDynamicInterfaceHidesMenuButtonAndShowsBottomBar() = runTest { + whenever(mockOmnibarFeatureRepository.isSplitOmnibarEnabled).thenReturn(true) + + initializeViewModel() + + val viewState = testee.viewState.value + assertFalse(viewState.dynamicInterface.isMenuButtonVisible) + assertTrue(viewState.dynamicInterface.isBottomBarVisible) + assertFalse(viewState.dynamicInterface.isFireButtonVisible) + assertFalse(viewState.dynamicInterface.isNewTabButtonVisible) + } + private class FakeTabSwitcherDataStore : TabSwitcherDataStore { private val animationTileDismissedFlow = MutableStateFlow(false) @@ -1580,7 +1850,7 @@ class TabSwitcherViewModelTest { private fun TestScope.prepareSelectionMode() { backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - testee.selectionViewState.collect() + testee.viewState.collect() } } From 468dafbf822bdb97560d517deeb413b5c00ea740 Mon Sep 17 00:00:00 2001 From: 0nko Date: Wed, 5 Nov 2025 18:26:16 +0100 Subject: [PATCH 2/4] Fix the bottom browser menu anchoring --- .../src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt index d641cf100ab1..5d6f2dc200b2 100644 --- a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt +++ b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt @@ -142,7 +142,7 @@ open class PopupMenu( val y = if (anchorMoreToTopOfScreen) { anchorLocation[1] + anchorView.height + 4.toDp() } else { - screenHeight - anchorLocation[1] + 4.toDp() + screenHeight - anchorLocation[1] - anchorView.height + 4.toDp() } showAtLocation(rootView, gravity, x, y) From d5f78ae815e1adec545a6a17a39cae074009fbab Mon Sep 17 00:00:00 2001 From: 0nko Date: Fri, 7 Nov 2025 22:05:49 +0100 Subject: [PATCH 3/4] Don't throw an exception but use the top as a default value --- .../com/duckduckgo/app/appearance/AppearanceActivity.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt b/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt index 61dc0eb487a4..c95ed95a829f 100644 --- a/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/appearance/AppearanceActivity.kt @@ -200,9 +200,8 @@ class AppearanceActivity : DuckDuckGoActivity() { val subtitle = getString( when (omnibarType) { - OmnibarType.SINGLE_TOP -> R.string.settingsAddressBarPositionTop + OmnibarType.SPLIT, OmnibarType.SINGLE_TOP -> R.string.settingsAddressBarPositionTop OmnibarType.SINGLE_BOTTOM -> R.string.settingsAddressBarPositionBottom - OmnibarType.SPLIT -> throw IllegalStateException("Split omnibar not supported in the position selection settings") }, ) binding.addressBarPositionSetting.setSecondaryText(subtitle) @@ -239,9 +238,9 @@ class AppearanceActivity : DuckDuckGoActivity() { override fun onPositiveButtonClicked(selectedItem: Int) { val selectedTheme = when (selectedItem) { - 2 -> DuckDuckGoTheme.LIGHT - 3 -> DuckDuckGoTheme.DARK - else -> DuckDuckGoTheme.SYSTEM_DEFAULT + 2 -> LIGHT + 3 -> DARK + else -> SYSTEM_DEFAULT } viewModel.onThemeSelected(selectedTheme) } From 0853ac4b6c020183bd7b17150fe73afecf0c3561 Mon Sep 17 00:00:00 2001 From: 0nko Date: Mon, 10 Nov 2025 10:27:03 -0500 Subject: [PATCH 4/4] Remove unused constants --- .../java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt index c05bee5b75d6..f60948efce1b 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt @@ -948,8 +948,5 @@ class TabSwitcherActivity : private const val TAB_GRID_COLUMN_WIDTH_DP = 180 private const val TAB_GRID_MAX_COLUMN_COUNT = 4 private const val KEY_FIRST_TIME_LOADING = "FIRST_TIME_LOADING" - private const val FAB_SCROLL_THRESHOLD = 7 - private const val TABS_CONTENT_PADDING_DP = 56 - private const val NAVIGATION_BAR_HEIGHT_DP = 60 } }