diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index aa4f96061314..0bbfec41c666 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -2512,6 +2512,10 @@ class BrowserTabFragment : override fun onVoiceSearchPressed() { onOmnibarVoiceSearchPressed() } + + override fun onDuckChatButtonPressed() { + onOmnibarDuckChatPressed(omnibar.getText()) + } }, ) } @@ -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 { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt index 78765fa9bdff..e1e1de9d0f28 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/Omnibar.kt @@ -110,6 +110,7 @@ class Omnibar( fun onCustomTabClosePressed() fun onCustomTabPrivacyDashboardPressed() fun onVoiceSearchPressed() + fun onDuckChatButtonPressed() } interface FindInPageListener { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt index c8278fcbeeaa..fa60b0fccf57 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt @@ -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 @@ -159,6 +164,7 @@ 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) } @@ -166,7 +172,7 @@ open class OmnibarLayout @JvmOverloads constructor( 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), @@ -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 = listOf( clearTextButton, @@ -374,6 +401,9 @@ open class OmnibarLayout @JvmOverloads constructor( browserMenu.setOnClickListener { omnibarItemPressedListener?.onBrowserMenuPressed() } + aiChatMenu.setOnClickListener { + omnibarItemPressedListener?.onDuckChatButtonPressed() + } shieldIcon.setOnClickListener { if (isAttachedToWindow) { viewModel.onPrivacyShieldButtonPressed() @@ -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) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt index 137a2b380a6e..9c34395d7ad0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt @@ -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, @@ -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 { @@ -171,6 +200,7 @@ class OmnibarLayoutViewModel @Inject constructor( showTabsMenu = showControls, showFireIcon = showControls, showBrowserMenu = showControls, + showChatMenu = !showControls, showVoiceSearch = shouldShowVoiceSearch( hasFocus = true, query = _viewState.value.omnibarText, @@ -204,6 +234,7 @@ class OmnibarLayoutViewModel @Inject constructor( showTabsMenu = true, showFireIcon = true, showBrowserMenu = true, + showChatMenu = false, showVoiceSearch = shouldShowVoiceSearch( hasFocus = false, query = _viewState.value.omnibarText, @@ -296,6 +327,7 @@ class OmnibarLayoutViewModel @Inject constructor( showBrowserMenu = true, showTabsMenu = false, showFireIcon = false, + showChatMenu = false, ) } } @@ -366,6 +398,7 @@ class OmnibarLayoutViewModel @Inject constructor( showBrowserMenu = showControls, showTabsMenu = showControls, showFireIcon = showControls, + showChatMenu = !showControls, ) } } @@ -445,6 +478,7 @@ class OmnibarLayoutViewModel @Inject constructor( showBrowserMenu = showControls, showTabsMenu = showControls, showFireIcon = showControls, + showChatMenu = !showControls, showClearButton = showClearButton, showVoiceSearch = shouldShowVoiceSearch( hasFocus = hasFocus, @@ -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, diff --git a/app/src/main/res/drawable/ic_ai_chat_outline_24.xml b/app/src/main/res/drawable/ic_ai_chat_outline_24.xml new file mode 100644 index 000000000000..8aa3cb89aabe --- /dev/null +++ b/app/src/main/res/drawable/ic_ai_chat_outline_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/view_new_omnibar.xml b/app/src/main/res/layout/view_new_omnibar.xml index a765950662e2..f480436efea5 100644 --- a/app/src/main/res/layout/view_new_omnibar.xml +++ b/app/src/main/res/layout/view_new_omnibar.xml @@ -39,7 +39,7 @@ app:contentInsetStart="0dp" android:layout_marginHorizontal="@dimen/keyline_2" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/fireIconMenu" + app:layout_constraintEnd_toStartOf="@+id/iconsContainer" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:popupTheme="@style/Widget.DuckDuckGo.PopUpOverflowMenu"> @@ -216,7 +216,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - @@ -217,7 +217,7 @@ - + app:layout_constraintTop_toTopOf="parent"> - - - - - - + android:layout_height="match_parent" + android:layout_marginEnd="6dp" + android:padding="@dimen/keyline_2" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/fireIconMenu" + app:layout_constraintHorizontal_chainStyle="spread" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + + + - + + + - - + android:layout_marginHorizontal="@dimen/keyline_0" + android:padding="@dimen/keyline_2" + android:background="?selectableItemBackgroundBorderless" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/browserMenu" + app:layout_constraintStart_toEndOf="@id/fireIconMenu" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + - +