Skip to content

Commit 435c5af

Browse files
joshliebedaxmobile
andauthored
Add AI entry point to search bar (#5692)
Task/Issue URL: https://app.asana.com/0/488551667048375/1209499349481348 ### Description - Adds a Duck.ai entry point to the address bar - Adds animations between the focussed and unfocussed states - Adds a setting to enable/disable the entry point ### Steps to test this PR _Default behavior_ - [ ] Check out this branch - [ ] Verify that the omnibar has the same behavior as current prod - [ ] The Duck.ai “Show in address bar” setting should not be visible _Entry point enabled_ - [ ] Point at the config linked in the task - [ ] Verify that the new tab has the entry point - [ ] Focus the address bar - [ ] Verify that the other buttons animate away and the entry point is visible - [ ] Search for something - [ ] Verify that the entry point is not visible - [ ] Focus the address bar - [ ] Verify that the entry point is visible _Setting disabled_ - [ ] Disable “Show in address bar” in settings - [ ] Verify that the entry point is no longer visible ### UI changes ![focussed_combined](https://github.com/user-attachments/assets/8e86e24a-c84a-4ba2-b02c-c99bd40a9146) ![input_combined](https://github.com/user-attachments/assets/f611fe83-6b23-484e-8947-11098799d4f4) ![voice_search_combined](https://github.com/user-attachments/assets/a4ae7eb8-c952-44ce-8cb5-6d7f3f10ce0f) ![settings_combined](https://github.com/user-attachments/assets/de4c6fef-c075-44cd-8ae7-9480d8e262a4) --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1209988558452810 --------- Co-authored-by: Dax The Translator <[email protected]>
1 parent 9c9b9d7 commit 435c5af

File tree

49 files changed

+1108
-224
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1108
-224
lines changed

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6218,6 +6218,32 @@ class BrowserTabViewModelTest {
62186218
assertCommandIssued<Command.StartTrackersExperimentShieldPopAnimation>()
62196219
}
62206220

6221+
@Test
6222+
fun whenOpenDuckChatWithNonEmptyQueryThenOpenWithAutoPrompt() = runTest {
6223+
val query = "example"
6224+
6225+
testee.openDuckChat("example")
6226+
6227+
verify(mockDuckChat).openDuckChatWithAutoPrompt(query)
6228+
verify(mockDuckChat, never()).openDuckChat()
6229+
}
6230+
6231+
@Test
6232+
fun whenOpenDuckChatWithEmptyStringQueryThenOpenDuckChat() = runTest {
6233+
testee.openDuckChat("")
6234+
6235+
verify(mockDuckChat).openDuckChat()
6236+
verify(mockDuckChat, never()).openDuckChatWithAutoPrompt(any())
6237+
}
6238+
6239+
@Test
6240+
fun whenOpenDuckChatWithNullQueryThenOpenDuckChat() = runTest {
6241+
testee.openDuckChat(null)
6242+
6243+
verify(mockDuckChat).openDuckChat()
6244+
verify(mockDuckChat, never()).openDuckChatWithAutoPrompt(any())
6245+
}
6246+
62216247
private fun aCredential(): LoginCredentials {
62226248
return LoginCredentials(domain = null, username = null, password = null)
62236249
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,10 @@ open class BrowserActivity : DuckDuckGoActivity() {
11171117
binding.bottomMockupToolbar
11181118
}
11191119
}
1120+
1121+
if (!duckChat.showInAddressBar()) {
1122+
toolbarMockupBinding.aiChatIconMenuMockup.isVisible = false
1123+
}
11201124
}
11211125
}
11221126
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,10 @@ class BrowserTabFragment :
27082708
override fun onVoiceSearchPressed() {
27092709
onOmnibarVoiceSearchPressed()
27102710
}
2711+
2712+
override fun onDuckChatButtonPressed() {
2713+
onOmnibarDuckChatPressed(omnibar.getText())
2714+
}
27112715
},
27122716
)
27132717
omnibar.configureFadeOmnibarItemPressedListeners(

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4097,6 +4097,14 @@ class BrowserTabViewModel @Inject constructor(
40974097
site?.resetTrackingEvents()
40984098
}
40994099

4100+
fun openDuckChat(query: String?) {
4101+
if (query?.isNotEmpty() == true) {
4102+
duckChat.openDuckChatWithAutoPrompt(query)
4103+
} else {
4104+
duckChat.openDuckChat()
4105+
}
4106+
}
4107+
41004108
companion object {
41014109
private const val FIXED_PROGRESS = 50
41024110

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ class Omnibar(
133133
fun onCustomTabClosePressed()
134134
fun onCustomTabPrivacyDashboardPressed()
135135
fun onVoiceSearchPressed()
136+
fun onDuckChatButtonPressed()
136137
}
137138

138139
interface FindInPageListener {

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

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,23 @@ import android.content.Context
2121
import android.graphics.Color
2222
import android.graphics.drawable.ColorDrawable
2323
import android.text.Editable
24+
import android.transition.ChangeBounds
25+
import android.transition.Fade
26+
import android.transition.TransitionManager
27+
import android.transition.TransitionSet
2428
import android.util.AttributeSet
2529
import android.view.KeyEvent
2630
import android.view.View
2731
import android.view.ViewGroup
32+
import android.view.animation.OvershootInterpolator
2833
import android.view.inputmethod.EditorInfo
2934
import android.widget.FrameLayout
3035
import android.widget.ImageView
3136
import android.widget.ProgressBar
3237
import android.widget.TextView
3338
import androidx.appcompat.widget.Toolbar
3439
import androidx.coordinatorlayout.widget.CoordinatorLayout
40+
import androidx.core.transition.doOnEnd
3541
import androidx.core.view.doOnLayout
3642
import androidx.core.view.isInvisible
3743
import androidx.core.view.isVisible
@@ -51,6 +57,7 @@ import com.duckduckgo.app.browser.databinding.IncludeFindInPageBinding
5157
import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState
5258
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode
5359
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.CustomTab
60+
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode.NewTab
5461
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.ChangeCustomTabTitle
5562
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.DisableVoiceSearch
5663
import com.duckduckgo.app.browser.omnibar.OmnibarLayout.Decoration.HighlightOmnibarItem
@@ -70,6 +77,7 @@ import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.ViewState
7077
import com.duckduckgo.app.browser.omnibar.animations.BrowserTrackersAnimatorHelper
7178
import com.duckduckgo.app.browser.omnibar.animations.PrivacyShieldAnimationHelper
7279
import com.duckduckgo.app.browser.omnibar.animations.TrackersAnimatorListener
80+
import com.duckduckgo.app.browser.omnibar.animations.omnibaranimation.OmnibarAnimationManager
7381
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
7482
import com.duckduckgo.app.browser.tabswitcher.TabSwitcherButton
7583
import com.duckduckgo.app.browser.viewstate.LoadingViewState
@@ -80,6 +88,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel
8088
import com.duckduckgo.app.trackerdetection.model.Entity
8189
import com.duckduckgo.common.ui.DuckDuckGoActivity
8290
import com.duckduckgo.common.ui.experiments.visual.AppPersonalityFeature
91+
import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore
8392
import com.duckduckgo.common.ui.view.KeyboardAwareEditText
8493
import com.duckduckgo.common.ui.view.KeyboardAwareEditText.ShowSuggestionsListener
8594
import com.duckduckgo.common.ui.view.gone
@@ -92,6 +101,7 @@ import com.duckduckgo.common.utils.FragmentViewModelFactory
92101
import com.duckduckgo.common.utils.extensions.replaceTextChangedListener
93102
import com.duckduckgo.common.utils.text.TextChangedWatcher
94103
import com.duckduckgo.di.scopes.FragmentScope
104+
import com.duckduckgo.duckchat.api.DuckChat
95105
import com.google.android.material.appbar.AppBarLayout
96106
import javax.inject.Inject
97107
import kotlinx.coroutines.flow.collectLatest
@@ -132,6 +142,17 @@ open class OmnibarLayout @JvmOverloads constructor(
132142
data class LoadingStateChange(val loadingViewState: LoadingViewState) : StateChange()
133143
}
134144

145+
data class TransitionState(
146+
val showClearButton: Boolean,
147+
val showVoiceSearch: Boolean,
148+
val showTabsMenu: Boolean,
149+
val showFireIcon: Boolean,
150+
val showBrowserMenu: Boolean,
151+
val showBrowserMenuHighlight: Boolean,
152+
val showChatMenu: Boolean,
153+
val showSpacer: Boolean,
154+
)
155+
135156
@Inject
136157
lateinit var viewModelFactory: FragmentViewModelFactory
137158

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

168+
@Inject
169+
lateinit var duckChat: DuckChat
170+
147171
@Inject
148172
lateinit var dispatchers: DispatcherProvider
149173

150174
@Inject
151175
lateinit var appPersonalityFeature: AppPersonalityFeature
152176

177+
@Inject
178+
lateinit var visualDesignExperimentDataStore: VisualDesignExperimentDataStore
179+
180+
@Inject
181+
lateinit var omnibarAnimationManager: OmnibarAnimationManager
182+
183+
private var previousTransitionState: TransitionState? = null
184+
153185
private val lifecycleOwner: LifecycleOwner by lazy {
154186
requireNotNull(findViewTreeLifecycleOwner())
155187
}
@@ -171,14 +203,15 @@ open class OmnibarLayout @JvmOverloads constructor(
171203
internal val omnibarTextInput: KeyboardAwareEditText by lazy { findViewById(R.id.omnibarTextInput) }
172204
internal val tabsMenu: TabSwitcherButton by lazy { findViewById(R.id.tabsMenu) }
173205
internal val fireIconMenu: FrameLayout by lazy { findViewById(R.id.fireIconMenu) }
206+
internal val aiChatMenu: FrameLayout? by lazy { findViewById(R.id.aiChatIconMenu) }
174207
internal val browserMenu: FrameLayout by lazy { findViewById(R.id.browserMenu) }
175208
internal val browserMenuHighlight: View by lazy { findViewById(R.id.browserMenuHighlight) }
176209
internal val cookieDummyView: View by lazy { findViewById(R.id.cookieDummyView) }
177210
internal val cookieAnimation: LottieAnimationView by lazy { findViewById(R.id.cookieAnimation) }
178211
internal val sceneRoot: ViewGroup by lazy { findViewById(R.id.sceneRoot) }
179212
internal val omniBarContainer: View by lazy { findViewById(R.id.omniBarContainer) }
180213
internal val toolbar: Toolbar by lazy { findViewById(R.id.toolbar) }
181-
internal val toolbarContainer: View by lazy { findViewById(R.id.toolbarContainer) }
214+
internal val toolbarContainer: ViewGroup by lazy { findViewById(R.id.toolbarContainer) }
182215
internal val customTabToolbarContainer by lazy {
183216
IncludeCustomTabToolbarBinding.bind(
184217
findViewById(R.id.customTabToolbarContainer),
@@ -198,6 +231,30 @@ open class OmnibarLayout @JvmOverloads constructor(
198231
internal val spacer: View by lazy { findViewById(R.id.spacer) }
199232
internal val trackersAnimation: LottieAnimationView by lazy { findViewById(R.id.trackersAnimation) }
200233
internal val duckPlayerIcon: ImageView by lazy { findViewById(R.id.duckPlayerIcon) }
234+
internal val spacer1X: View? by lazy { findViewById(R.id.spacer1X) }
235+
internal val spacer2X: View? by lazy { findViewById(R.id.spacer2X) }
236+
internal val omniBarButtonTransitionSet: TransitionSet by lazy {
237+
TransitionSet().apply {
238+
ordering = TransitionSet.ORDERING_TOGETHER
239+
addTransition(
240+
ChangeBounds().apply {
241+
duration = omnibarAnimationManager.getChangeBoundsDuration()
242+
interpolator = OvershootInterpolator(omnibarAnimationManager.getTension())
243+
},
244+
)
245+
addTransition(
246+
Fade().apply {
247+
duration = omnibarAnimationManager.getFadeDuration()
248+
addTarget(clearTextButton)
249+
addTarget(voiceSearchButton)
250+
addTarget(fireIconMenu)
251+
addTarget(tabsMenu)
252+
addTarget(aiChatMenu)
253+
addTarget(browserMenu)
254+
},
255+
)
256+
}
257+
}
201258

202259
internal fun omnibarViews(): List<View> = listOf(
203260
clearTextButton,
@@ -401,6 +458,9 @@ open class OmnibarLayout @JvmOverloads constructor(
401458
browserMenu.setOnClickListener {
402459
omnibarItemPressedListener?.onBrowserMenuPressed()
403460
}
461+
aiChatMenu?.setOnClickListener {
462+
omnibarItemPressedListener?.onDuckChatButtonPressed()
463+
}
404464
shieldIcon.setOnClickListener {
405465
if (isAttachedToWindow) {
406466
viewModel.onPrivacyShieldButtonPressed()
@@ -428,7 +488,6 @@ open class OmnibarLayout @JvmOverloads constructor(
428488
is CustomTab -> {
429489
renderCustomTabMode(viewState, viewState.viewMode)
430490
}
431-
432491
else -> {
433492
renderBrowserMode(viewState)
434493
}
@@ -439,7 +498,16 @@ open class OmnibarLayout @JvmOverloads constructor(
439498
} else {
440499
lastSeenPrivacyShield = null
441500
}
442-
renderButtons(viewState)
501+
502+
if (visualDesignExperimentDataStore.experimentState.value.isEnabled) {
503+
renderButtons(viewState)
504+
} else {
505+
renderAnimatedButtons(viewState)
506+
}
507+
508+
omniBarButtonTransitionSet.doOnEnd {
509+
omnibarTextInput.requestLayout()
510+
}
443511
}
444512

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

619+
private fun renderAnimatedButtons(viewState: ViewState) {
620+
val newTransitionState = TransitionState(
621+
showClearButton = viewState.showClearButton,
622+
showVoiceSearch = viewState.showVoiceSearch,
623+
showTabsMenu = viewState.showTabsMenu,
624+
showFireIcon = viewState.showFireIcon,
625+
showBrowserMenu = viewState.showBrowserMenu,
626+
showBrowserMenuHighlight = viewState.showBrowserMenuHighlight,
627+
showChatMenu = duckChat.showInAddressBar() && (viewState.showChatMenu || viewState.viewMode is NewTab),
628+
showSpacer = viewState.showClearButton || viewState.showVoiceSearch,
629+
)
630+
631+
if (omnibarAnimationManager.isFeatureEnabled() &&
632+
previousTransitionState != null &&
633+
newTransitionState != previousTransitionState &&
634+
!viewState.isLoading
635+
) {
636+
TransitionManager.beginDelayedTransition(toolbarContainer, omniBarButtonTransitionSet)
637+
}
638+
639+
if (!newTransitionState.showVoiceSearch) {
640+
clearTextButton.isInvisible = !newTransitionState.showClearButton
641+
spacer1X?.isVisible = newTransitionState.showSpacer
642+
spacer2X?.isVisible = false
643+
} else {
644+
clearTextButton.isVisible = newTransitionState.showClearButton
645+
if (newTransitionState.showClearButton) {
646+
spacer2X?.isVisible = newTransitionState.showSpacer
647+
spacer1X?.isVisible = false
648+
} else {
649+
spacer1X?.isVisible = newTransitionState.showSpacer
650+
spacer2X?.isVisible = false
651+
}
652+
}
653+
voiceSearchButton.isInvisible = !newTransitionState.showVoiceSearch
654+
tabsMenu.isVisible = newTransitionState.showTabsMenu
655+
fireIconMenu.isVisible = newTransitionState.showFireIcon
656+
browserMenu.isVisible = newTransitionState.showBrowserMenu
657+
browserMenuHighlight.isVisible = newTransitionState.showBrowserMenuHighlight
658+
aiChatMenu?.isVisible = newTransitionState.showChatMenu
659+
660+
if (omnibarAnimationManager.isFeatureEnabled()) {
661+
toolbarContainer.requestLayout()
662+
}
663+
664+
previousTransitionState = newTransitionState
665+
}
666+
551667
private fun renderBrowserMode(viewState: ViewState) {
552668
renderOutline(viewState.hasFocus)
553669
if (viewState.updateOmnibarText) {
@@ -572,6 +688,16 @@ open class OmnibarLayout @JvmOverloads constructor(
572688
renderPulseAnimation(viewState)
573689

574690
renderLeadingIconState(viewState.leadingIconState)
691+
692+
renderHint(viewState)
693+
}
694+
695+
private fun renderHint(viewState: ViewState) {
696+
if (viewState.viewMode is NewTab && duckChat.showInAddressBar()) {
697+
omnibarTextInput.hint = context.getString(R.string.search)
698+
} else {
699+
omnibarTextInput.hint = context.getString(R.string.omnibarInputHint)
700+
}
575701
}
576702

577703
private fun renderCustomTabMode(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class OmnibarLayoutViewModel @Inject constructor(
134134
val showTabsMenu: Boolean = true,
135135
val showFireIcon: Boolean = true,
136136
val showBrowserMenu: Boolean = true,
137+
val showChatMenu: Boolean = true,
137138
val showBrowserMenuHighlight: Boolean = false,
138139
val showChat: Boolean = false,
139140
val scrollingEnabled: Boolean = true,
@@ -196,6 +197,7 @@ class OmnibarLayoutViewModel @Inject constructor(
196197
showFireIcon = showControls,
197198
showBrowserMenu = showControls,
198199
showChat = shouldShowAIChat(),
200+
showChatMenu = !showControls,
199201
showVoiceSearch = shouldShowVoiceSearch(
200202
hasFocus = true,
201203
query = _viewState.value.omnibarText,
@@ -230,6 +232,7 @@ class OmnibarLayoutViewModel @Inject constructor(
230232
showFireIcon = true,
231233
showBrowserMenu = true,
232234
showChat = shouldShowAIChat(),
235+
showChatMenu = false,
233236
showVoiceSearch = shouldShowVoiceSearch(
234237
hasFocus = false,
235238
query = _viewState.value.omnibarText,
@@ -330,6 +333,7 @@ class OmnibarLayoutViewModel @Inject constructor(
330333
showBrowserMenu = true,
331334
showTabsMenu = false,
332335
showFireIcon = false,
336+
showChatMenu = false,
333337
)
334338
}
335339
}
@@ -392,6 +396,7 @@ class OmnibarLayoutViewModel @Inject constructor(
392396
showBrowserMenu = showControls,
393397
showTabsMenu = showControls,
394398
showFireIcon = showControls,
399+
showChatMenu = false,
395400
)
396401
}
397402
}
@@ -471,6 +476,7 @@ class OmnibarLayoutViewModel @Inject constructor(
471476
showBrowserMenu = showControls,
472477
showTabsMenu = showControls,
473478
showFireIcon = showControls,
479+
showChatMenu = !showControls,
474480
showClearButton = showClearButton,
475481
showVoiceSearch = shouldShowVoiceSearch(
476482
hasFocus = hasFocus,

0 commit comments

Comments
 (0)