Skip to content

Add AI entry point to search bar #5692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7b5ea06
Add updated AI entry point
joshliebe Apr 14, 2025
87a0591
Fix lint issues
joshliebe Apr 14, 2025
b5851de
Fix bottom omnibar
joshliebe Apr 15, 2025
685afca
Add address bar config
joshliebe Apr 15, 2025
5bd6ba4
Remove negative margin
joshliebe Apr 15, 2025
49ded80
Hide setting when entry point is disabled
joshliebe Apr 15, 2025
4db8f9f
Fix linting
joshliebe Apr 15, 2025
6e592a1
Set default value of duckChatShowInAddressBar to duckChatShowInBrowse…
joshliebe Apr 17, 2025
1602126
Fix merge conflicts
joshliebe Apr 23, 2025
3c03639
Re-integrate changes
joshliebe Apr 23, 2025
95d4271
Fix linting
joshliebe Apr 23, 2025
26122b8
Merge branch 'develop' into feature/josh/ai-search-bar
joshliebe Apr 23, 2025
6dd27a3
Only show search string when setting is enabled
joshliebe Apr 23, 2025
e056763
Branch on experimental settings
joshliebe Apr 23, 2025
e22b849
Fix formatting
joshliebe Apr 23, 2025
349b883
Add BrowserTabViewModel tests
joshliebe Apr 23, 2025
c8e0fd5
Add DuckChatSettingsViewModel tests
joshliebe Apr 23, 2025
12e5db0
Add DuckChatDataStore tests
joshliebe Apr 23, 2025
a3c3f73
Add OmnibarLayoutViewModel tests
joshliebe Apr 23, 2025
de24790
Translate Show Duck.ai in Address Bar (#5953)
joshliebe Apr 23, 2025
9ea8c0b
Add RealDuckChat tests
joshliebe Apr 23, 2025
c7b2a49
Fix formatting
joshliebe Apr 23, 2025
f8cf1e1
Use Space instead of FrameLayout
joshliebe Apr 23, 2025
6340858
Rename ButtonState to TransitionState
joshliebe Apr 23, 2025
2d67f82
Hide Duck.ai on custom tabs
joshliebe Apr 23, 2025
fccee4a
Remove isInitialRender boolean
joshliebe Apr 23, 2025
ddcb043
Move address bar settings into separate feature
joshliebe Apr 23, 2025
fa644be
Add RealOmnibarAnimationManagerTest
joshliebe Apr 23, 2025
ba3519f
Use correct remote config URL
joshliebe Apr 23, 2025
b6db808
Move logic into renderBrowserMode
joshliebe Apr 23, 2025
316df95
Move logic into renderHint
joshliebe Apr 23, 2025
451d2ed
Remove redundant case
joshliebe Apr 24, 2025
7c75343
Remove unfocus when keyboard hidden
joshliebe Apr 24, 2025
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 @@ -6218,6 +6218,32 @@ class BrowserTabViewModelTest {
assertCommandIssued<Command.StartTrackersExperimentShieldPopAnimation>()
}

@Test
fun whenOpenDuckChatWithNonEmptyQueryThenOpenWithAutoPrompt() = runTest {
val query = "example"

testee.openDuckChat("example")

verify(mockDuckChat).openDuckChatWithAutoPrompt(query)
verify(mockDuckChat, never()).openDuckChat()
}

@Test
fun whenOpenDuckChatWithEmptyStringQueryThenOpenDuckChat() = runTest {
testee.openDuckChat("")

verify(mockDuckChat).openDuckChat()
verify(mockDuckChat, never()).openDuckChatWithAutoPrompt(any())
}

@Test
fun whenOpenDuckChatWithNullQueryThenOpenDuckChat() = runTest {
testee.openDuckChat(null)

verify(mockDuckChat).openDuckChat()
verify(mockDuckChat, never()).openDuckChatWithAutoPrompt(any())
}

private fun aCredential(): LoginCredentials {
return LoginCredentials(domain = null, username = null, password = null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,10 @@ open class BrowserActivity : DuckDuckGoActivity() {
binding.bottomMockupToolbar
}
}

if (!duckChat.showInAddressBar()) {
toolbarMockupBinding.aiChatIconMenuMockup.isVisible = false
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2708,6 +2708,10 @@ class BrowserTabFragment :
override fun onVoiceSearchPressed() {
onOmnibarVoiceSearchPressed()
}

override fun onDuckChatButtonPressed() {
onOmnibarDuckChatPressed(omnibar.getText())
}
},
)
omnibar.configureFadeOmnibarItemPressedListeners(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4097,6 +4097,14 @@ class BrowserTabViewModel @Inject constructor(
site?.resetTrackingEvents()
}

fun openDuckChat(query: String?) {
if (query?.isNotEmpty() == true) {
duckChat.openDuckChatWithAutoPrompt(query)
} else {
duckChat.openDuckChat()
}
}

companion object {
private const val FIXED_PROGRESS = 50

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

interface FindInPageListener {
Expand Down
132 changes: 129 additions & 3 deletions app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@ 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
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.transition.doOnEnd
import androidx.core.view.doOnLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
Expand All @@ -51,6 +57,7 @@ import com.duckduckgo.app.browser.databinding.IncludeFindInPageBinding
import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.CustomTab
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.NewTab
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.ChangeCustomTabTitle
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.DisableVoiceSearch
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.HighlightOmnibarItem
Expand All @@ -70,6 +77,7 @@ import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.ViewState
import com.duckduckgo.app.browser.omnibar.animations.BrowserTrackersAnimatorHelper
import com.duckduckgo.app.browser.omnibar.animations.PrivacyShieldAnimationHelper
import com.duckduckgo.app.browser.omnibar.animations.TrackersAnimatorListener
import com.duckduckgo.app.browser.omnibar.animations.omnibaranimation.OmnibarAnimationManager
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
import com.duckduckgo.app.browser.tabswitcher.TabSwitcherButton
import com.duckduckgo.app.browser.viewstate.LoadingViewState
Expand All @@ -80,6 +88,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.trackerdetection.model.Entity
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.experiments.visual.AppPersonalityFeature
import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore
import com.duckduckgo.common.ui.view.KeyboardAwareEditText
import com.duckduckgo.common.ui.view.KeyboardAwareEditText.ShowSuggestionsListener
import com.duckduckgo.common.ui.view.gone
Expand All @@ -92,6 +101,7 @@ import com.duckduckgo.common.utils.FragmentViewModelFactory
import com.duckduckgo.common.utils.extensions.replaceTextChangedListener
import com.duckduckgo.common.utils.text.TextChangedWatcher
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.duckchat.api.DuckChat
import com.google.android.material.appbar.AppBarLayout
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
Expand Down Expand Up @@ -132,6 +142,17 @@ open class OmnibarLayout @JvmOverloads constructor(
data class LoadingStateChange(val loadingViewState: LoadingViewState) : StateChange()
}

data class TransitionState(
val showClearButton: Boolean,
val showVoiceSearch: Boolean,
val showTabsMenu: Boolean,
val showFireIcon: Boolean,
val showBrowserMenu: Boolean,
val showBrowserMenuHighlight: Boolean,
val showChatMenu: Boolean,
val showSpacer: Boolean,
)

@Inject
lateinit var viewModelFactory: FragmentViewModelFactory

Expand All @@ -144,12 +165,23 @@ open class OmnibarLayout @JvmOverloads constructor(
@Inject
lateinit var pixel: Pixel

@Inject
lateinit var duckChat: DuckChat

@Inject
lateinit var dispatchers: DispatcherProvider

@Inject
lateinit var appPersonalityFeature: AppPersonalityFeature

@Inject
lateinit var visualDesignExperimentDataStore: VisualDesignExperimentDataStore

@Inject
lateinit var omnibarAnimationManager: OmnibarAnimationManager

private var previousTransitionState: TransitionState? = null

private val lifecycleOwner: LifecycleOwner by lazy {
requireNotNull(findViewTreeLifecycleOwner())
}
Expand All @@ -171,14 +203,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 @@ -198,6 +231,30 @@ 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 spacer1X: View? by lazy { findViewById(R.id.spacer1X) }
internal val spacer2X: View? by lazy { findViewById(R.id.spacer2X) }
internal val omniBarButtonTransitionSet: TransitionSet by lazy {
TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(
ChangeBounds().apply {
duration = omnibarAnimationManager.getChangeBoundsDuration()
interpolator = OvershootInterpolator(omnibarAnimationManager.getTension())
},
)
addTransition(
Fade().apply {
duration = omnibarAnimationManager.getFadeDuration()
addTarget(clearTextButton)
addTarget(voiceSearchButton)
addTarget(fireIconMenu)
addTarget(tabsMenu)
addTarget(aiChatMenu)
addTarget(browserMenu)
},
)
}
}

internal fun omnibarViews(): List<View> = listOf(
clearTextButton,
Expand Down Expand Up @@ -401,6 +458,9 @@ open class OmnibarLayout @JvmOverloads constructor(
browserMenu.setOnClickListener {
omnibarItemPressedListener?.onBrowserMenuPressed()
}
aiChatMenu?.setOnClickListener {
omnibarItemPressedListener?.onDuckChatButtonPressed()
}
shieldIcon.setOnClickListener {
if (isAttachedToWindow) {
viewModel.onPrivacyShieldButtonPressed()
Expand Down Expand Up @@ -428,7 +488,6 @@ open class OmnibarLayout @JvmOverloads constructor(
is CustomTab -> {
renderCustomTabMode(viewState, viewState.viewMode)
}

else -> {
renderBrowserMode(viewState)
}
Expand All @@ -439,7 +498,16 @@ open class OmnibarLayout @JvmOverloads constructor(
} else {
lastSeenPrivacyShield = null
}
renderButtons(viewState)

if (visualDesignExperimentDataStore.experimentState.value.isEnabled) {
renderButtons(viewState)
} else {
renderAnimatedButtons(viewState)
}

omniBarButtonTransitionSet.doOnEnd {
omnibarTextInput.requestLayout()
}
}

open fun processCommand(command: OmnibarLayoutViewModel.Command) {
Expand Down Expand Up @@ -548,6 +616,54 @@ open class OmnibarLayout @JvmOverloads constructor(
spacer.isVisible = viewState.showVoiceSearch && viewState.showClearButton
}

private fun renderAnimatedButtons(viewState: ViewState) {
val newTransitionState = TransitionState(
showClearButton = viewState.showClearButton,
showVoiceSearch = viewState.showVoiceSearch,
showTabsMenu = viewState.showTabsMenu,
showFireIcon = viewState.showFireIcon,
showBrowserMenu = viewState.showBrowserMenu,
showBrowserMenuHighlight = viewState.showBrowserMenuHighlight,
showChatMenu = duckChat.showInAddressBar() && (viewState.showChatMenu || viewState.viewMode is NewTab),
showSpacer = viewState.showClearButton || viewState.showVoiceSearch,
)

if (omnibarAnimationManager.isFeatureEnabled() &&
previousTransitionState != null &&
newTransitionState != previousTransitionState &&
!viewState.isLoading
) {
TransitionManager.beginDelayedTransition(toolbarContainer, omniBarButtonTransitionSet)
}

if (!newTransitionState.showVoiceSearch) {
clearTextButton.isInvisible = !newTransitionState.showClearButton
spacer1X?.isVisible = newTransitionState.showSpacer
spacer2X?.isVisible = false
} else {
clearTextButton.isVisible = newTransitionState.showClearButton
if (newTransitionState.showClearButton) {
spacer2X?.isVisible = newTransitionState.showSpacer
spacer1X?.isVisible = false
} else {
spacer1X?.isVisible = newTransitionState.showSpacer
spacer2X?.isVisible = false
}
}
voiceSearchButton.isInvisible = !newTransitionState.showVoiceSearch
tabsMenu.isVisible = newTransitionState.showTabsMenu
fireIconMenu.isVisible = newTransitionState.showFireIcon
browserMenu.isVisible = newTransitionState.showBrowserMenu
browserMenuHighlight.isVisible = newTransitionState.showBrowserMenuHighlight
aiChatMenu?.isVisible = newTransitionState.showChatMenu

if (omnibarAnimationManager.isFeatureEnabled()) {
toolbarContainer.requestLayout()
}

previousTransitionState = newTransitionState
}

private fun renderBrowserMode(viewState: ViewState) {
renderOutline(viewState.hasFocus)
if (viewState.updateOmnibarText) {
Expand All @@ -572,6 +688,16 @@ open class OmnibarLayout @JvmOverloads constructor(
renderPulseAnimation(viewState)

renderLeadingIconState(viewState.leadingIconState)

renderHint(viewState)
}

private fun renderHint(viewState: ViewState) {
if (viewState.viewMode is NewTab && duckChat.showInAddressBar()) {
omnibarTextInput.hint = context.getString(R.string.search)
} else {
omnibarTextInput.hint = context.getString(R.string.omnibarInputHint)
}
}

private fun renderCustomTabMode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class OmnibarLayoutViewModel @Inject constructor(
val showTabsMenu: Boolean = true,
val showFireIcon: Boolean = true,
val showBrowserMenu: Boolean = true,
val showChatMenu: Boolean = true,
val showBrowserMenuHighlight: Boolean = false,
val showChat: Boolean = false,
val scrollingEnabled: Boolean = true,
Expand Down Expand Up @@ -196,6 +197,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showFireIcon = showControls,
showBrowserMenu = showControls,
showChat = shouldShowAIChat(),
showChatMenu = !showControls,
showVoiceSearch = shouldShowVoiceSearch(
hasFocus = true,
query = _viewState.value.omnibarText,
Expand Down Expand Up @@ -230,6 +232,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showFireIcon = true,
showBrowserMenu = true,
showChat = shouldShowAIChat(),
showChatMenu = false,
showVoiceSearch = shouldShowVoiceSearch(
hasFocus = false,
query = _viewState.value.omnibarText,
Expand Down Expand Up @@ -330,6 +333,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showBrowserMenu = true,
showTabsMenu = false,
showFireIcon = false,
showChatMenu = false,
)
}
}
Expand Down Expand Up @@ -392,6 +396,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showBrowserMenu = showControls,
showTabsMenu = showControls,
showFireIcon = showControls,
showChatMenu = false,
)
}
}
Expand Down Expand Up @@ -471,6 +476,7 @@ class OmnibarLayoutViewModel @Inject constructor(
showBrowserMenu = showControls,
showTabsMenu = showControls,
showFireIcon = showControls,
showChatMenu = !showControls,
showClearButton = showClearButton,
showVoiceSearch = shouldShowVoiceSearch(
hasFocus = hasFocus,
Expand Down
Loading
Loading