Skip to content

Commit

Permalink
Add AI entry point to search bar
Browse files Browse the repository at this point in the history
  • Loading branch information
joshliebe committed Mar 6, 2025
1 parent 27dcebb commit 0f630b5
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 129 deletions.
12 changes: 12 additions & 0 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2512,6 +2512,10 @@ class BrowserTabFragment :
override fun onVoiceSearchPressed() {
onOmnibarVoiceSearchPressed()
}

override fun onDuckChatButtonPressed() {
onOmnibarDuckChatPressed(omnibar.getText())
}
},
)
}
Expand Down Expand Up @@ -4207,6 +4211,14 @@ class BrowserTabFragment :
val roomParameters = "?skipMediaPermissionPrompt"
webView?.loadUrl("${webView?.url.orEmpty()}$roomParameters")
}

private fun onOmnibarDuckChatPressed(text: String) {
if (text.isNotEmpty()) {
duckChat.openDuckChatWithAutoPrompt(text)
} else {
duckChat.openDuckChat()
}
}
}

private class JsOrientationHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class Omnibar(
fun onCustomTabClosePressed()
fun onCustomTabPrivacyDashboardPressed()
fun onVoiceSearchPressed()
fun onDuckChatButtonPressed()
}

interface FindInPageListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.text.Editable
import android.transition.ChangeBounds
import android.transition.Fade
import android.transition.TransitionManager
import android.transition.TransitionSet
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import android.view.inputmethod.EditorInfo
import android.widget.FrameLayout
import android.widget.ImageView
Expand Down Expand Up @@ -159,14 +164,15 @@ open class OmnibarLayout @JvmOverloads constructor(
internal val omnibarTextInput: KeyboardAwareEditText by lazy { findViewById(R.id.omnibarTextInput) }
internal val tabsMenu: TabSwitcherButton by lazy { findViewById(R.id.tabsMenu) }
internal val fireIconMenu: FrameLayout by lazy { findViewById(R.id.fireIconMenu) }
internal val aiChatMenu: FrameLayout by lazy { findViewById(R.id.aiChatIconMenu) }
internal val browserMenu: FrameLayout by lazy { findViewById(R.id.browserMenu) }
internal val browserMenuHighlight: View by lazy { findViewById(R.id.browserMenuHighlight) }
internal val cookieDummyView: View by lazy { findViewById(R.id.cookieDummyView) }
internal val cookieAnimation: LottieAnimationView by lazy { findViewById(R.id.cookieAnimation) }
internal val sceneRoot: ViewGroup by lazy { findViewById(R.id.sceneRoot) }
internal val omniBarContainer: View by lazy { findViewById(R.id.omniBarContainer) }
internal val toolbar: Toolbar by lazy { findViewById(R.id.toolbar) }
internal val toolbarContainer: View by lazy { findViewById(R.id.toolbarContainer) }
internal val toolbarContainer: ViewGroup by lazy { findViewById(R.id.toolbarContainer) }
internal val customTabToolbarContainer by lazy {
IncludeCustomTabToolbarBinding.bind(
findViewById(R.id.customTabToolbarContainer),
Expand All @@ -185,6 +191,27 @@ open class OmnibarLayout @JvmOverloads constructor(
internal val spacer: View by lazy { findViewById(R.id.spacer) }
internal val trackersAnimation: LottieAnimationView by lazy { findViewById(R.id.trackersAnimation) }
internal val duckPlayerIcon: ImageView by lazy { findViewById(R.id.duckPlayerIcon) }
internal val omniBarButtonTransitionSet: TransitionSet by lazy {
TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(
ChangeBounds().apply {
duration = 400
interpolator = OvershootInterpolator(1.3f)
}
)
addTransition(
Fade().apply {
duration = 200
addTarget(clearTextButton)
addTarget(aiChatMenu)
addTarget(fireIconMenu)
addTarget(tabsMenu)
addTarget(browserMenu)
}
)
}
}

internal fun omnibarViews(): List<View> = listOf(
clearTextButton,
Expand Down Expand Up @@ -374,6 +401,9 @@ open class OmnibarLayout @JvmOverloads constructor(
browserMenu.setOnClickListener {
omnibarItemPressedListener?.onBrowserMenuPressed()
}
aiChatMenu.setOnClickListener {
omnibarItemPressedListener?.onDuckChatButtonPressed()
}
shieldIcon.setOnClickListener {
if (isAttachedToWindow) {
viewModel.onPrivacyShieldButtonPressed()
Expand Down Expand Up @@ -482,14 +512,49 @@ open class OmnibarLayout @JvmOverloads constructor(
}
}

private var isInitialRender = true

data class ButtonState(
val showClearButton: Boolean,
val showVoiceSearch: Boolean,
val showTabsMenu: Boolean,
val showFireIcon: Boolean,
val showBrowserMenu: Boolean,
val showBrowserMenuHighlight: Boolean,
val showChatMenu: Boolean,
val experimentalIconsEnabled: Boolean
)

private var previousButtonState: ButtonState? = null

private fun renderButtons(viewState: ViewState) {
val newButtonState = ButtonState(
showClearButton = viewState.showClearButton,
showVoiceSearch = viewState.showVoiceSearch,
showTabsMenu = viewState.showTabsMenu,
showFireIcon = viewState.showFireIcon,
showBrowserMenu = viewState.showBrowserMenu,
showBrowserMenuHighlight = viewState.showBrowserMenuHighlight,
showChatMenu = viewState.showChatMenu,
experimentalIconsEnabled = viewState.experimentalIconsEnabled
)

if (!isInitialRender && newButtonState != previousButtonState) {
TransitionManager.beginDelayedTransition(toolbarContainer, omniBarButtonTransitionSet)
}

clearTextButton.isVisible = viewState.showClearButton
voiceSearchButton.isVisible = viewState.showVoiceSearch
tabsMenu.isVisible = viewState.showTabsMenu
fireIconMenu.isVisible = viewState.showFireIcon
browserMenu.isVisible = viewState.showBrowserMenu
browserMenuHighlight.isVisible = viewState.showBrowserMenuHighlight
spacer.isVisible = viewState.showVoiceSearch && viewState.showClearButton
spacer.isVisible = viewState.showVoiceSearch || viewState.showClearButton
aiChatMenu.isVisible = viewState.showChatMenu
toolbarContainer.requestLayout()

isInitialRender = false
previousButtonState = newButtonState
}

private fun renderBrowserMode(viewState: ViewState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,11 @@ class OmnibarLayoutViewModel @Inject constructor(
val hasUnreadTabs: Boolean = false,
val shouldUpdateTabsCount: Boolean = false,
val showVoiceSearch: Boolean = false,
val showClearButton: Boolean = false,
val showTabsMenu: Boolean = true,
val showFireIcon: Boolean = true,
val showBrowserMenu: Boolean = true,
val showClearButton: Boolean = true,
val showTabsMenu: Boolean = false,
val showFireIcon: Boolean = false,
val showBrowserMenu: Boolean = false,
val showChatMenu: Boolean = true,
val showBrowserMenuHighlight: Boolean = false,
val scrollingEnabled: Boolean = true,
val isLoading: Boolean = false,
Expand Down Expand Up @@ -148,13 +149,41 @@ class OmnibarLayoutViewModel @Inject constructor(
logVoiceSearchAvailability()
}

private fun onOmnibarClicked() {
Timber.d("Omnibar: onOmnibarClicked")

viewModelScope.launch {
command.send(Command.CancelTrackersAnimation)
}

_viewState.update {
it.copy(
hasFocus = true,
expanded = true,
leadingIconState = SEARCH,
highlightPrivacyShield = HighlightableButton.Gone,
showClearButton = true,
showTabsMenu = false,
showFireIcon = false,
showBrowserMenu = false,
showChatMenu = true,
showVoiceSearch = shouldShowVoiceSearch(
hasFocus = true,
query = _viewState.value.omnibarText,
hasQueryChanged = false,
urlLoaded = _viewState.value.url,
),
)
}
}

fun onOmnibarFocusChanged(
hasFocus: Boolean,
query: String,
) {
Timber.d("Omnibar: onOmnibarFocusChanged")
val showClearButton = hasFocus && query.isNotBlank()
val showControls = query.isBlank()
val showClearButton = hasFocus
val showControls = !hasFocus

if (hasFocus) {
viewModelScope.launch {
Expand All @@ -171,6 +200,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showTabsMenu = showControls,
showFireIcon = showControls,
showBrowserMenu = showControls,
showChatMenu = !showControls,
showVoiceSearch = shouldShowVoiceSearch(
hasFocus = true,
query = _viewState.value.omnibarText,
Expand Down Expand Up @@ -204,6 +234,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showTabsMenu = true,
showFireIcon = true,
showBrowserMenu = true,
showChatMenu = false,
showVoiceSearch = shouldShowVoiceSearch(
hasFocus = false,
query = _viewState.value.omnibarText,
Expand Down Expand Up @@ -296,6 +327,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showBrowserMenu = true,
showTabsMenu = false,
showFireIcon = false,
showChatMenu = false,
)
}
}
Expand Down Expand Up @@ -366,6 +398,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showBrowserMenu = showControls,
showTabsMenu = showControls,
showFireIcon = showControls,
showChatMenu = !showControls,
)
}
}
Expand Down Expand Up @@ -445,6 +478,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showBrowserMenu = showControls,
showTabsMenu = showControls,
showFireIcon = showControls,
showChatMenu = !showControls,
showClearButton = showClearButton,
showVoiceSearch = shouldShowVoiceSearch(
hasFocus = hasFocus,
Expand Down Expand Up @@ -534,6 +568,7 @@ class OmnibarLayoutViewModel @Inject constructor(

fun onUserTouchedOmnibarTextInput(touchAction: Int) {
Timber.d("Omnibar: onUserTouchedOmnibarTextInput")
onOmnibarClicked()
if (touchAction == ACTION_UP) {
firePixelBasedOnCurrentUrl(
AppPixelName.ADDRESS_BAR_NEW_TAB_PAGE_CLICKED,
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/res/drawable/ic_ai_chat_outline_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.382,6.284C12.282,5.886 11.717,5.886 11.618,6.284L11.247,7.767C10.953,8.942 10.036,9.86 8.861,10.153L7.377,10.524C6.98,10.624 6.98,11.189 7.377,11.288L8.861,11.659C10.036,11.953 10.953,12.87 11.247,14.045L11.618,15.529C11.717,15.926 12.282,15.926 12.382,15.529L12.753,14.045C13.047,12.87 13.964,11.953 15.139,11.659L16.623,11.288C17.02,11.189 17.02,10.624 16.623,10.524L15.139,10.153C13.964,9.86 13.047,8.942 12.753,7.767L12.382,6.284Z"
android:fillColor="?attr/daxColorPrimaryIcon" />
<path
android:pathData="M4.268,21.916C7.587,21.346 13.404,20.243 15.768,19.159C19.423,17.833 22,14.638 22,10.906C22,5.987 17.523,2 12,2C6.477,2 2,5.987 2,10.906C2,13.374 3.127,15.607 4.947,17.22C5.346,17.574 5.422,18.187 5.072,18.59L3.455,20.454C2.903,21.091 3.438,22.058 4.268,21.916ZM14.934,17.341L15.009,17.306L15.087,17.278C18.09,16.189 20,13.656 20,10.906C20,7.304 16.642,4 12,4C7.358,4 4,7.304 4,10.906C4,12.739 4.832,14.446 6.274,15.723C7.258,16.596 7.641,18.136 6.937,19.398C7.916,19.21 8.943,19.001 9.941,18.778C12.12,18.293 13.969,17.784 14.934,17.341Z"
android:fillColor="?attr/daxColorPrimaryIcon"
android:fillType="evenOdd"/>
</vector>
Loading

0 comments on commit 0f630b5

Please sign in to comment.