Skip to content

Commit 7cdc90f

Browse files
authored
Duck.ai: Omnibar polish' (#7207)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1212068221873640?focus=true ### Description This PR adds polishing to the Duck.ai Omnibar in fullscreen mode ### Steps to test this PR _History button in Omnibar_ - [x] Install app and enable fullscreen mode - [x] Open Duck.ai - [x] Verify Duck.ai omnibar is present and there’s a new button at the left of the input field" - [x] Tap on it, verify that the sidebar opens _Show input field after tapping on Duck.ai header_ - [x] Install app and enable fullscreen mode - [x] Enable Search & Duck.ai mode - [x] Open Duck.ai - [x] Tap on the Duck.ai header - [x] Verify that the Input Screen is enabled _Show input field after tapping on Duck.ai header_ - [x] Install app and enable fullscreen mode - [x] Enable Search mode - [x] Open Duck.ai - [x] Tap on the Duck.ai header - [x] Verify that the Input field takes focus and you can type to get out of Duck.ai mode _Onboarding_ _For this you want to enable fullscreenMode in DuckChatFeature to true_ - [x] Install app and go through onboarding - [x] Open Duck.ai as soon as you can - [x] Verify that no onboarding dialogs are present - [x] Navigate to another site - [x] Verify that trackers dax dialog appears
1 parent c82e1cb commit 7cdc90f

File tree

20 files changed

+390
-32
lines changed

20 files changed

+390
-32
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!--
2+
~ Copyright (c) 2025 DuckDuckGo
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:width="24dp"
19+
android:height="24dp"
20+
android:viewportWidth="24"
21+
android:viewportHeight="24">
22+
<path
23+
android:pathData="M15.25,16.5C15.664,16.5 16,16.836 16,17.25C16,17.664 15.664,18 15.25,18H3.75C3.336,18 3,17.664 3,17.25C3,16.836 3.336,16.5 3.75,16.5H15.25ZM20.289,6.001C20.685,6.021 21,6.349 21,6.75C21,7.151 20.685,7.479 20.289,7.499L20.25,7.5H3.75C3.336,7.5 3,7.164 3,6.75C3,6.336 3.336,6 3.75,6H20.25L20.289,6.001Z"
24+
android:fillColor="?daxColorPrimaryIcon" />
25+
</vector>

android-design-system/design-system/src/main/res/values/design-system-dimensions.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353

5454
<!-- Omnibar -->
5555
<dimen name="omnibarCookieAnimationBannerHeight">38dp</dimen>
56-
<dimen name="omnibarCardMarginHorizontal">16dp</dimen>
56+
<dimen name="omnibarCardMarginHorizontal">8dp</dimen>
5757
<dimen name="omnibarCardMarginEnd">8dp</dimen>
5858
<dimen name="omnibarCardExtendedMarginEnd">4dp</dimen>
5959
<dimen name="omnibarCardMarginTop">4dp</dimen>

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ class BrowserTabViewModelTest {
622622
private val mockDeviceAppLookup: DeviceAppLookup = mock()
623623

624624
private val mockDuckAiFullScreenMode = MutableStateFlow(false)
625+
private val mockDuckAiFullScreenModeEnabled = MutableStateFlow(true)
625626

626627
private lateinit var fakeContentScopeScriptsSubscriptionEventPluginPoint: FakeContentScopeScriptsSubscriptionEventPluginPoint
627628
private var serpSettingsFeature = FakeFeatureToggleFactory.create(SerpSettingsFeature::class.java)
@@ -8330,9 +8331,9 @@ class BrowserTabViewModelTest {
83308331
subscriptionName = "subscription1",
83318332
params = JSONObject(),
83328333
)
8333-
whenever(mockDuckChatJSHelper.onNativeAction(NativeAction.HISTORY)).thenReturn(expectedEvent)
8334+
whenever(mockDuckChatJSHelper.onNativeAction(NativeAction.SIDEBAR)).thenReturn(expectedEvent)
83348335

8335-
testee.openDuckChatHistory()
8336+
testee.openDuckChatSidebar()
83368337

83378338
testee.subscriptionEventDataFlow.test {
83388339
val emittedEvent = awaitItem()
@@ -8362,4 +8363,142 @@ class BrowserTabViewModelTest {
83628363
cancelAndIgnoreRemainingEvents()
83638364
}
83648365
}
8366+
8367+
@Test
8368+
fun whenNonDuckAiPageFinishedAndFullscreenModeEnabledThenDisabledDuckAiModeCommandSent() = runTest {
8369+
whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoChatUrl(any())).thenReturn(false)
8370+
whenever(mockDuckAiFeatureState.showFullScreenMode).thenReturn(mockDuckAiFullScreenModeEnabled)
8371+
8372+
val nonDdgUrl = "https://example.com/search?q=test"
8373+
val webViewNavState = WebViewNavigationState(mockStack, 100)
8374+
8375+
testee.pageFinished(mockWebView, webViewNavState, nonDdgUrl)
8376+
8377+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
8378+
8379+
val commands = commandCaptor.allValues
8380+
assertFalse(commands.any { it is Command.EnableDuckAIFullScreen })
8381+
assertTrue(commands.any { it is Command.DisableDuckAIFullScreen })
8382+
}
8383+
8384+
@Test
8385+
fun whenDuckAiPageFinishedAndFullscreenModeEnabledThenEnableDuckAiModeCommandSent() = runTest {
8386+
whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoChatUrl(any())).thenReturn(true)
8387+
whenever(mockDuckAiFeatureState.showFullScreenMode).thenReturn(mockDuckAiFullScreenModeEnabled)
8388+
8389+
val nonDdgUrl = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5"
8390+
val webViewNavState = WebViewNavigationState(mockStack, 100)
8391+
8392+
testee.pageFinished(mockWebView, webViewNavState, nonDdgUrl)
8393+
8394+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
8395+
8396+
val commands = commandCaptor.allValues
8397+
assertFalse(commands.any { it is Command.DisableDuckAIFullScreen })
8398+
assertTrue(commands.any { it is Command.EnableDuckAIFullScreen })
8399+
}
8400+
8401+
@Test
8402+
fun whenNonDuckAiPageFinishedAndFullscreenModeDisabledThenDuckAiCommandsNotSent() = runTest {
8403+
mockDuckAiFullScreenMode.emit(false)
8404+
8405+
val nonDdgUrl = "https://example.com/search?q=test"
8406+
val webViewNavState = WebViewNavigationState(mockStack, 100)
8407+
8408+
testee.pageFinished(mockWebView, webViewNavState, nonDdgUrl)
8409+
8410+
val commands = commandCaptor.allValues
8411+
assertFalse(commands.any { it is Command.EnableDuckAIFullScreen })
8412+
assertFalse(commands.any { it is Command.DisableDuckAIFullScreen })
8413+
}
8414+
8415+
@Test
8416+
fun whenDuckAiPageFinishedAndFullscreenModeDisabledThenDuckAiCommandsNotSent() = runTest {
8417+
mockDuckAiFullScreenMode.emit(false)
8418+
8419+
val nonDdgUrl = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5"
8420+
val webViewNavState = WebViewNavigationState(mockStack, 100)
8421+
8422+
testee.pageFinished(mockWebView, webViewNavState, nonDdgUrl)
8423+
8424+
val commands = commandCaptor.allValues
8425+
assertFalse(commands.any { it is Command.DisableDuckAIFullScreen })
8426+
assertFalse(commands.any { it is Command.EnableDuckAIFullScreen })
8427+
}
8428+
8429+
@Test
8430+
fun whenNewPageWithDuckAIUrlAndFullscreenModeEnabledThenEnableDuckAiModeCommandSent() = runTest {
8431+
whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoChatUrl(any())).thenReturn(true)
8432+
whenever(mockDuckAiFeatureState.showFullScreenMode).thenReturn(mockDuckAiFullScreenModeEnabled)
8433+
testee.browserViewState.value = browserViewState().copy(browserShowing = true)
8434+
8435+
testee.navigationStateChanged(
8436+
buildWebNavigation(
8437+
currentUrl = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5",
8438+
originalUrl = "https://www.example.com",
8439+
),
8440+
)
8441+
8442+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
8443+
8444+
val commands = commandCaptor.allValues
8445+
assertFalse(commands.any { it is Command.DisableDuckAIFullScreen })
8446+
assertTrue(commands.any { it is Command.EnableDuckAIFullScreen })
8447+
}
8448+
8449+
@Test
8450+
fun whenNewPageWithNonDuckAIUrlAndFullscreenModeEnabledThenDisableDuckAiModeCommandSent() = runTest {
8451+
whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoChatUrl(any())).thenReturn(false)
8452+
whenever(mockDuckAiFeatureState.showFullScreenMode).thenReturn(mockDuckAiFullScreenModeEnabled)
8453+
testee.browserViewState.value = browserViewState().copy(browserShowing = true)
8454+
8455+
testee.navigationStateChanged(
8456+
buildWebNavigation(
8457+
currentUrl = "https://test.com",
8458+
originalUrl = "https://www.example.com",
8459+
),
8460+
)
8461+
8462+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
8463+
8464+
val commands = commandCaptor.allValues
8465+
assertTrue(commands.any { it is Command.DisableDuckAIFullScreen })
8466+
assertFalse(commands.any { it is Command.EnableDuckAIFullScreen })
8467+
}
8468+
8469+
@Test
8470+
fun whenNewPageWithDuckAIUrlAndFullscreenModeDisabledThenNoDuckAICommandsSent() = runTest {
8471+
testee.browserViewState.value = browserViewState().copy(browserShowing = true)
8472+
8473+
testee.navigationStateChanged(
8474+
buildWebNavigation(
8475+
currentUrl = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5",
8476+
originalUrl = "https://www.example.com",
8477+
),
8478+
)
8479+
8480+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
8481+
8482+
val commands = commandCaptor.allValues
8483+
assertFalse(commands.any { it is Command.DisableDuckAIFullScreen })
8484+
assertFalse(commands.any { it is Command.EnableDuckAIFullScreen })
8485+
}
8486+
8487+
@Test
8488+
fun whenNewPageWithNonDuckAIUrlAndFullscreenModeDisabledThenNoDuckAICommandsSent() = runTest {
8489+
testee.browserViewState.value = browserViewState().copy(browserShowing = true)
8490+
8491+
testee.navigationStateChanged(
8492+
buildWebNavigation(
8493+
currentUrl = "https://test.com",
8494+
originalUrl = "https://www.example.com",
8495+
),
8496+
)
8497+
8498+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
8499+
8500+
val commands = commandCaptor.allValues
8501+
assertFalse(commands.any { it is Command.DisableDuckAIFullScreen })
8502+
assertFalse(commands.any { it is Command.EnableDuckAIFullScreen })
8503+
}
83658504
}

app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,20 @@ class CtaViewModelTest {
940940
assertNull(value)
941941
}
942942

943+
@Test
944+
fun givenDuckAISiteWhenRefreshCtaWhileBrowsingThenReturnNull() = runTest {
945+
givenDaxOnboardingActive()
946+
val site = site(url = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5")
947+
948+
val value = testee.refreshCta(
949+
coroutineRule.testDispatcher,
950+
isBrowserShowing = true,
951+
site = site,
952+
detectedRefreshPatterns = detectedRefreshPatterns,
953+
)
954+
assertNull(value)
955+
}
956+
943957
private suspend fun givenDaxOnboardingActive() {
944958
whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
945959
}

app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,13 @@ open class BrowserActivity : DuckDuckGoActivity() {
962962
if (result.resultCode == RESULT_OK) {
963963
// Handle any result data if needed
964964
result.data?.let { intent ->
965+
logcat { "Duck.ai: tabSwitcherActivityResult ${intent.extras}" }
966+
if (intent.hasExtra(TabSwitcherActivity.EXTRA_KEY_DUCK_AI_URL)) {
967+
val duckAIUrl = intent.getStringExtra(TabSwitcherActivity.EXTRA_KEY_DUCK_AI_URL)
968+
if (!duckAIUrl.isNullOrEmpty()) {
969+
currentTab?.submitQuery(duckAIUrl)
970+
}
971+
}
965972
intent.extras?.let { extras ->
966973
val deletedTabIds = extras.getStringArrayList(TabSwitcherActivity.EXTRA_KEY_DELETED_TAB_IDS)
967974
if (!deletedTabIds.isNullOrEmpty()) {

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1443,7 +1443,7 @@ class BrowserTabFragment :
14431443
viewModel.openNewDuckChat()
14441444
}
14451445
onMenuItemClicked(duckChatHistoryMenuItem) {
1446-
viewModel.openDuckChatHistory()
1446+
viewModel.openDuckChatSidebar()
14471447
}
14481448
onMenuItemClicked(duckChatSettingsMenuItem) {
14491449
viewModel.openDuckChatSettings()
@@ -3126,6 +3126,10 @@ class BrowserTabFragment :
31263126
override fun onBackButtonPressed() {
31273127
hideKeyboard()
31283128
}
3129+
3130+
override fun onDuckAISidebarButtonPressed() {
3131+
viewModel.openDuckChatSidebar()
3132+
}
31293133
},
31303134
)
31313135
}

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,7 @@ class BrowserTabViewModel @Inject constructor(
15791579
}
15801580
} else {
15811581
withContext(dispatchers.main()) {
1582+
evaluateDuckAIPage(stateChange.url)
15821583
pageChanged(stateChange.url, stateChange.title)
15831584
}
15841585
}
@@ -2212,7 +2213,6 @@ class BrowserTabViewModel @Inject constructor(
22122213
}
22132214

22142215
private fun onSiteChanged() {
2215-
logcat { "Duck.ai: onSiteChanged" }
22162216
httpsUpgraded = false
22172217
site?.isDesktopMode = currentBrowserViewState().isDesktopBrowsingMode
22182218
viewModelScope.launch {
@@ -4514,9 +4514,9 @@ class BrowserTabViewModel @Inject constructor(
45144514
}
45154515
}
45164516

4517-
fun openDuckChatHistory() {
4517+
fun openDuckChatSidebar() {
45184518
viewModelScope.launch {
4519-
val subscriptionEvent = duckChatJSHelper.onNativeAction(NativeAction.HISTORY)
4519+
val subscriptionEvent = duckChatJSHelper.onNativeAction(NativeAction.SIDEBAR)
45204520
_subscriptionEventDataChannel.send(subscriptionEvent)
45214521
}
45224522
}

app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class Omnibar(
8181
fun onDuckChatButtonPressed()
8282

8383
fun onBackButtonPressed()
84+
85+
fun onDuckAISidebarButtonPressed()
8486
}
8587

8688
interface FindInPageListener {

app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class OmnibarLayout @JvmOverloads constructor(
149149
val showBrowserMenuHighlight: Boolean,
150150
val showChatMenu: Boolean,
151151
val showSpacer: Boolean,
152+
val showDuckSidebar: Boolean,
152153
)
153154

154155
@Inject
@@ -224,6 +225,7 @@ class OmnibarLayout @JvmOverloads constructor(
224225
private val customTabToolbarContainerWrapper: ViewGroup by lazy { findViewById(R.id.customTabToolbarContainerWrapper) }
225226
private val leadingIconContainer: View by lazy { findViewById(R.id.omnibarIconContainer) }
226227
private val duckAIHeader: View by lazy { findViewById(R.id.duckAIHeader) }
228+
private val duckAISidebar: View by lazy { findViewById(R.id.duckAiSidebar) }
227229

228230
private var isFindInPageVisible = false
229231
private val findInPageLayoutVisibilityChangeListener =
@@ -328,6 +330,7 @@ class OmnibarLayout @JvmOverloads constructor(
328330
addTarget(tabsMenu)
329331
addTarget(aiChatMenu)
330332
addTarget(browserMenu)
333+
addTarget(duckAISidebar)
331334
},
332335
)
333336
}
@@ -581,7 +584,10 @@ class OmnibarLayout @JvmOverloads constructor(
581584
omnibarItemPressedListener?.onBackButtonPressed()
582585
}
583586
duckAIHeader.setOnClickListener {
584-
viewModel.onTextInputClickCatcherClicked()
587+
viewModel.onDuckAiHeaderClicked()
588+
}
589+
duckAISidebar.setOnClickListener {
590+
omnibarItemPressedListener?.onDuckAISidebarButtonPressed()
585591
}
586592
}
587593

@@ -604,9 +610,10 @@ class OmnibarLayout @JvmOverloads constructor(
604610
}
605611
}
606612

607-
duckAIHeader.isVisible = viewState.viewMode is ViewMode.DuckAI
608-
leadingIconContainer.isGone = viewState.viewMode is ViewMode.DuckAI
609-
omnibarTextInput.isGone = viewState.viewMode is ViewMode.DuckAI
613+
duckAIHeader.isVisible = viewState.showDuckAIHeader
614+
615+
leadingIconContainer.isGone = viewState.showDuckAIHeader
616+
omnibarTextInput.isGone = viewState.showDuckAIHeader
610617

611618
if (viewState.leadingIconState == PrivacyShield) {
612619
renderPrivacyShield(viewState.privacyShield, viewState.viewMode)
@@ -709,6 +716,15 @@ class OmnibarLayout @JvmOverloads constructor(
709716
is Command.EasterEggLogoClicked -> {
710717
onLogoClicked(command.url)
711718
}
719+
720+
is Command.FocusInputField -> {
721+
omnibarTextInput.postDelayed(
722+
{
723+
omnibarTextInput.requestFocus()
724+
},
725+
200,
726+
)
727+
}
712728
}
713729
}
714730

@@ -810,13 +826,15 @@ class OmnibarLayout @JvmOverloads constructor(
810826
showBrowserMenuHighlight = viewState.showBrowserMenuHighlight,
811827
showChatMenu = viewState.showChatMenu,
812828
showSpacer = viewState.showClearButton || viewState.showVoiceSearch,
829+
showDuckSidebar = viewState.showDuckAISidebar,
813830
)
814831

815832
if (omnibarAnimationManager.isFeatureEnabled() && previousTransitionState != null &&
816833
(
817834
newTransitionState.showFireIcon != previousTransitionState?.showFireIcon ||
818835
newTransitionState.showTabsMenu != previousTransitionState?.showTabsMenu ||
819-
newTransitionState.showBrowserMenu != previousTransitionState?.showBrowserMenu
836+
newTransitionState.showBrowserMenu != previousTransitionState?.showBrowserMenu ||
837+
newTransitionState.showDuckSidebar != previousTransitionState?.showDuckSidebar
820838
)
821839
) {
822840
TransitionManager.beginDelayedTransition(toolbarContainer, omniBarButtonTransitionSet)
@@ -830,6 +848,7 @@ class OmnibarLayout @JvmOverloads constructor(
830848
browserMenuHighlight.isVisible = newTransitionState.showBrowserMenuHighlight
831849
aiChatMenu?.isVisible = newTransitionState.showChatMenu
832850
aiChatDivider.isVisible = (viewState.showVoiceSearch || viewState.showClearButton) && viewState.showChatMenu
851+
duckAISidebar.isVisible = newTransitionState.showDuckSidebar
833852

834853
if (omnibarAnimationManager.isFeatureEnabled()) {
835854
toolbarContainer.requestLayout()
@@ -883,7 +902,6 @@ class OmnibarLayout @JvmOverloads constructor(
883902
private fun renderDuckAiMode(viewState: ViewState) {
884903
logcat { "Omnibar: renderDuckAiMode $viewState" }
885904
renderTabIcon(viewState)
886-
renderPulseAnimation(viewState)
887905
pageLoadingIndicator.isVisible = viewState.isLoading
888906
voiceSearchButton.isVisible = viewState.showVoiceSearch
889907
}

0 commit comments

Comments
 (0)