Skip to content

Commit 3dc61a6

Browse files
authored
show only favorites without other NTP content on the Input Screen (#6342)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1210708810784115?focus=true ### Description Exposes favorites grid view so that it can be independently displayed on the Input Screen without having to bundle the whole NTP. ### Steps to test this PR - [x] Clear cache and open a new internal flavor of the app. - [x] Enabled Duck.ai -> Input Screen. - [x] Verify that you see the new visual design RMF banner. - [x] Click on the omnibar, as it focuses verify that you don't see the RMF banner. - [x] Add some favorites and verify they are visible on the "Search" page of the input screen.
1 parent 47de951 commit 3dc61a6

File tree

7 files changed

+169
-64
lines changed

7 files changed

+169
-64
lines changed

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/tabs/SearchTabFragment.kt

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ package com.duckduckgo.duckchat.impl.inputscreen.ui.tabs
1919
import android.os.Bundle
2020
import android.transition.Transition
2121
import android.view.View
22-
import android.view.ViewGroup
2322
import android.view.animation.OvershootInterpolator
2423
import androidx.core.content.ContextCompat
2524
import androidx.core.view.isVisible
2625
import androidx.lifecycle.ViewModelProvider
27-
import androidx.lifecycle.lifecycleScope
2826
import androidx.recyclerview.widget.LinearLayoutManager
2927
import com.duckduckgo.anvil.annotations.InjectWith
3028
import com.duckduckgo.browser.api.ui.BrowserScreens.PrivateSearchScreenNoParams
@@ -33,7 +31,6 @@ import com.duckduckgo.common.ui.view.gone
3331
import com.duckduckgo.common.ui.view.show
3432
import com.duckduckgo.common.ui.viewbinding.viewBinding
3533
import com.duckduckgo.common.utils.FragmentViewModelFactory
36-
import com.duckduckgo.common.utils.plugins.ActivePluginPoint
3734
import com.duckduckgo.di.scopes.FragmentScope
3835
import com.duckduckgo.duckchat.impl.R
3936
import com.duckduckgo.duckchat.impl.databinding.FragmentSearchTabBinding
@@ -44,15 +41,16 @@ import com.duckduckgo.duckchat.impl.inputscreen.autocomplete.SuggestionItemDecor
4441
import com.duckduckgo.duckchat.impl.inputscreen.ui.util.renderIfChanged
4542
import com.duckduckgo.duckchat.impl.inputscreen.ui.viewmodel.InputScreenViewModel
4643
import com.duckduckgo.navigation.api.GlobalActivityStarter
47-
import com.duckduckgo.newtabpage.api.NewTabPagePlugin
44+
import com.duckduckgo.savedsites.api.views.FavoritesGridConfig
45+
import com.duckduckgo.savedsites.api.views.FavoritesPlacement
46+
import com.duckduckgo.savedsites.api.views.SavedSitesViewsProvider
4847
import javax.inject.Inject
49-
import kotlinx.coroutines.launch
5048

5149
@InjectWith(FragmentScope::class)
5250
class SearchTabFragment : DuckDuckGoFragment(R.layout.fragment_search_tab) {
5351

5452
@Inject
55-
lateinit var newTabPagePlugins: ActivePluginPoint<NewTabPagePlugin>
53+
lateinit var savedSitesViewsProvider: SavedSitesViewsProvider
5654

5755
@Inject lateinit var viewModelFactory: FragmentViewModelFactory
5856

@@ -67,62 +65,62 @@ class SearchTabFragment : DuckDuckGoFragment(R.layout.fragment_search_tab) {
6765
private lateinit var autoCompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter
6866

6967
private val binding: FragmentSearchTabBinding by viewBinding()
68+
private lateinit var favoritesView: View
7069

7170
override fun onCreate(savedInstanceState: Bundle?) {
7271
super.onCreate(savedInstanceState)
7372
renderer = SearchInterstitialFragmentRenderer()
7473
}
7574

76-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
75+
override fun onViewCreated(
76+
view: View,
77+
savedInstanceState: Bundle?,
78+
) {
7779
super.onViewCreated(view, savedInstanceState)
7880

7981
requireActivity().window.sharedElementEnterTransition?.addListener(
8082
object : Transition.TransitionListener {
8183
override fun onTransitionEnd(transition: Transition) {
82-
setupNewTabPage()
84+
configureFavorites()
85+
configureAutoComplete()
86+
configureObservers()
8387
transition.removeListener(this)
8488
}
89+
8590
override fun onTransitionStart(transition: Transition) {}
8691
override fun onTransitionCancel(transition: Transition) {}
8792
override fun onTransitionPause(transition: Transition) {}
8893
override fun onTransitionResume(transition: Transition) {}
8994
},
9095
)
91-
92-
configureObservers()
93-
configureAutoComplete()
9496
}
9597

96-
private fun setupNewTabPage() {
97-
lifecycleScope.launch {
98-
newTabPagePlugins.getPlugins().firstOrNull()?.let { plugin ->
99-
val newTabView = plugin.getView(requireContext())
100-
newTabView.alpha = 0f
101-
102-
val displayMetrics = requireContext().resources.displayMetrics
103-
val slideDistance = displayMetrics.heightPixels * CONTENT_SLIDE_DISTANCE
104-
newTabView.translationY = -slideDistance
105-
106-
binding.contentContainer.addView(
107-
newTabView,
108-
ViewGroup.LayoutParams(
109-
ViewGroup.LayoutParams.MATCH_PARENT,
110-
ViewGroup.LayoutParams.MATCH_PARENT,
111-
),
112-
)
113-
114-
newTabView.animate()
115-
.alpha(1f)
116-
.setDuration(CONTENT_ANIMATION_DURATION)
117-
.start()
118-
119-
newTabView.animate()
120-
.translationY(0f)
121-
.setInterpolator(OvershootInterpolator(CONTENT_INTERPOLATOR_TENSION))
122-
.setDuration(CONTENT_ANIMATION_DURATION)
123-
.start()
124-
}
125-
}
98+
private fun configureFavorites() {
99+
val favoritesGridConfig = FavoritesGridConfig(
100+
isExpandable = false,
101+
showPlaceholders = false,
102+
showDaxWhenEmpty = true,
103+
placement = FavoritesPlacement.FOCUSED_STATE,
104+
)
105+
favoritesView = savedSitesViewsProvider.getFavoritesGridView(requireContext(), config = favoritesGridConfig)
106+
favoritesView.alpha = 0f
107+
108+
val displayMetrics = requireContext().resources.displayMetrics
109+
val slideDistance = displayMetrics.heightPixels * CONTENT_SLIDE_DISTANCE
110+
favoritesView.translationY = -slideDistance
111+
112+
binding.contentContainer.addView(favoritesView)
113+
114+
favoritesView.animate()
115+
.alpha(1f)
116+
.setDuration(CONTENT_ANIMATION_DURATION)
117+
.start()
118+
119+
favoritesView.animate()
120+
.translationY(0f)
121+
.setInterpolator(OvershootInterpolator(CONTENT_INTERPOLATOR_TENSION))
122+
.setDuration(CONTENT_ANIMATION_DURATION)
123+
.start()
126124
}
127125

128126
private fun configureAutoComplete() {

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ class InputScreenViewModel @Inject constructor(
122122
.flowOn(dispatchers.io())
123123
.onEach { filteredFavourites ->
124124
withContext(dispatchers.main()) {
125+
val currentState = currentAutoCompleteViewState()
125126
val favorites = filteredFavourites.map { it }
126-
autoCompleteViewState.value = currentAutoCompleteViewState().copy(favorites = favorites)
127+
val showFavorites = !currentState.showSuggestions && favorites.isNotEmpty()
128+
autoCompleteViewState.value = currentAutoCompleteViewState().copy(favorites = favorites, showFavorites = showFavorites)
127129
}
128130
}
129131
.launchIn(viewModelScope)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
package com.duckduckgo.savedsites.api.views
18+
19+
import android.content.Context
20+
import android.view.View
21+
22+
/**
23+
* Provides views for the Saved Sites feature.
24+
*/
25+
interface SavedSitesViewsProvider {
26+
27+
/**
28+
* Returns a view for displaying a grid of favorite sites.
29+
*/
30+
fun getFavoritesGridView(context: Context, config: FavoritesGridConfig? = null): View
31+
}
32+
33+
/**
34+
* Configuration for the Favorites Grid view.
35+
*
36+
* @property isExpandable If true and there are two or more rows, the favorites are collapsed and can be expanded.
37+
* If false, the favorites are always expanded.
38+
* @property showPlaceholders Whether to show placeholder with onboarding if favorites list is empty. Takes precedent over [showDaxWhenEmpty].
39+
* @property showDaxWhenEmpty Whether to show Dax icon if favorites list is empty.
40+
* @property placement The screen on which favorites are displayed.
41+
*/
42+
data class FavoritesGridConfig(
43+
val isExpandable: Boolean,
44+
val showPlaceholders: Boolean,
45+
val showDaxWhenEmpty: Boolean,
46+
val placement: FavoritesPlacement,
47+
)
48+
49+
enum class FavoritesPlacement {
50+
FOCUSED_STATE,
51+
NEW_TAB_PAGE,
52+
;
53+
54+
companion object {
55+
fun from(type: Int): FavoritesPlacement {
56+
return when (type) {
57+
0 -> FOCUSED_STATE
58+
1 -> NEW_TAB_PAGE
59+
else -> FOCUSED_STATE
60+
}
61+
}
62+
}
63+
}

saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import android.widget.LinearLayout
2828
import android.widget.PopupWindow
2929
import androidx.core.text.HtmlCompat
3030
import androidx.core.text.toSpannable
31+
import androidx.core.view.isVisible
3132
import androidx.fragment.app.FragmentManager
3233
import androidx.lifecycle.ViewModelProvider
3334
import androidx.lifecycle.findViewTreeLifecycleOwner
@@ -66,6 +67,8 @@ import com.duckduckgo.savedsites.api.models.SavedSite
6667
import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark
6768
import com.duckduckgo.savedsites.api.models.SavedSite.Favorite
6869
import com.duckduckgo.savedsites.api.models.SavedSitesNames
70+
import com.duckduckgo.savedsites.api.views.FavoritesGridConfig
71+
import com.duckduckgo.savedsites.api.views.FavoritesPlacement
6972
import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment
7073
import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.DeleteBookmarkListener
7174
import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.EditSavedSiteListener
@@ -74,7 +77,6 @@ import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Co
7477
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteFavoriteConfirmation
7578
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteSavedSiteConfirmation
7679
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.ShowEditSavedSiteDialog
77-
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Placement
7880
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.SavedSiteChangedViewState
7981
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.ViewState
8082
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionsAdapter.Companion.QUICK_ACCESS_GRID_MAX_COLUMNS
@@ -111,7 +113,8 @@ class FavouritesNewTabSectionView @JvmOverloads constructor(
111113

112114
private var isExpandable = true
113115
private var showPlaceholders = false
114-
private var placement = Placement.NEW_TAB_PAGE
116+
private var showDaxWhenEmpty = false
117+
private var placement: FavoritesPlacement = FavoritesPlacement.NEW_TAB_PAGE
115118

116119
private val binding: ViewNewTabFavouritesSectionBinding by viewBinding()
117120

@@ -142,11 +145,20 @@ class FavouritesNewTabSectionView @JvmOverloads constructor(
142145
).apply {
143146
isExpandable = getBoolean(R.styleable.FavouritesNewTabSectionView_isExpandable, true)
144147
showPlaceholders = getBoolean(R.styleable.FavouritesNewTabSectionView_showPlaceholders, true)
145-
placement = Placement.from(getInt(R.styleable.FavouritesNewTabSectionView_favoritesPlacement, 1))
148+
placement = FavoritesPlacement.from(getInt(R.styleable.FavouritesNewTabSectionView_favoritesPlacement, 1))
146149
recycle()
147150
}
148151
}
149152

153+
constructor(context: Context, config: FavoritesGridConfig?) : this(context, null, 0) {
154+
config?.let {
155+
isExpandable = it.isExpandable
156+
showPlaceholders = it.showPlaceholders
157+
showDaxWhenEmpty = it.showDaxWhenEmpty
158+
placement = it.placement
159+
}
160+
}
161+
150162
override fun onAttachedToWindow() {
151163
AndroidSupportInjection.inject(this)
152164
super.onAttachedToWindow()
@@ -345,6 +357,7 @@ class FavouritesNewTabSectionView @JvmOverloads constructor(
345357
}
346358
viewModel.onNewTabFavouritesShown()
347359
}
360+
binding.ddgLogo.isVisible = showDaxWhenEmpty && viewState.favourites.isEmpty()
348361
}
349362

350363
private fun processCommands(command: Command) {

saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.duckduckgo.savedsites.api.models.BookmarkFolder
3232
import com.duckduckgo.savedsites.api.models.SavedSite
3333
import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark
3434
import com.duckduckgo.savedsites.api.models.SavedSite.Favorite
35+
import com.duckduckgo.savedsites.api.views.FavoritesPlacement
3536
import com.duckduckgo.savedsites.impl.SavedSitesPixelName
3637
import com.duckduckgo.savedsites.impl.SavedSitesPixelName.*
3738
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteFavoriteConfirmation
@@ -71,23 +72,6 @@ class FavouritesNewTabSectionViewModel @Inject constructor(
7172
val bookmarkFolder: BookmarkFolder?,
7273
)
7374

74-
enum class Placement {
75-
FOCUSED_STATE,
76-
NEW_TAB_PAGE,
77-
;
78-
79-
companion object {
80-
fun from(type: Int): Placement {
81-
// same order as attrs-saved-sites.xml
82-
return when (type) {
83-
0 -> Placement.FOCUSED_STATE
84-
1 -> Placement.NEW_TAB_PAGE
85-
else -> Placement.FOCUSED_STATE
86-
}
87-
}
88-
}
89-
}
90-
9175
sealed class Command {
9276
class ShowEditSavedSiteDialog(val savedSiteChangedViewState: SavedSiteChangedViewState) : Command()
9377
class DeleteFavoriteConfirmation(val savedSite: SavedSite) : Command()
@@ -288,14 +272,14 @@ class FavouritesNewTabSectionViewModel @Inject constructor(
288272
pixel.fire(EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED)
289273
}
290274

291-
fun onFavoriteClicked(placement: Placement) {
275+
fun onFavoriteClicked(placement: FavoritesPlacement) {
292276
pixel.fire(formatPixelWithPlacement(FAVOURITE_CLICKED, placement))
293277
pixel.fire(formatPixelWithPlacement(FAVOURITE_CLICKED_DAILY, placement), type = Daily())
294278
}
295279

296280
private fun formatPixelWithPlacement(
297281
pixelName: SavedSitesPixelName,
298-
placement: Placement,
282+
placement: FavoritesPlacement,
299283
): String {
300284
return pixelName.pixelName + "_" + placement.name.lowercase()
301285
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
package com.duckduckgo.savedsites.impl.views
18+
19+
import android.content.Context
20+
import android.view.View
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.duckduckgo.savedsites.api.views.FavoritesGridConfig
23+
import com.duckduckgo.savedsites.api.views.SavedSitesViewsProvider
24+
import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionView
25+
import com.squareup.anvil.annotations.ContributesBinding
26+
import javax.inject.Inject
27+
28+
@ContributesBinding(scope = AppScope::class)
29+
class SavedSitesViewsProviderImpl @Inject constructor() : SavedSitesViewsProvider {
30+
override fun getFavoritesGridView(context: Context, config: FavoritesGridConfig?): View {
31+
return FavouritesNewTabSectionView(context, config)
32+
}
33+
}

saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_section.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@
2020
android:layout_height="wrap_content"
2121
android:orientation="vertical">
2222

23+
<ImageView
24+
android:id="@+id/ddgLogo"
25+
android:layout_height="wrap_content"
26+
android:layout_width="@dimen/ntpDaxLogoIconWidth"
27+
android:layout_marginTop="@dimen/homeTabDdgLogoTopMargin"
28+
android:adjustViewBounds="true"
29+
android:maxWidth="180dp"
30+
android:maxHeight="180dp"
31+
app:srcCompat="@drawable/logo_full"
32+
android:visibility="gone"
33+
android:layout_gravity="center_horizontal" />
34+
2335
<LinearLayout
2436
android:id="@+id/sectionHeaderLayout"
2537
android:layout_width="match_parent"

0 commit comments

Comments
 (0)