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 850d66dd613a..e158fdc88e39 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -148,7 +148,6 @@ import com.duckduckgo.app.browser.menu.BrowserPopupMenu import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget -import com.duckduckgo.app.browser.newtab.FocusedViewProvider import com.duckduckgo.app.browser.newtab.NewTabPageProvider import com.duckduckgo.app.browser.omnibar.OmnibarScrolling import com.duckduckgo.app.browser.omnibar.animations.BrowserTrackersAnimatorHelper @@ -534,9 +533,6 @@ class BrowserTabFragment : @Inject lateinit var newTabPageProvider: NewTabPageProvider - @Inject - lateinit var focusedViewProvider: FocusedViewProvider - @Inject lateinit var singlePrintSafeguardFeature: SinglePrintSafeguardFeature @@ -3752,26 +3748,11 @@ class BrowserTabFragment : } private fun showFocusedView() { - binding.focusedViewContainerLayout.show() - configureFocusedView() - } - - private fun configureFocusedView() { - if (binding.focusedViewContainerLayout.childCount == 0) { - focusedViewProvider.provideFocusedViewVersion().onEach { focusedView -> - binding.focusedViewContainerLayout.addView( - focusedView.getView(requireContext()), - LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - ), - ) - }.launchIn(lifecycleScope) - } + binding.focusedView.show() } private fun hideFocusedView() { - binding.focusedViewContainerLayout.gone() + binding.focusedView.gone() } fun renderOmnibar(viewState: OmnibarViewState) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyView.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyView.kt deleted file mode 100644 index fab578c429ed..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyView.kt +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.browser.newtab - -import android.annotation.SuppressLint -import android.content.Context -import android.text.Spanned -import android.util.AttributeSet -import android.widget.LinearLayout -import androidx.core.text.toSpannable -import androidx.fragment.app.findFragment -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.findViewTreeLifecycleOwner -import androidx.lifecycle.findViewTreeViewModelStoreOwner -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import com.duckduckgo.anvil.annotations.InjectWith -import com.duckduckgo.app.browser.BrowserTabFragment.Companion.ADD_SAVED_SITE_FRAGMENT_TAG -import com.duckduckgo.app.browser.R -import com.duckduckgo.app.browser.databinding.ViewFocusedViewLegacyBinding -import com.duckduckgo.app.browser.favicon.FaviconManager -import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.Companion.QUICK_ACCESS_GRID_MAX_COLUMNS -import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.Companion.QUICK_ACCESS_ITEM_MAX_SIZE_DP -import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.QuickAccessFavorite -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.DeleteFavoriteConfirmation -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.DeleteSavedSiteConfirmation -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.ShowEditSavedSiteDialog -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.ViewState -import com.duckduckgo.app.browser.newtab.QuickAccessDragTouchItemListener.DragDropListener -import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState -import com.duckduckgo.app.pixels.AppPixelName -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.tabs.BrowserNav -import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator -import com.duckduckgo.common.ui.DuckDuckGoFragment -import com.duckduckgo.common.ui.recyclerviewext.disableAnimation -import com.duckduckgo.common.ui.recyclerviewext.enableAnimation -import com.duckduckgo.common.ui.view.gone -import com.duckduckgo.common.ui.view.makeSnackbarWithNoBottomInset -import com.duckduckgo.common.ui.view.show -import com.duckduckgo.common.ui.viewbinding.viewBinding -import com.duckduckgo.common.utils.ViewViewModelFactory -import com.duckduckgo.common.utils.extensions.html -import com.duckduckgo.di.scopes.ViewScope -import com.duckduckgo.savedsites.api.models.SavedSite -import com.duckduckgo.savedsites.api.models.SavedSitesNames -import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar -import dagger.android.support.AndroidSupportInjection -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -@InjectWith(ViewScope::class) -class FocusedLegacyView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0, -) : LinearLayout(context, attrs, defStyle) { - - @Inject - lateinit var viewModelFactory: ViewViewModelFactory - - @Inject - lateinit var browserNav: BrowserNav - - @Inject - lateinit var gridViewColumnCalculator: GridViewColumnCalculator - - @Inject - lateinit var faviconManager: FaviconManager - - @Inject - lateinit var pixel: Pixel - - private var coroutineScope: CoroutineScope? = null - - private val binding: ViewFocusedViewLegacyBinding by viewBinding() - - private lateinit var quickAccessAdapter: FavoritesQuickAccessAdapter - private lateinit var quickAccessItemTouchHelper: ItemTouchHelper - - private val viewModel: FocusedLegacyViewModel by lazy { - ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[FocusedLegacyViewModel::class.java] - } - - override fun onAttachedToWindow() { - AndroidSupportInjection.inject(this) - super.onAttachedToWindow() - - findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel) - - @SuppressLint("NoHardcodedCoroutineDispatcher") - coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - - viewModel.viewState - .onEach { render(it) } - .launchIn(coroutineScope!!) - - viewModel.commands() - .onEach { processCommands(it) } - .launchIn(coroutineScope!!) - - configureViews() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - - findViewTreeLifecycleOwner()?.lifecycle?.removeObserver(viewModel) - coroutineScope?.cancel() - coroutineScope = null - } - - private fun configureViews() { - configureHomeTabQuickAccessGrid() - setOnClickListener(null) - } - - private fun configureHomeTabQuickAccessGrid() { - configureQuickAccessGridLayout(binding.quickAccessSuggestionsRecyclerView) - quickAccessAdapter = createQuickAccessAdapter(originPixel = AppPixelName.FAVORITE_OMNIBAR_ITEM_PRESSED) { viewHolder -> - binding.quickAccessSuggestionsRecyclerView.enableAnimation() - quickAccessItemTouchHelper.startDrag(viewHolder) - } - quickAccessItemTouchHelper = createQuickAccessItemHolder(binding.quickAccessSuggestionsRecyclerView, quickAccessAdapter) - binding.quickAccessSuggestionsRecyclerView.adapter = quickAccessAdapter - binding.quickAccessSuggestionsRecyclerView.disableAnimation() - } - - private fun createQuickAccessAdapter( - originPixel: AppPixelName, - onMoveListener: (RecyclerView.ViewHolder) -> Unit, - ): FavoritesQuickAccessAdapter { - return FavoritesQuickAccessAdapter( - findViewTreeLifecycleOwner()!!, - faviconManager, - onMoveListener, - { - pixel.fire(originPixel) - submitUrl(it.favorite.url) - }, - { viewModel.onEditSavedSiteRequested(it.favorite) }, - { viewModel.onDeleteFavoriteRequested(it.favorite) }, - { viewModel.onDeleteSavedSiteRequested(it.favorite) }, - ) - } - - private fun submitUrl(url: String) { - context.startActivity(browserNav.openInCurrentTab(context, url)) - } - - private fun createQuickAccessItemHolder( - recyclerView: RecyclerView, - apapter: FavoritesQuickAccessAdapter, - ): ItemTouchHelper { - return ItemTouchHelper( - QuickAccessDragTouchItemListener( - apapter, - object : DragDropListener { - override fun onListChanged(listElements: List) { - viewModel.onQuickAccessListChanged(listElements) - recyclerView.disableAnimation() - } - }, - ), - ).also { - it.attachToRecyclerView(recyclerView) - } - } - - private fun configureQuickAccessGridLayout(recyclerView: RecyclerView) { - val numOfColumns = gridViewColumnCalculator.calculateNumberOfColumns(QUICK_ACCESS_ITEM_MAX_SIZE_DP, QUICK_ACCESS_GRID_MAX_COLUMNS) - val layoutManager = GridLayoutManager(context, numOfColumns) - recyclerView.layoutManager = layoutManager - val sidePadding = gridViewColumnCalculator.calculateSidePadding(QUICK_ACCESS_ITEM_MAX_SIZE_DP, numOfColumns) - recyclerView.setPadding(sidePadding, recyclerView.paddingTop, sidePadding, recyclerView.paddingBottom) - } - - private fun render(viewState: ViewState) { - if (viewState.favourites.isEmpty()) { - binding.quickAccessSuggestionsRecyclerView.gone() - } else { - binding.quickAccessSuggestionsRecyclerView.show() - quickAccessAdapter.submitList(viewState.favourites.map { QuickAccessFavorite(it) }) - } - } - - private fun processCommands(command: Command) { - when (command) { - is ShowEditSavedSiteDialog -> editSavedSite(command.savedSiteChangedViewState) - is DeleteFavoriteConfirmation -> confirmDeleteSavedSite( - command.savedSite, - context.getString(R.string.favoriteDeleteConfirmationMessage).toSpannable(), - ) { - viewModel.onDeleteFavoriteSnackbarDismissed(command.savedSite) - } - is DeleteSavedSiteConfirmation -> confirmDeleteSavedSite( - command.savedSite, - context.getString(com.duckduckgo.saved.sites.impl.R.string.bookmarkDeleteConfirmationMessage, command.savedSite.title).html(context), - ) { - viewModel.onDeleteSavedSiteSnackbarDismissed(command.savedSite) - } - } - } - - private fun editSavedSite(savedSiteChangedViewState: SavedSiteChangedViewState) { - val addBookmarkDialog = EditSavedSiteDialogFragment.instance( - savedSiteChangedViewState.savedSite, - savedSiteChangedViewState.bookmarkFolder?.id ?: SavedSitesNames.BOOKMARKS_ROOT, - savedSiteChangedViewState.bookmarkFolder?.name, - ) - val btf = findFragment() - addBookmarkDialog.show(btf.childFragmentManager, ADD_SAVED_SITE_FRAGMENT_TAG) - addBookmarkDialog.listener = viewModel - addBookmarkDialog.deleteBookmarkListener = viewModel - } - - private fun confirmDeleteSavedSite( - savedSite: SavedSite, - message: Spanned, - onDeleteSnackbarDismissed: (SavedSite) -> Unit, - ) { - binding.root.makeSnackbarWithNoBottomInset( - message, - Snackbar.LENGTH_LONG, - ).setAction(R.string.fireproofWebsiteSnackbarAction) { - viewModel.undoDelete(savedSite) - } - .addCallback( - object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed( - transientBottomBar: Snackbar?, - event: Int, - ) { - if (event != DISMISS_EVENT_ACTION) { - onDeleteSnackbarDismissed(savedSite) - } - } - }, - ) - .show() - } -} diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyViewModel.kt deleted file mode 100644 index 0c8fe15be07b..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyViewModel.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.browser.newtab - -import android.annotation.SuppressLint -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.duckduckgo.anvil.annotations.ContributesViewModel -import com.duckduckgo.app.browser.BrowserTabViewModel.HiddenBookmarksIds -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.DeleteFavoriteConfirmation -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.DeleteSavedSiteConfirmation -import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.ShowEditSavedSiteDialog -import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.ViewScope -import com.duckduckgo.savedsites.api.SavedSitesRepository -import com.duckduckgo.savedsites.api.models.BookmarkFolder -import com.duckduckgo.savedsites.api.models.SavedSite -import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark -import com.duckduckgo.savedsites.api.models.SavedSite.Favorite -import com.duckduckgo.savedsites.impl.SavedSitesPixelName -import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.DeleteBookmarkListener -import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.EditSavedSiteListener -import javax.inject.Inject -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle -@ContributesViewModel(ViewScope::class) -class FocusedLegacyViewModel @Inject constructor( - private val dispatchers: DispatcherProvider, - private val savedSitesRepository: SavedSitesRepository, - private val pixel: Pixel, -) : ViewModel(), DefaultLifecycleObserver, EditSavedSiteListener, DeleteBookmarkListener { - - data class ViewState( - val favourites: List = emptyList(), - ) - - sealed class Command { - class ShowEditSavedSiteDialog(val savedSiteChangedViewState: SavedSiteChangedViewState) : Command() - class DeleteFavoriteConfirmation(val savedSite: SavedSite) : Command() - class DeleteSavedSiteConfirmation(val savedSite: SavedSite) : Command() - } - - val hiddenIds = MutableStateFlow(HiddenBookmarksIds()) - - private val _viewState = MutableStateFlow(ViewState()) - val viewState = _viewState.asStateFlow() - private val command = Channel(1, BufferOverflow.DROP_OLDEST) - internal fun commands(): Flow = command.receiveAsFlow() - - override fun onStart(owner: LifecycleOwner) { - super.onStart(owner) - - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.getFavorites() - .combine(hiddenIds) { favorites, hiddenIds -> - favorites.filter { it.id !in hiddenIds.favorites } - } - .flowOn(dispatchers.io()) - .onEach { favourites -> - withContext(dispatchers.main()) { - _viewState.emit( - viewState.value.copy( - favourites = favourites, - ), - ) - } - } - .flowOn(dispatchers.main()) - .launchIn(viewModelScope) - } - } - - fun onEditSavedSiteRequested(savedSite: SavedSite) { - viewModelScope.launch(dispatchers.io()) { - val bookmarkFolder = - if (savedSite is SavedSite.Bookmark) { - getBookmarkFolder(savedSite) - } else { - null - } - - withContext(dispatchers.main()) { - command.send( - ShowEditSavedSiteDialog( - SavedSiteChangedViewState( - savedSite, - bookmarkFolder, - ), - ), - ) - } - } - } - - fun onDeleteFavoriteRequested(savedSite: SavedSite) { - hide(savedSite, DeleteFavoriteConfirmation(savedSite)) - } - - private fun hide( - savedSite: SavedSite, - deleteCommand: Command, - ) { - viewModelScope.launch(dispatchers.io()) { - when (savedSite) { - is Bookmark -> { - hiddenIds.emit( - hiddenIds.value.copy( - bookmarks = hiddenIds.value.bookmarks + savedSite.id, - favorites = hiddenIds.value.favorites + savedSite.id, - ), - ) - } - - is Favorite -> { - hiddenIds.emit(hiddenIds.value.copy(favorites = hiddenIds.value.favorites + savedSite.id)) - } - } - withContext(dispatchers.main()) { - command.send(deleteCommand) - } - } - } - - fun onDeleteFavoriteSnackbarDismissed(savedSite: SavedSite) { - delete(savedSite) - } - - fun onDeleteSavedSiteSnackbarDismissed(savedSite: SavedSite) { - delete(savedSite, true) - } - - private fun delete( - savedSite: SavedSite, - deleteBookmark: Boolean = false, - ) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.delete(savedSite, deleteBookmark) - } - } - - fun undoDelete(savedSite: SavedSite) { - viewModelScope.launch(dispatchers.io()) { - hiddenIds.emit( - hiddenIds.value.copy( - favorites = hiddenIds.value.favorites - savedSite.id, - bookmarks = hiddenIds.value.bookmarks - savedSite.id, - ), - ) - } - } - - fun onQuickAccessListChanged(newList: List) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.updateWithPosition(newList.map { it.favorite }) - } - } - - private suspend fun getBookmarkFolder(bookmark: SavedSite.Bookmark?): BookmarkFolder? { - if (bookmark == null) return null - return withContext(dispatchers.io()) { - savedSitesRepository.getFolder(bookmark.parentId) - } - } - - override fun onFavouriteEdited(favorite: Favorite) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.updateFavourite(favorite) - } - } - - override fun onBookmarkEdited( - bookmark: Bookmark, - oldFolderId: String, - updateFavorite: Boolean, - ) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.updateBookmark(bookmark, oldFolderId, updateFavorite) - } - } - - override fun onFavoriteAdded() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) - } - - override fun onFavoriteRemoved() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED) - } - - override fun onSavedSiteDeleted(savedSite: SavedSite) { - onDeleteSavedSiteRequested(savedSite) - } - - override fun onSavedSiteDeleteCancelled() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_DELETE_BOOKMARK_CANCELLED) - } - - override fun onSavedSiteDeleteRequested() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_DELETE_BOOKMARK_CLICKED) - } - - fun onDeleteSavedSiteRequested(savedSite: SavedSite) { - hide(savedSite, DeleteSavedSiteConfirmation(savedSite)) - } -} diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt deleted file mode 100644 index 841943cdda8f..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.browser.newtab - -import android.content.Context -import android.view.View -import com.duckduckgo.anvil.annotations.ContributesActivePlugin -import com.duckduckgo.common.utils.plugins.ActivePluginPoint -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.newtabpage.api.FocusedViewPlugin -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -interface FocusedViewProvider { - - fun provideFocusedViewVersion(): Flow -} - -@ContributesBinding( - scope = AppScope::class, -) -class RealFocusedViewProvider @Inject constructor( - private val focusedViewVersions: ActivePluginPoint, -) : FocusedViewProvider { - override fun provideFocusedViewVersion(): Flow = flow { - val focusedView = focusedViewVersions.getPlugins().firstOrNull() ?: FocusedLegacyPage() - emit(focusedView) - } -} - -@ContributesActivePlugin( - scope = AppScope::class, - boundType = FocusedViewPlugin::class, - priority = FocusedViewPlugin.PRIORITY_LEGACY_FOCUSED_PAGE, - supportExperiments = true, -) -class FocusedLegacyPage @Inject constructor() : FocusedViewPlugin { - - override fun getView(context: Context): View { - return FocusedLegacyView(context) - } -} - -@ContributesActivePlugin( - scope = AppScope::class, - boundType = FocusedViewPlugin::class, - priority = FocusedViewPlugin.PRIORITY_NEW_FOCUSED_PAGE, - defaultActiveValue = false, - supportExperiments = true, - internalAlwaysEnabled = true, -) -class FocusedPage @Inject constructor() : FocusedViewPlugin { - - override fun getView(context: Context): View { - return FocusedView(context) - } -} diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt index 2cf713dfa6e1..377570cd3cef 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt @@ -21,69 +21,41 @@ import android.app.PendingIntent import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent -import android.content.res.Configuration -import android.text.Spanned import android.util.AttributeSet import android.view.View import android.widget.LinearLayout -import androidx.core.text.toSpannable import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.fragment.app.findFragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.anvil.annotations.InjectWith -import com.duckduckgo.app.browser.BrowserTabFragment.Companion.ADD_SAVED_SITE_FRAGMENT_TAG import com.duckduckgo.app.browser.HomeBackgroundLogo -import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.ViewNewTabLegacyBinding import com.duckduckgo.app.browser.favicon.FaviconManager -import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.Companion.QUICK_ACCESS_GRID_MAX_COLUMNS -import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.Companion.QUICK_ACCESS_ITEM_MAX_SIZE_DP -import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.QuickAccessFavorite import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command -import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.DeleteFavoriteConfirmation -import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.DeleteSavedSiteConfirmation import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.DismissMessage import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.LaunchAppTPOnboarding import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.LaunchDefaultBrowser import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.LaunchPlayStore import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.LaunchScreen import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.SharePromoLinkRMF -import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.ShowEditSavedSiteDialog import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.SubmitUrl import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.ViewState import com.duckduckgo.app.browser.remotemessage.SharePromoLinkRMFBroadCastReceiver import com.duckduckgo.app.browser.remotemessage.asMessage -import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.global.view.launchDefaultAppActivity -import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.tabs.BrowserNav -import com.duckduckgo.common.ui.DuckDuckGoFragment -import com.duckduckgo.common.ui.recyclerviewext.GridColumnCalculator -import com.duckduckgo.common.ui.recyclerviewext.disableAnimation -import com.duckduckgo.common.ui.recyclerviewext.enableAnimation import com.duckduckgo.common.ui.view.gone -import com.duckduckgo.common.ui.view.makeSnackbarWithNoBottomInset import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.common.utils.ViewViewModelFactory -import com.duckduckgo.common.utils.extensions.html import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.navigation.api.GlobalActivityStarter.DeeplinkActivityParams import com.duckduckgo.remote.messaging.api.RemoteMessage -import com.duckduckgo.savedsites.api.models.SavedSite -import com.duckduckgo.savedsites.api.models.SavedSitesNames -import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar import dagger.android.support.AndroidSupportInjection import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -120,21 +92,12 @@ class NewTabLegacyPageView @JvmOverloads constructor( private val binding: ViewNewTabLegacyBinding by viewBinding() - private lateinit var quickAccessAdapter: FavoritesQuickAccessAdapter - private lateinit var quickAccessItemTouchHelper: ItemTouchHelper - private val homeBackgroundLogo by lazy { HomeBackgroundLogo(binding.ddgLogo) } private val viewModel: NewTabLegacyPageViewModel by lazy { ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[NewTabLegacyPageViewModel::class.java] } - // BrowserTabFragment overrides onConfigurationChange, so we have to do this too - override fun onConfigurationChanged(newConfig: Configuration?) { - super.onConfigurationChanged(newConfig) - configureQuickAccessGridLayout(binding.quickAccessRecyclerView) - } - override fun onAttachedToWindow() { AndroidSupportInjection.inject(this) super.onAttachedToWindow() @@ -151,8 +114,6 @@ class NewTabLegacyPageView @JvmOverloads constructor( viewModel.commands() .onEach { processCommands(it) } .launchIn(coroutineScope!!) - - configureViews() } override fun onDetachedFromWindow() { @@ -163,69 +124,8 @@ class NewTabLegacyPageView @JvmOverloads constructor( coroutineScope = null } - private fun configureViews() { - configureHomeTabQuickAccessGrid() - setOnClickListener(null) - } - - private fun configureHomeTabQuickAccessGrid() { - configureQuickAccessGridLayout(binding.quickAccessRecyclerView) - quickAccessAdapter = createQuickAccessAdapter(originPixel = AppPixelName.FAVORITE_HOMETAB_ITEM_PRESSED) { viewHolder -> - binding.quickAccessRecyclerView.enableAnimation() - quickAccessItemTouchHelper.startDrag(viewHolder) - } - quickAccessItemTouchHelper = createQuickAccessItemHolder(binding.quickAccessRecyclerView, quickAccessAdapter) - binding.quickAccessRecyclerView.adapter = quickAccessAdapter - binding.quickAccessRecyclerView.disableAnimation() - } - - private fun createQuickAccessAdapter( - originPixel: AppPixelName, - onMoveListener: (RecyclerView.ViewHolder) -> Unit, - ): FavoritesQuickAccessAdapter { - return FavoritesQuickAccessAdapter( - findViewTreeLifecycleOwner()!!, - faviconManager, - onMoveListener, - { - pixel.fire(originPixel) - submitUrl(it.favorite.url) - }, - { viewModel.onEditSavedSiteRequested(it.favorite) }, - { viewModel.onDeleteFavoriteRequested(it.favorite) }, - { viewModel.onDeleteSavedSiteRequested(it.favorite) }, - ) - } - - private fun createQuickAccessItemHolder( - recyclerView: RecyclerView, - apapter: FavoritesQuickAccessAdapter, - ): ItemTouchHelper { - return ItemTouchHelper( - QuickAccessDragTouchItemListener( - apapter, - object : QuickAccessDragTouchItemListener.DragDropListener { - override fun onListChanged(listElements: List) { - viewModel.onQuickAccessListChanged(listElements) - recyclerView.disableAnimation() - } - }, - ), - ).also { - it.attachToRecyclerView(recyclerView) - } - } - - private fun configureQuickAccessGridLayout(recyclerView: RecyclerView) { - val gridColumnCalculator = GridColumnCalculator(context) - val numOfColumns = gridColumnCalculator.calculateNumberOfColumns(QUICK_ACCESS_ITEM_MAX_SIZE_DP, QUICK_ACCESS_GRID_MAX_COLUMNS) - val layoutManager = GridLayoutManager(context, numOfColumns) - recyclerView.layoutManager = layoutManager - val sidePadding = gridColumnCalculator.calculateSidePadding(QUICK_ACCESS_ITEM_MAX_SIZE_DP, numOfColumns) - recyclerView.setPadding(sidePadding, recyclerView.paddingTop, sidePadding, recyclerView.paddingBottom) - } - private fun render(viewState: ViewState) { + Timber.d("New Tab: render $viewState") if (viewState.message == null && viewState.favourites.isEmpty()) { homeBackgroundLogo.showLogo() } else { @@ -236,11 +136,11 @@ class NewTabLegacyPageView @JvmOverloads constructor( } else { binding.messageCta.gone() } + if (viewState.favourites.isEmpty()) { - binding.quickAccessRecyclerView.gone() + binding.focusedFavourites.gone() } else { - binding.quickAccessRecyclerView.show() - quickAccessAdapter.submitList(viewState.favourites.map { QuickAccessFavorite(it) }) + binding.focusedFavourites.show() } } @@ -253,19 +153,6 @@ class NewTabLegacyPageView @JvmOverloads constructor( is LaunchScreen -> launchScreen(command.screen, command.payload) is SharePromoLinkRMF -> launchSharePromoRMFPageChooser(command.url, command.shareTitle) is SubmitUrl -> submitUrl(command.url) - is ShowEditSavedSiteDialog -> editSavedSite(command.savedSiteChangedViewState) - is DeleteFavoriteConfirmation -> confirmDeleteSavedSite( - command.savedSite, - context.getString(R.string.favoriteDeleteConfirmationMessage).toSpannable(), - ) { - viewModel.onDeleteFavoriteSnackbarDismissed(command.savedSite) - } - is DeleteSavedSiteConfirmation -> confirmDeleteSavedSite( - command.savedSite, - context.getString(com.duckduckgo.saved.sites.impl.R.string.bookmarkDeleteConfirmationMessage, command.savedSite.title).html(context), - ) { - viewModel.onDeleteSavedSiteSnackbarDismissed(it) - } } } @@ -277,7 +164,10 @@ class NewTabLegacyPageView @JvmOverloads constructor( globalActivityStarter.start(context, AppTrackerOnboardingActivityWithEmptyParamsParams) } - private fun launchSharePromoRMFPageChooser(url: String, shareTitle: String) { + private fun launchSharePromoRMFPageChooser( + url: String, + shareTitle: String, + ) { val share = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, url) @@ -311,44 +201,6 @@ class NewTabLegacyPageView @JvmOverloads constructor( context.startActivity(browserNav.openInCurrentTab(context, url)) } - private fun editSavedSite(savedSiteChangedViewState: SavedSiteChangedViewState) { - val addBookmarkDialog = EditSavedSiteDialogFragment.instance( - savedSiteChangedViewState.savedSite, - savedSiteChangedViewState.bookmarkFolder?.id ?: SavedSitesNames.BOOKMARKS_ROOT, - savedSiteChangedViewState.bookmarkFolder?.name, - ) - val btf = findFragment() - addBookmarkDialog.show(btf.childFragmentManager, ADD_SAVED_SITE_FRAGMENT_TAG) - addBookmarkDialog.listener = viewModel - addBookmarkDialog.deleteBookmarkListener = viewModel - } - - private fun confirmDeleteSavedSite( - savedSite: SavedSite, - message: Spanned, - onDeleteSnackbarDismissed: (SavedSite) -> Unit, - ) { - binding.root.makeSnackbarWithNoBottomInset( - message, - Snackbar.LENGTH_LONG, - ).setAction(R.string.fireproofWebsiteSnackbarAction) { - viewModel.undoDelete(savedSite) - } - .addCallback( - object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed( - transientBottomBar: Snackbar?, - event: Int, - ) { - if (event != DISMISS_EVENT_ACTION) { - onDeleteSnackbarDismissed(savedSite) - } - } - }, - ) - .show() - } - private fun showRemoteMessage( message: RemoteMessage, newMessage: Boolean, diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt index 79be3223253a..b79edd58046f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt @@ -22,30 +22,16 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel -import com.duckduckgo.app.browser.BrowserTabViewModel.HiddenBookmarksIds -import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.QuickAccessFavorite -import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.DeleteFavoriteConfirmation -import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.DeleteSavedSiteConfirmation -import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.ShowEditSavedSiteDialog import com.duckduckgo.app.browser.remotemessage.CommandActionMapper -import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.playstore.PlayStoreUtils import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.api.RemoteMessageModel import com.duckduckgo.savedsites.api.SavedSitesRepository -import com.duckduckgo.savedsites.api.models.BookmarkFolder -import com.duckduckgo.savedsites.api.models.SavedSite -import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSite.Favorite -import com.duckduckgo.savedsites.impl.SavedSitesPixelName -import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.DeleteBookmarkListener -import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.EditSavedSiteListener import com.duckduckgo.sync.api.engine.SyncEngine import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.FEATURE_READ import javax.inject.Inject @@ -72,8 +58,7 @@ class NewTabLegacyPageViewModel @Inject constructor( private val syncEngine: SyncEngine, private val commandActionMapper: CommandActionMapper, private val dismissedCtaDao: DismissedCtaDao, - private val pixel: Pixel, -) : ViewModel(), DefaultLifecycleObserver, EditSavedSiteListener, DeleteBookmarkListener { +) : ViewModel(), DefaultLifecycleObserver { data class ViewState( val message: RemoteMessage? = null, @@ -102,17 +87,9 @@ class NewTabLegacyPageViewModel @Inject constructor( val screen: String, val payload: String, ) : Command() - - class ShowEditSavedSiteDialog(val savedSiteChangedViewState: SavedSiteChangedViewState) : Command() - class DeleteFavoriteConfirmation(val savedSite: SavedSite) : Command() - class DeleteSavedSiteConfirmation(val savedSite: SavedSite) : Command() } private var lastRemoteMessageSeen: RemoteMessage? = null - val hiddenIds = MutableStateFlow(HiddenBookmarksIds()) - - private val _hiddenIds = MutableStateFlow(HiddenBookmarksIds()) - private val _viewState = MutableStateFlow(ViewState()) val viewState = _viewState.asStateFlow() @@ -124,14 +101,11 @@ class NewTabLegacyPageViewModel @Inject constructor( viewModelScope.launch(dispatchers.io()) { savedSitesRepository.getFavorites() - .combine(hiddenIds) { favorites, hiddenIds -> + .combine(remoteMessagingModel.getActiveMessages()) { favorites, activeMessage -> if (favorites.isNotEmpty()) { syncEngine.triggerSync(FEATURE_READ) } - favorites.filter { it.id !in hiddenIds.favorites } - } - .combine(remoteMessagingModel.getActiveMessages()) { filteredFavourites, activeMessage -> - ViewStateSnapshot(filteredFavourites, activeMessage) + ViewStateSnapshot(favorites, activeMessage) } .flowOn(dispatchers.io()) .onEach { snapshot -> @@ -206,137 +180,4 @@ class NewTabLegacyPageViewModel @Inject constructor( fun openPlayStore(appPackage: String) { playStoreUtils.launchPlayStore(appPackage) } - - fun onEditSavedSiteRequested(savedSite: SavedSite) { - viewModelScope.launch(dispatchers.io()) { - val bookmarkFolder = - if (savedSite is SavedSite.Bookmark) { - getBookmarkFolder(savedSite) - } else { - null - } - - withContext(dispatchers.main()) { - command.send( - ShowEditSavedSiteDialog( - SavedSiteChangedViewState( - savedSite, - bookmarkFolder, - ), - ), - ) - } - } - } - - fun onDeleteFavoriteRequested(savedSite: SavedSite) { - hide(savedSite, DeleteFavoriteConfirmation(savedSite)) - } - - private fun hide( - savedSite: SavedSite, - deleteCommand: Command, - ) { - viewModelScope.launch(dispatchers.io()) { - when (savedSite) { - is Bookmark -> { - _hiddenIds.emit( - hiddenIds.value.copy( - bookmarks = hiddenIds.value.bookmarks + savedSite.id, - favorites = hiddenIds.value.favorites + savedSite.id, - ), - ) - } - - is Favorite -> { - _hiddenIds.emit(hiddenIds.value.copy(favorites = hiddenIds.value.favorites + savedSite.id)) - } - } - withContext(dispatchers.main()) { - command.send(deleteCommand) - } - } - } - - fun onDeleteFavoriteSnackbarDismissed(savedSite: SavedSite) { - delete(savedSite) - } - - fun onDeleteSavedSiteSnackbarDismissed(savedSite: SavedSite) { - delete(savedSite, true) - } - - private fun delete( - savedSite: SavedSite, - deleteBookmark: Boolean = false, - ) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.delete(savedSite, deleteBookmark) - } - } - - fun undoDelete(savedSite: SavedSite) { - viewModelScope.launch(dispatchers.io()) { - _hiddenIds.emit( - hiddenIds.value.copy( - favorites = hiddenIds.value.favorites - savedSite.id, - bookmarks = hiddenIds.value.bookmarks - savedSite.id, - ), - ) - } - } - - fun onQuickAccessListChanged(newList: List) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.updateWithPosition(newList.map { it.favorite }) - } - } - - private suspend fun getBookmarkFolder(bookmark: SavedSite.Bookmark?): BookmarkFolder? { - if (bookmark == null) return null - return withContext(dispatchers.io()) { - savedSitesRepository.getFolder(bookmark.parentId) - } - } - - override fun onFavouriteEdited(favorite: Favorite) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.updateFavourite(favorite) - } - } - - override fun onBookmarkEdited( - bookmark: Bookmark, - oldFolderId: String, - updateFavorite: Boolean, - ) { - viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.updateBookmark(bookmark, oldFolderId, updateFavorite) - } - } - - override fun onFavoriteAdded() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) - } - - override fun onFavoriteRemoved() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED) - } - - override fun onSavedSiteDeleted(savedSite: SavedSite) { - onDeleteSavedSiteRequested(savedSite) - } - - override fun onSavedSiteDeleteCancelled() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_DELETE_BOOKMARK_CANCELLED) - } - - override fun onSavedSiteDeleteRequested() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_DELETE_BOOKMARK_CLICKED) - } - - fun onDeleteSavedSiteRequested(savedSite: SavedSite) { - hide(savedSite, DeleteSavedSiteConfirmation(savedSite)) - } } diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index a7f152f57415..04ccda0ea190 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -47,9 +47,8 @@ tools:listitem="@layout/item_autocomplete_search_suggestion" tools:visibility="visible" /> - diff --git a/app/src/main/res/layout/view_focused_view_legacy.xml b/app/src/main/res/layout/view_focused_view_legacy.xml deleted file mode 100644 index 0fe952daaf33..000000000000 --- a/app/src/main/res/layout/view_focused_view_legacy.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/view_new_tab_legacy.xml b/app/src/main/res/layout/view_new_tab_legacy.xml index 5f5123b69ca7..daf3801ae58c 100644 --- a/app/src/main/res/layout/view_new_tab_legacy.xml +++ b/app/src/main/res/layout/view_new_tab_legacy.xml @@ -65,18 +65,11 @@ app:layout_constraintWidth_max="600dp" /> - + app:isExpandable="false" + app:showPlaceholders="false" /> \ No newline at end of file diff --git a/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt index 8649adeb9917..6046a438e8a3 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt @@ -22,7 +22,6 @@ import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command import com.duckduckgo.app.browser.remotemessage.CommandActionMapper import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId.DAX_END -import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.playstore.PlayStoreUtils import com.duckduckgo.remote.messaging.api.Action @@ -30,11 +29,7 @@ import com.duckduckgo.remote.messaging.api.Content import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.api.RemoteMessageModel import com.duckduckgo.savedsites.api.SavedSitesRepository -import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark -import com.duckduckgo.savedsites.api.models.SavedSite.Favorite -import com.duckduckgo.savedsites.impl.SavedSitesPixelName import com.duckduckgo.sync.api.engine.SyncEngine -import java.util.UUID import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -59,7 +54,6 @@ class NewTabLegacyPageViewModelTest { private var mockPlaystoreUtils: PlayStoreUtils = mock() private var mockRemoteMessageModel: RemoteMessageModel = mock() private var mockDismissedCtaDao: DismissedCtaDao = mock() - private var mockPixel: Pixel = mock() private lateinit var testee: NewTabLegacyPageViewModel @@ -76,7 +70,6 @@ class NewTabLegacyPageViewModelTest { syncEngine = mockSyncEngine, commandActionMapper = mockCommandActionMapper, dismissedCtaDao = mockDismissedCtaDao, - pixel = mockPixel, ) } @@ -134,22 +127,6 @@ class NewTabLegacyPageViewModelTest { } } - @Test - fun whenFavoriteEditedThenRepositoryUpdated() = runTest { - val favorite = Favorite(UUID.randomUUID().toString(), "A title", "www.example.com", lastModified = "timestamp", 1) - testee.onFavouriteEdited(favorite) - verify(mockSavedSitesRepository).updateFavourite(favorite) - } - - @Test - fun whenBookmarkEditedThenRepositoryIsUpdated() = runTest { - val folderId = "folder1" - val bookmark = - Bookmark(id = UUID.randomUUID().toString(), title = "A title", url = "www.example.com", parentId = folderId, lastModified = "timestamp") - testee.onBookmarkEdited(bookmark, folderId, false) - verify(mockSavedSitesRepository).updateBookmark(bookmark, folderId) - } - @Test fun whenRemoteMessageShownThenFirePixelAndMarkAsShown() = runTest { val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList()) @@ -233,32 +210,4 @@ class NewTabLegacyPageViewModelTest { } } } - - @Test - fun whenOnFavoriteAddedThenPixelFired() { - testee.onFavoriteAdded() - - verify(mockPixel).fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) - } - - @Test - fun whenOnFavoriteRemovedThenPixelFired() { - testee.onFavoriteRemoved() - - verify(mockPixel).fire(SavedSitesPixelName.EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED) - } - - @Test - fun whenOnSavedSiteDeleteCancelledThenPixelFired() { - testee.onSavedSiteDeleteCancelled() - - verify(mockPixel).fire(SavedSitesPixelName.EDIT_BOOKMARK_DELETE_BOOKMARK_CANCELLED) - } - - @Test - fun whenOnSavedSiteDeleteRequestedThenPixelFired() { - testee.onSavedSiteDeleteRequested() - - verify(mockPixel).fire(SavedSitesPixelName.EDIT_BOOKMARK_DELETE_BOOKMARK_CLICKED) - } } diff --git a/new-tab-page/new-tab-page-api/src/main/java/com/duckduckgo/newtabpage/api/FocusedViewPlugin.kt b/new-tab-page/new-tab-page-api/src/main/java/com/duckduckgo/newtabpage/api/FocusedViewPlugin.kt deleted file mode 100644 index e82d5b9b50ac..000000000000 --- a/new-tab-page/new-tab-page-api/src/main/java/com/duckduckgo/newtabpage/api/FocusedViewPlugin.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.newtabpage.api - -import android.content.Context -import android.view.View -import com.duckduckgo.common.utils.plugins.ActivePlugin - -/** - * This class is used to provide one of the two different version of FocusedView - * Legacy -> What existed before https://app.asana.com/0/1174433894299346/1207064372575037 - * New -> Implementation of https://app.asana.com/0/1174433894299346/1207064372575037 - */ -interface FocusedViewPlugin : ActivePlugin { - - /** - * This method returns a [View] that will be used as the Focused View content - * @return [View] - */ - fun getView(context: Context): View - - companion object { - const val PRIORITY_NEW_FOCUSED_PAGE = 0 - const val PRIORITY_LEGACY_FOCUSED_PAGE = 100 - } -} diff --git a/new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/FocusedViewPluginPointTrigger.kt b/new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/FocusedViewPluginPointTrigger.kt deleted file mode 100644 index c0a984a67953..000000000000 --- a/new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/FocusedViewPluginPointTrigger.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.newtabpage.impl - -import com.duckduckgo.anvil.annotations.ContributesActivePluginPoint -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.newtabpage.api.FocusedViewPlugin - -@ContributesActivePluginPoint( - scope = AppScope::class, - boundType = FocusedViewPlugin::class, -) -private interface FocusedViewPluginPointTrigger diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt index eaca277f40e2..6a2a5d4fbf3e 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt @@ -28,6 +28,8 @@ enum class SavedSitesPixelName(override val pixelName: String) : Pixel.PixelName BOOKMARK_LAUNCHED_DAILY("m_bookmark_launched_daily"), /** Edit Bookmark Dialog **/ + EDIT_FAVOURITE_DIALOG_SHOWN("m_edit_favourite_dialog_shown"), + EDIT_FAVOURITE_DIALOG_SHOWN_DAILY("m_edit_favourite_dialog_shown_daily"), EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED("m_edit_bookmark_add_favorite"), EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY("m_edit_bookmark_add_favorite_daily"), EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED("m_edit_bookmark_remove_favorite"), @@ -50,4 +52,6 @@ enum class SavedSitesPixelName(override val pixelName: String) : Pixel.PixelName FAVOURITE_CLICKED("m_favorite_clicked"), FAVOURITE_CLICKED_DAILY("m_favorite_clicked_daily"), MENU_ACTION_ADD_FAVORITE_PRESSED_DAILY("m_nav_af_p_daily"), + FAVOURITE_REMOVED("m_favorite_removed"), + FAVOURITE_DELETED("m_favorite_deleted"), } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt index b838eab90097..6f74e7635c35 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt @@ -105,6 +105,7 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( private var coroutineScope: CoroutineScope? = null private var isExpandable = true + private var showPlaceholders = false private val binding: ViewNewTabFavouritesSectionBinding by viewBinding() @@ -131,6 +132,7 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( R.style.Widget_DuckDuckGo_FavouritesNewTabSection, ).apply { isExpandable = getBoolean(R.styleable.FavouritesNewTabSectionView_isExpandable, true) + showPlaceholders = getBoolean(R.styleable.FavouritesNewTabSectionView_showPlaceholders, true) recycle() } } @@ -269,14 +271,18 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( if (viewState.favourites.isEmpty()) { binding.newTabFavoritesToggleLayout.gone() - binding.sectionHeaderLayout.show() - binding.sectionHeaderLayout.setOnClickListener { - showNewTabFavouritesPopup(binding.sectionHeaderOverflowIcon) - } - if (numOfColumns == QUICK_ACCESS_GRID_MAX_COLUMNS) { - adapter.submitList(FavouritesNewTabSectionsAdapter.LANDSCAPE_PLACEHOLDERS) + if (showPlaceholders) { + binding.sectionHeaderLayout.show() + binding.sectionHeaderLayout.setOnClickListener { + showNewTabFavouritesPopup(binding.sectionHeaderOverflowIcon) + } + if (numOfColumns == QUICK_ACCESS_GRID_MAX_COLUMNS) { + adapter.submitList(FavouritesNewTabSectionsAdapter.LANDSCAPE_PLACEHOLDERS) + } else { + adapter.submitList(FavouritesNewTabSectionsAdapter.PORTRAIT_PLACEHOLDERS) + } } else { - adapter.submitList(FavouritesNewTabSectionsAdapter.PORTRAIT_PLACEHOLDERS) + adapter.submitList(emptyList()) } } else { binding.sectionHeaderLayout.setOnClickListener(null) @@ -399,20 +405,14 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( viewModel.onSavedSiteDeleted(savedSite) } - override fun onSavedSiteDeleteCancelled() { - } + override fun onSavedSiteDeleteCancelled() {} - override fun onSavedSiteDeleteRequested() { - } + override fun onSavedSiteDeleteRequested() {} } } private companion object { - const val EDGE_TREATMENT_DISTANCE_FROM_EDGE = 10f const val ADD_SAVED_SITE_FRAGMENT_TAG = "ADD_SAVED_SITE" - - // Alignment of popup left edge vs. anchor left edge - const val POPUP_HORIZONTAL_OFFSET_DP = -4 } } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt index c409f8e60bdf..0c9cfc20c088 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt @@ -32,7 +32,7 @@ import com.duckduckgo.savedsites.api.models.BookmarkFolder import com.duckduckgo.savedsites.api.models.SavedSite import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSite.Favorite -import com.duckduckgo.savedsites.impl.SavedSitesPixelName +import com.duckduckgo.savedsites.impl.SavedSitesPixelName.* import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteFavoriteConfirmation import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteSavedSiteConfirmation import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.ShowEditSavedSiteDialog @@ -51,6 +51,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import logcat.logcat @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle @ContributesViewModel(ViewScope::class) @@ -77,7 +78,6 @@ class FavouritesNewTabSectionViewModel @Inject constructor( data class HiddenBookmarksIds( val favorites: List = emptyList(), - val bookmarks: List = emptyList(), ) val hiddenIds = MutableStateFlow(HiddenBookmarksIds()) @@ -97,6 +97,7 @@ class FavouritesNewTabSectionViewModel @Inject constructor( } .flowOn(dispatchers.io()) .onEach { favourites -> + logcat { "New Tab: Favourites $favourites" } withContext(dispatchers.main()) { _viewState.emit( viewState.value.copy( @@ -132,6 +133,8 @@ class FavouritesNewTabSectionViewModel @Inject constructor( } withContext(dispatchers.main()) { + pixel.fire(EDIT_FAVOURITE_DIALOG_SHOWN) + pixel.fire(pixel = EDIT_FAVOURITE_DIALOG_SHOWN_DAILY, type = DAILY) command.send( ShowEditSavedSiteDialog( SavedSiteChangedViewState( @@ -164,7 +167,6 @@ class FavouritesNewTabSectionViewModel @Inject constructor( is Bookmark -> { hiddenIds.emit( hiddenIds.value.copy( - bookmarks = hiddenIds.value.bookmarks + savedSite.id, favorites = hiddenIds.value.favorites + savedSite.id, ), ) @@ -189,7 +191,6 @@ class FavouritesNewTabSectionViewModel @Inject constructor( hiddenIds.emit( hiddenIds.value.copy( favorites = hiddenIds.value.favorites - savedSite.id, - bookmarks = hiddenIds.value.bookmarks - savedSite.id, ), ) } @@ -197,10 +198,12 @@ class FavouritesNewTabSectionViewModel @Inject constructor( fun onDeleteFavoriteSnackbarDismissed(savedSite: SavedSite) { delete(savedSite) + pixel.fire(FAVOURITE_REMOVED) } fun onDeleteSavedSiteSnackbarDismissed(savedSite: SavedSite) { delete(savedSite, true) + pixel.fire(FAVOURITE_DELETED) } private fun delete( @@ -212,6 +215,11 @@ class FavouritesNewTabSectionViewModel @Inject constructor( faviconManager.deletePersistedFavicon(savedSite.url) } savedSitesRepository.delete(savedSite, deleteBookmark) + hiddenIds.emit( + hiddenIds.value.copy( + favorites = hiddenIds.value.favorites - savedSite.id, + ), + ) } } @@ -222,15 +230,15 @@ class FavouritesNewTabSectionViewModel @Inject constructor( } fun onTooltipPressed() { - pixel.fire(SavedSitesPixelName.FAVOURITES_TOOLTIP_PRESSED) + pixel.fire(FAVOURITES_TOOLTIP_PRESSED) } fun onListExpanded() { - pixel.fire(SavedSitesPixelName.FAVOURITES_LIST_EXPANDED) + pixel.fire(FAVOURITES_LIST_EXPANDED) } fun onListCollapsed() { - pixel.fire(SavedSitesPixelName.FAVOURITES_LIST_COLLAPSED) + pixel.fire(FAVOURITES_LIST_COLLAPSED) } fun onFavouriteEdited(favorite: Favorite) { @@ -254,16 +262,16 @@ class FavouritesNewTabSectionViewModel @Inject constructor( } fun onFavoriteAdded() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) + pixel.fire(EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) + pixel.fire(EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) } fun onFavoriteRemoved() { - pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED) + pixel.fire(EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED) } fun onFavoriteClicked() { - pixel.fire(SavedSitesPixelName.FAVOURITE_CLICKED) - pixel.fire(SavedSitesPixelName.FAVOURITE_CLICKED_DAILY, type = DAILY) + pixel.fire(FAVOURITE_CLICKED) + pixel.fire(FAVOURITE_CLICKED_DAILY, type = DAILY) } } diff --git a/saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml b/saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml index 3b7f9147a98d..825f1660af4c 100644 --- a/saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml +++ b/saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml @@ -32,6 +32,7 @@ + \ No newline at end of file