Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ import com.duckduckgo.app.browser.viewstate.FindInPageViewState
import com.duckduckgo.app.browser.viewstate.GlobalLayoutViewState
import com.duckduckgo.app.browser.viewstate.HighlightableButton
import com.duckduckgo.app.browser.viewstate.LoadingViewState
import com.duckduckgo.app.browser.viewstate.VpnMenuState
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.LearnMore
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.LeaveSite
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.ReportError
Expand Down Expand Up @@ -225,6 +224,7 @@ import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggesti
import com.duckduckgo.browser.api.autocomplete.AutoCompleteSettings
import com.duckduckgo.browser.api.brokensite.BrokenSiteContext
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
import com.duckduckgo.browser.ui.browsermenu.VpnMenuState
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.InstantSchedulersRule
import com.duckduckgo.common.ui.tabs.SwipingTabsFeature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ class BasicJourneyTest {
onView(allOf(withId(R.id.browserMenu), isClickable())).perform(click())

// check that the forward arrow is visible
onView(withId(R.id.forwardMenuItem)).check(matches(isDisplayed()))
onView(withContentDescription("Forward")).check(matches(isDisplayed()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ class DaxDialogsJourneyTest {
onView(isRoot()).perform(waitForView(withId(R.id.browserMenu)))
onView(allOf(withId(R.id.browserMenu), isClickable())).perform(click())

onView(withId(R.id.forwardMenuItem)).check(matches(isDisplayed()))
onView(withContentDescription("Forward")).check(matches(isDisplayed()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ class RequestBlockingTest {
IdlingRegistry.getInstance().register(idlingResourceForDisableProtections)

onView(allOf(withId(R.id.browserMenu), isClickable())).perform(click())
onView(isRoot()).perform(waitForView(withId(R.id.privacyProtectionMenuItem)))
onView(withId(R.id.privacyProtectionMenuItem)).perform(click())
onView(isRoot()).perform(waitForView(withText("Disable Privacy Protection")))
onView(withText("Disable Privacy Protection")).perform(click())

// handle the privacy protection toggle check screen showing
onView(isRoot()).perform(ViewActions.pressBack())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ class SurrogatesTest {
IdlingRegistry.getInstance().register(idlingResourceForDisableProtections)

onView(allOf(withId(R.id.browserMenu), isClickable())).perform(ViewActions.click())
onView(isRoot()).perform(waitForView(withId(R.id.privacyProtectionMenuItem)))
onView(withId(R.id.privacyProtectionMenuItem)).perform(ViewActions.click())
onView(isRoot()).perform(waitForView(withText("Disable Privacy Protection")))
onView(withText("Disable Privacy Protection")).perform(ViewActions.click())

// handle the privacy protection toggle check screen showing
onView(isRoot()).perform(ViewActions.pressBack())
Expand Down
39 changes: 24 additions & 15 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ import com.duckduckgo.app.browser.history.NavigationHistorySheet
import com.duckduckgo.app.browser.history.NavigationHistorySheet.NavigationHistorySheetListener
import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore
import com.duckduckgo.app.browser.logindetection.DOMLoginDetector
import com.duckduckgo.app.browser.menu.BrowserPopupMenu
import com.duckduckgo.app.browser.menu.BrowserMenuViewStateFactory
import com.duckduckgo.app.browser.menu.VpnMenuStore
import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials
import com.duckduckgo.app.browser.model.BasicAuthenticationRequest
Expand Down Expand Up @@ -178,7 +178,6 @@ import com.duckduckgo.app.browser.viewstate.LoadingViewState
import com.duckduckgo.app.browser.viewstate.OmnibarViewState
import com.duckduckgo.app.browser.viewstate.PrivacyShieldViewState
import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState
import com.duckduckgo.app.browser.viewstate.VpnMenuState
import com.duckduckgo.app.browser.webauthn.WebViewPasskeyInitializer
import com.duckduckgo.app.browser.webshare.WebShareChooser
import com.duckduckgo.app.browser.webshare.WebViewCompatWebShareChooser
Expand Down Expand Up @@ -253,6 +252,8 @@ import com.duckduckgo.browser.api.ui.BrowserScreens.PrivateSearchScreenNoParams
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
import com.duckduckgo.browser.ui.autocomplete.BrowserAutoCompleteSuggestionsAdapter
import com.duckduckgo.browser.ui.browsermenu.BrowserMenu
import com.duckduckgo.browser.ui.browsermenu.VpnMenuState
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.DuckDuckGoFragment
import com.duckduckgo.common.ui.store.BrowserAppTheme
Expand Down Expand Up @@ -614,7 +615,7 @@ class BrowserTabFragment :

private val skipHome get() = requireArguments().getBoolean(SKIP_HOME_ARG)

private lateinit var popupMenu: BrowserPopupMenu
private lateinit var popupMenu: BrowserMenu
private lateinit var ctaBottomSheet: PromoBottomSheetDialog
private lateinit var widgetBottomSheetDialog: AlternativeHomeScreenWidgetBottomSheetDialog

Expand Down Expand Up @@ -1313,17 +1314,11 @@ class BrowserTabFragment :
}

private fun createPopupMenu() {
val popupMenuResourceType =
when (omnibar.omnibarType) {
OmnibarType.SINGLE_TOP -> BrowserPopupMenu.ResourceType.TOP
OmnibarType.SINGLE_BOTTOM, OmnibarType.SPLIT -> BrowserPopupMenu.ResourceType.BOTTOM
}

popupMenu =
BrowserPopupMenu(
BrowserMenu(
context = requireContext(),
layoutInflater = layoutInflater,
popupMenuResourceType = popupMenuResourceType,
omnibarType = omnibar.omnibarType,
)
popupMenu.apply {
onMenuItemClicked(forwardMenuItem) {
Expand Down Expand Up @@ -1443,7 +1438,6 @@ class BrowserTabFragment :

private fun launchPopupMenu(anchorToNavigationBar: Boolean, addExtraDelay: Boolean = false) {
val isFocusedNtp = omnibar.viewMode == ViewMode.NewTab && omnibar.getText().isEmpty() && omnibar.omnibarTextInput.hasFocus()

val delay = if (addExtraDelay) POPUP_MENU_DELAY * 2 else POPUP_MENU_DELAY
// small delay added to let keyboard disappear and avoid jarring transition
binding.rootView.postDelayed(delay) {
Expand All @@ -1456,7 +1450,6 @@ class BrowserTabFragment :
vpnMenuStore.incrementVpnMenuShownCount()
}
}

if (anchorToNavigationBar) {
val anchorView = browserNavigationBarIntegration.navigationBarView.popupMenuAnchor
popupMenu.showAnchoredView(requireActivity(), binding.rootView, anchorView)
Expand Down Expand Up @@ -1778,6 +1771,16 @@ class BrowserTabFragment :
browserNavigationBarIntegration.configureBrowserViewMode()
}

private fun showDuckAI(browserViewState: BrowserViewState) {
val browseMenuState = BrowserMenuViewStateFactory.create(
omnibarViewMode = ViewMode.DuckAI,
viewState = browserViewState,
customTabsMode = tabDisplayedInCustomTabScreen,
)
popupMenu.render(browseMenuState)
omnibar.setViewMode(ViewMode.DuckAI)
}

private fun showMaliciousWarning(
siteUrl: Uri,
feed: Feed,
Expand Down Expand Up @@ -2362,7 +2365,7 @@ class BrowserTabFragment :
is Command.SubmitChat -> duckChat.openDuckChatWithAutoPrompt(it.query)
is Command.EnqueueCookiesAnimation -> enqueueCookiesAnimation(it.isCosmetic)
is Command.PageStarted -> onPageStarted()
is Command.EnableDuckAIFullScreen -> omnibar.setViewMode(ViewMode.DuckAI)
is Command.EnableDuckAIFullScreen -> showDuckAI(it.browserViewState)
is Command.DisableDuckAIFullScreen -> omnibar.setViewMode(ViewMode.Browser(it.url))
}
}
Expand Down Expand Up @@ -4613,7 +4616,13 @@ class BrowserTabFragment :

browserNavigationBarIntegration.configureFireButtonHighlight(highlighted = viewState.fireButton.isHighlighted())

popupMenu.renderState(browserShowing, viewState, tabDisplayedInCustomTabScreen)
val browseMenuState = BrowserMenuViewStateFactory.create(
omnibarViewMode = omnibar.viewMode,
viewState = viewState,
customTabsMode = tabDisplayedInCustomTabScreen,
)
logcat { "BrowserMenu: viewMode ${omnibar.viewMode} render browseMenuState $browseMenuState" }
popupMenu.render(browseMenuState)

renderFullscreenMode(viewState)
privacyProtectionsPopup.setViewState(viewState.privacyProtectionsPopupViewState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ import com.duckduckgo.app.browser.viewstate.LoadingViewState
import com.duckduckgo.app.browser.viewstate.OmnibarViewState
import com.duckduckgo.app.browser.viewstate.PrivacyShieldViewState
import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState
import com.duckduckgo.app.browser.viewstate.VpnMenuState
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.LearnMore
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.LeaveSite
Expand Down Expand Up @@ -295,6 +294,7 @@ import com.duckduckgo.browser.api.brokensite.BrokenSiteData
import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.MENU
import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.RELOAD_THREE_TIMES_WITHIN_20_SECONDS
import com.duckduckgo.browser.api.webviewcompat.WebViewCompatWrapper
import com.duckduckgo.browser.ui.browsermenu.VpnMenuState
import com.duckduckgo.common.ui.tabs.SwipingTabsFeatureProvider
import com.duckduckgo.common.utils.AppUrl
import com.duckduckgo.common.utils.AppUrl.ParamKey.QUERY
Expand Down Expand Up @@ -1969,7 +1969,7 @@ class BrowserTabViewModel @Inject constructor(
if (duckAiFeatureState.showFullScreenMode.value) {
if (duckDuckGoUrlDetector.isDuckDuckGoChatUrl(it)) {
logcat { "Duck.ai: AI Chat page loaded $it" }
command.value = Command.EnableDuckAIFullScreen
command.value = Command.EnableDuckAIFullScreen(currentBrowserViewState())
} else {
command.value = Command.DisableDuckAIFullScreen(url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.duckduckgo.app.browser.WebViewErrorResponse
import com.duckduckgo.app.browser.history.NavigationHistoryEntry
import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials
import com.duckduckgo.app.browser.model.BasicAuthenticationRequest
import com.duckduckgo.app.browser.viewstate.BrowserViewState
import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState
import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta
import com.duckduckgo.app.cta.ui.DaxBubbleCta
Expand Down Expand Up @@ -504,6 +505,6 @@ sealed class Command {
) : Command()
data object PageStarted : Command()

data object EnableDuckAIFullScreen : Command()
data class EnableDuckAIFullScreen(val browserViewState: BrowserViewState) : Command()
data class DisableDuckAIFullScreen(val url: String) : Command()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.browser.menu

import com.duckduckgo.app.browser.SSLErrorType.NONE
import com.duckduckgo.app.browser.omnibar.Omnibar
import com.duckduckgo.app.browser.viewstate.BrowserViewState
import com.duckduckgo.browser.ui.browsermenu.BrowserMenuViewState

object BrowserMenuViewStateFactory {
fun create(
omnibarViewMode: Omnibar.ViewMode,
viewState: BrowserViewState,
customTabsMode: Boolean,
): BrowserMenuViewState {
return if (customTabsMode) {
createCustomTabsViewState(viewState)
} else {
when (omnibarViewMode) {
Omnibar.ViewMode.NewTab -> createNewTabPageViewState(viewState)
Omnibar.ViewMode.DuckAI -> createDuckAiViewState(viewState)
Omnibar.ViewMode.Error -> createNewTabPageViewState(viewState)
Omnibar.ViewMode.SSLWarning -> createNewTabPageViewState(viewState)
Omnibar.ViewMode.MaliciousSiteWarning -> createNewTabPageViewState(viewState)
else -> createBrowserViewState(browserViewState = viewState)
}
}
}

private fun createCustomTabsViewState(
browserViewState: BrowserViewState,
): BrowserMenuViewState.CustomTabs {
return BrowserMenuViewState.CustomTabs(
canGoBack = browserViewState.canGoBack,
canGoForward = browserViewState.canGoForward,
canSharePage = browserViewState.canSharePage,
canChangeBrowsingMode = browserViewState.canChangeBrowsingMode,
isDesktopBrowsingMode = browserViewState.isDesktopBrowsingMode,
canChangePrivacyProtection = browserViewState.canChangePrivacyProtection,
isPrivacyProtectionDisabled = browserViewState.isPrivacyProtectionDisabled,
)
}

private fun createNewTabPageViewState(
browserViewState: BrowserViewState,
): BrowserMenuViewState.NewTabPage {
return BrowserMenuViewState.NewTabPage(
showDuckChatOption = browserViewState.showDuckChatOption,
vpnMenuState = browserViewState.vpnMenuState,
showAutofill = browserViewState.showAutofill,
)
}

private fun createDuckAiViewState(
browserViewState: BrowserViewState,
): BrowserMenuViewState.DuckAi {
return BrowserMenuViewState.DuckAi(
canPrintPage = browserViewState.canPrintPage,
canReportSite = browserViewState.canReportSite,
)
}

private fun createBrowserViewState(
browserViewState: BrowserViewState,
): BrowserMenuViewState.Browser {
return BrowserMenuViewState.Browser(
canGoBack = browserViewState.canGoBack,
canGoForward = browserViewState.canGoForward,
showDuckChatOption = browserViewState.showDuckChatOption,
canSharePage = browserViewState.canSharePage,
showSelectDefaultBrowserMenuItem = browserViewState.showSelectDefaultBrowserMenuItem,
canSaveSite = browserViewState.canSaveSite,
isBookmark = browserViewState.bookmark != null,
canFireproofSite = browserViewState.canFireproofSite,
isFireproofWebsite = browserViewState.isFireproofWebsite,
isEmailSignedIn = browserViewState.isEmailSignedIn,
canChangeBrowsingMode = browserViewState.canChangeBrowsingMode,
isDesktopBrowsingMode = browserViewState.isDesktopBrowsingMode,
hasPreviousAppLink = browserViewState.previousAppLink != null,
canFindInPage = browserViewState.canFindInPage,
addToHomeVisible = browserViewState.addToHomeVisible,
addToHomeEnabled = browserViewState.addToHomeEnabled,
canChangePrivacyProtection = browserViewState.canChangePrivacyProtection,
isPrivacyProtectionDisabled = browserViewState.isPrivacyProtectionDisabled,
canReportSite = browserViewState.canReportSite,
showAutofill = browserViewState.showAutofill,
isSSLError = browserViewState.sslError != NONE,
canPrintPage = browserViewState.canPrintPage,
)
}
}
Loading
Loading