for #23069: add blocklist middleware for home

upstream-sync
MatthewTighe 2 years ago committed by mergify[bot]
parent 0b26bac220
commit 2cc9ca3773

@ -7,6 +7,7 @@ package org.mozilla.fenix.ext
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.blocklist.BlocklistHandler
import org.mozilla.fenix.home.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.recenttabs.RecentTab.SearchGroup
@ -100,3 +101,17 @@ internal fun getFilteredStoriesCount(
*/
internal val HomeFragmentState.recentSearchGroup: SearchGroup?
get() = recentTabs.find { it is SearchGroup } as SearchGroup?
/**
* Filter a [HomeFragmentState] by the blocklist.
*
* @param blocklistHandler The handler that will filter the state.
*/
fun HomeFragmentState.filterState(blocklistHandler: BlocklistHandler): HomeFragmentState =
with(blocklistHandler) {
copy(
recentBookmarks = recentBookmarks.filteredByBlocklist(),
recentTabs = recentTabs.filteredByBlocklist(),
recentHistory = recentHistory.filteredByBlocklist()
)
}

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ext
import androidx.annotation.VisibleForTesting
import org.mozilla.fenix.home.recenttabs.RecentTab
/**
* Removes a [RecentTab.Tab] from a list of [RecentTab]. [RecentTab.SearchGroup]s will not be filtered.
*
* @param tab [RecentTab] to remove from the list
*/
@VisibleForTesting
internal fun List<RecentTab>.filterOutTab(tab: RecentTab): List<RecentTab> = filterNot {
it is RecentTab.Tab && tab is RecentTab.Tab && it.state.id == tab.state.id
}

@ -97,6 +97,7 @@ import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.filterState
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
@ -104,6 +105,8 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.sort
import org.mozilla.fenix.home.blocklist.BlocklistHandler
import org.mozilla.fenix.home.blocklist.BlocklistMiddleware
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
@ -234,9 +237,10 @@ class HomeFragment : Fragment() {
::dispatchModeChanges
)
val blocklistHandler = BlocklistHandler(components.settings)
homeFragmentStore = StoreProvider.get(this) {
HomeFragmentStore(
HomeFragmentState(
initialState = HomeFragmentState(
collections = components.core.tabCollectionStorage.cachedTabCollections,
expandedCollections = emptySet(),
mode = currentMode.getCurrentMode(),
@ -260,8 +264,9 @@ class HomeFragment : Fragment() {
// to some state.
recentTabs = getRecentTabs(components),
recentHistory = emptyList()
),
listOf(
).run { filterState(blocklistHandler) },
middlewares = listOf(
BlocklistMiddleware(blocklistHandler),
PocketUpdatesMiddleware(
lifecycleScope,
requireComponents.core.pocketStoriesService,
@ -358,11 +363,13 @@ class HomeFragment : Fragment() {
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
navController = findNavController(),
metrics = requireComponents.analytics.metrics,
store = components.core.store
store = components.core.store,
homeStore = homeFragmentStore,
),
recentBookmarksController = DefaultRecentBookmarksController(
activity = activity,
navController = findNavController()
navController = findNavController(),
homeStore = homeFragmentStore,
),
recentVisitsController = DefaultRecentVisitsController(
navController = findNavController(),

@ -15,6 +15,7 @@ import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.filterOutTab
import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.ext.recentSearchGroup
import org.mozilla.fenix.home.pocket.POCKET_STORIES_TO_SHOW_COUNT
@ -101,7 +102,9 @@ sealed class HomeFragmentAction : Action {
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction()
data class RecentTabsChange(val recentTabs: List<RecentTab>) : HomeFragmentAction()
data class RemoveRecentTab(val recentTab: RecentTab) : HomeFragmentAction()
data class RecentBookmarksChange(val recentBookmarks: List<RecentBookmark>) : HomeFragmentAction()
data class RemoveRecentBookmark(val recentBookmark: RecentBookmark) : HomeFragmentAction()
data class RecentHistoryChange(val recentHistory: List<RecentlyVisitedItem>) : HomeFragmentAction()
data class RemoveRecentHistoryHighlight(val highlightUrl: String) : HomeFragmentAction()
data class DisbandSearchGroupAction(val searchTerm: String) : HomeFragmentAction()
@ -167,7 +170,15 @@ private fun homeFragmentStateReducer(
recentHistory = state.recentHistory.filterOut(recentSearchGroup?.searchTerm)
)
}
is HomeFragmentAction.RemoveRecentTab -> {
state.copy(
recentTabs = state.recentTabs.filterOutTab(action.recentTab)
)
}
is HomeFragmentAction.RecentBookmarksChange -> state.copy(recentBookmarks = action.recentBookmarks)
is HomeFragmentAction.RemoveRecentBookmark -> {
state.copy(recentBookmarks = state.recentBookmarks.filterNot { it.url == action.recentBookmark.url })
}
is HomeFragmentAction.RecentHistoryChange -> state.copy(
recentHistory = action.recentHistory.filterOut(state.recentSearchGroup?.searchTerm)
)

@ -0,0 +1,89 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.blocklist
import androidx.annotation.VisibleForTesting
import mozilla.components.support.ktx.kotlin.sha1
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.utils.Settings
/**
* Class for interacting with the a blocklist stored in [settings].
* The blocklist is a set of SHA1 hashed URLs, which are stripped
* of protocols and common subdomains like "www" or "mobile".
*/
class BlocklistHandler(private val settings: Settings) {
/**
* Add an URL to the blocklist. The URL will be stripped and hashed,
* so no pre-formatted is required.
*/
fun addUrlToBlocklist(url: String) {
val updatedBlocklist = settings.homescreenBlocklist + url.stripAndHash()
settings.homescreenBlocklist = updatedBlocklist
}
/**
* Filter a list of recent bookmarks by the blocklist. Requires this class to be contextually
* in a scope.
*/
@JvmName("filterRecentBookmark")
fun List<RecentBookmark>.filteredByBlocklist(): List<RecentBookmark> =
settings.homescreenBlocklist.let { blocklist ->
filterNot {
it.url?.let { url -> blocklistContainsUrl(blocklist, url) } ?: false
}
}
/**
* Filter a list of recent tabs by the blocklist. Requires this class to be contextually
* in a scope.
*/
@JvmName("filterRecentTab")
fun List<RecentTab>.filteredByBlocklist(): List<RecentTab> =
settings.homescreenBlocklist.let { blocklist ->
filterNot {
it is RecentTab.Tab && blocklistContainsUrl(blocklist, it.state.content.url)
}
}
/**
* Filter a list of recent history items by the blocklist. Requires this class to be contextually
* in a scope.
*/
@JvmName("filterRecentHistory")
fun List<RecentlyVisitedItem>.filteredByBlocklist(): List<RecentlyVisitedItem> =
settings.homescreenBlocklist.let { blocklist ->
filterNot {
it is RecentlyVisitedItem.RecentHistoryHighlight &&
blocklistContainsUrl(blocklist, it.url)
}
}
private fun blocklistContainsUrl(blocklist: Set<String>, url: String): Boolean =
blocklist.any { it == url.stripAndHash() }
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun String.stripAndHash(): String =
this.stripProtocolAndCommonSubdomains().sha1()
// Eventually, this should be standardize in A-C and this can then be removed
// https://github.com/mozilla-mobile/android-components/issues/11743
private fun String.stripProtocolAndCommonSubdomains(): String {
val stripped = this.substringAfter("://").dropLastWhile { it == '/' }
// This kind of stripping allows us to match "twitter" to "mobile.twitter.com".
// Borrowed from DomainMatcher in A-C
val domainsToStrip = listOf("www", "mobile", "m")
domainsToStrip.forEach { domain ->
if (stripped.startsWith("$domain.")) {
return stripped.substring(domain.length + 1)
}
}
return stripped
}

@ -0,0 +1,99 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.blocklist
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.recenttabs.RecentTab
/**
* This [Middleware] reacts to item removals from the home screen, adding them to a blocklist.
* Additionally, it reacts to state changes in order to filter them by the blocklist.
*
* @param settings Blocklist is stored here as a string set
*/
class BlocklistMiddleware(
private val blocklistHandler: BlocklistHandler
) : Middleware<HomeFragmentState, HomeFragmentAction> {
/**
* Will filter "Change" actions using the blocklist and use "Remove" actions to update
* the blocklist.
*/
override fun invoke(
context: MiddlewareContext<HomeFragmentState, HomeFragmentAction>,
next: (HomeFragmentAction) -> Unit,
action: HomeFragmentAction
) {
next(getUpdatedAction(context.state, action))
}
private fun getUpdatedAction(
state: HomeFragmentState,
action: HomeFragmentAction
) = with(blocklistHandler) {
when (action) {
is HomeFragmentAction.Change -> {
action.copy(
recentBookmarks = action.recentBookmarks.filteredByBlocklist(),
recentTabs = action.recentTabs.filteredByBlocklist(),
recentHistory = action.recentHistory.filteredByBlocklist()
)
}
is HomeFragmentAction.RecentTabsChange -> {
action.copy(
recentTabs = action.recentTabs.filteredByBlocklist()
)
}
is HomeFragmentAction.RecentBookmarksChange -> {
action.copy(
recentBookmarks = action.recentBookmarks.filteredByBlocklist()
)
}
is HomeFragmentAction.RecentHistoryChange -> {
action.copy(recentHistory = action.recentHistory.filteredByBlocklist())
}
is HomeFragmentAction.RemoveRecentTab -> {
if (action.recentTab is RecentTab.Tab) {
addUrlToBlocklist(action.recentTab.state.content.url)
state.toActionFilteringAllState(this)
} else {
action
}
}
is HomeFragmentAction.RemoveRecentBookmark -> {
action.recentBookmark.url?.let { url ->
addUrlToBlocklist(url)
state.toActionFilteringAllState(this)
} ?: action
}
is HomeFragmentAction.RemoveRecentHistoryHighlight -> {
addUrlToBlocklist(action.highlightUrl)
state.toActionFilteringAllState(this)
}
else -> action
}
}
// When an item is removed from any part of the state, it should also be removed from any other
// relevant parts that contain it.
// This is a candidate for refactoring once context receivers lands in Kotlin 1.6.20
// https://blog.jetbrains.com/kotlin/2022/02/kotlin-1-6-20-m1-released/#prototype-of-context-receivers-for-kotlin-jvm
private fun HomeFragmentState.toActionFilteringAllState(blocklistHandler: BlocklistHandler) =
with(blocklistHandler) {
HomeFragmentAction.Change(
recentTabs = recentTabs.filteredByBlocklist(),
recentBookmarks = recentBookmarks.filteredByBlocklist(),
recentHistory = recentHistory.filteredByBlocklist(),
topSites = topSites,
mode = mode,
collections = collections,
tip = tip,
showCollectionPlaceholder = showCollectionPlaceholder
)
}
}

@ -15,7 +15,9 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
@ -34,6 +36,11 @@ interface RecentBookmarksController {
* @see [RecentBookmarksInteractor.onShowAllBookmarksClicked]
*/
fun handleShowAllBookmarksClicked()
/**
* @see [RecentBookmarksInteractor.onRecentBookmarkRemoved]
*/
fun handleBookmarkRemoved(bookmark: RecentBookmark)
}
/**
@ -41,7 +48,8 @@ interface RecentBookmarksController {
*/
class DefaultRecentBookmarksController(
private val activity: HomeActivity,
private val navController: NavController
private val navController: NavController,
private val homeStore: HomeFragmentStore,
) : RecentBookmarksController {
override fun handleBookmarkClicked(bookmark: RecentBookmark) {
@ -63,6 +71,10 @@ class DefaultRecentBookmarksController(
)
}
override fun handleBookmarkRemoved(bookmark: RecentBookmark) {
homeStore.dispatch(HomeFragmentAction.RemoveRecentBookmark(bookmark))
}
@VisibleForTesting(otherwise = PRIVATE)
fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {

@ -25,4 +25,12 @@ interface RecentBookmarksInteractor {
* recently saved bookmarks on the home screen.
*/
fun onShowAllBookmarksClicked()
/**
* Removes a bookmark from the recent bookmark list. Called when a user clicks the "Remove"
* button for recently saved bookmarks on the home screen.
*
* @param bookmark The bookmark that has been removed.
*/
fun onRecentBookmarkRemoved(bookmark: RecentBookmark)
}

@ -4,15 +4,17 @@
package org.mozilla.fenix.home.recentbookmarks.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
@ -21,8 +23,14 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -44,12 +52,14 @@ import org.mozilla.fenix.theme.FirefoxTheme
* A list of recent bookmarks.
*
* @param bookmarks List of [RecentBookmark]s to display.
* @param menuItems List of [RecentBookmarksMenuItem] shown when long clicking a [RecentBookmarkItem]
* @param onRecentBookmarkClick Invoked when the user clicks on a recent bookmark.
*/
@Composable
fun RecentBookmarks(
bookmarks: List<RecentBookmark>,
onRecentBookmarkClick: (RecentBookmark) -> Unit = {}
menuItems: List<RecentBookmarksMenuItem>,
onRecentBookmarkClick: (RecentBookmark) -> Unit = {},
) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
@ -58,7 +68,8 @@ fun RecentBookmarks(
items(bookmarks) { bookmark ->
RecentBookmarkItem(
bookmark = bookmark,
onRecentBookmarkClick = onRecentBookmarkClick
menuItems = menuItems,
onRecentBookmarkClick = onRecentBookmarkClick,
)
}
}
@ -70,15 +81,23 @@ fun RecentBookmarks(
* @param bookmark The [RecentBookmark] to display.
* @param onRecentBookmarkClick Invoked when the user clicks on the recent bookmark item.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RecentBookmarkItem(
bookmark: RecentBookmark,
menuItems: List<RecentBookmarksMenuItem>,
onRecentBookmarkClick: (RecentBookmark) -> Unit = {}
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.width(156.dp)
.clickable { onRecentBookmarkClick(bookmark) }
.combinedClickable(
enabled = true,
onClick = { onRecentBookmarkClick(bookmark) },
onLongClick = { isMenuExpanded = true }
)
) {
Card(
modifier = Modifier
@ -98,6 +117,13 @@ private fun RecentBookmarkItem(
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
RecentBookmarksMenu(
showMenu = isMenuExpanded,
menuItems = menuItems,
recentBookmark = bookmark,
onDismissRequest = { isMenuExpanded = false }
)
}
}
@ -146,6 +172,49 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
}
}
/**
* Menu shown for a [RecentBookmark].
*
* @see [DropdownMenu]
*
* @param showMenu Whether this is currently open and visible to the user.
* @param menuItems List of options shown.
* @param recentBookmark The [RecentBookmark] for which this menu is shown.
* @param onDismissRequest Called when the user chooses a menu option or requests to dismiss the menu.
*/
@Composable
private fun RecentBookmarksMenu(
showMenu: Boolean,
menuItems: List<RecentBookmarksMenuItem>,
recentBookmark: RecentBookmark,
onDismissRequest: () -> Unit,
) {
DropdownMenu(
expanded = showMenu,
onDismissRequest = { onDismissRequest() },
modifier = Modifier
.background(color = FirefoxTheme.colors.layer2)
) {
for (item in menuItems) {
DropdownMenuItem(
onClick = {
onDismissRequest()
item.onClick(recentBookmark)
},
) {
Text(
text = item.title,
color = FirefoxTheme.colors.textPrimary,
maxLines = 1,
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterVertically)
)
}
}
}
}
@Composable
@Preview
private fun RecentBookmarksPreview() {
@ -172,7 +241,8 @@ private fun RecentBookmarksPreview() {
url = "https://www.example.com",
previewImageUrl = null
)
)
),
menuItems = listOf()
)
}
}

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.recentbookmarks.view
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
/**
* A menu item in the recent bookmarks dropdown menu.
*
* @property title The menu item title.
* @property onClick Invoked when the user clicks on the menu item.
*/
data class RecentBookmarksMenuItem(
val title: String,
val onClick: (RecentBookmark) -> Unit
)

@ -7,8 +7,10 @@ package org.mozilla.fenix.home.recentbookmarks.view
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.LifecycleOwner
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.compose.ComposeViewHolder
@ -37,7 +39,13 @@ class RecentBookmarksViewHolder(
RecentBookmarks(
bookmarks = recentBookmarks.value ?: emptyList(),
onRecentBookmarkClick = interactor::onRecentBookmarkClicked
onRecentBookmarkClick = interactor::onRecentBookmarkClicked,
menuItems = listOf(
RecentBookmarksMenuItem(
stringResource(id = R.string.recently_saved_menu_item_remove),
onClick = { bookmark -> interactor.onRecentBookmarkRemoved(bookmark) }
)
)
)
}
}

@ -13,7 +13,10 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.inProgressMediaTab
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
/**
@ -35,6 +38,11 @@ interface RecentTabController {
* @see [RecentTabInteractor.onRecentTabShowAllClicked]
*/
fun handleRecentTabShowAllClicked()
/**
* @see [RecentTabInteractor.onRemoveRecentTab]
*/
fun handleRecentTabRemoved(tab: RecentTab.Tab)
}
/**
@ -47,7 +55,8 @@ class DefaultRecentTabsController(
private val selectTabUseCase: SelectTabUseCase,
private val navController: NavController,
private val metrics: MetricController,
private val store: BrowserStore
private val store: BrowserStore,
private val homeStore: HomeFragmentStore,
) : RecentTabController {
override fun handleRecentTabClicked(tabId: String) {
@ -76,6 +85,10 @@ class DefaultRecentTabsController(
)
}
override fun handleRecentTabRemoved(tab: RecentTab.Tab) {
homeStore.dispatch(HomeFragmentAction.RemoveRecentTab(tab))
}
@VisibleForTesting(otherwise = PRIVATE)
fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {

@ -4,6 +4,8 @@
package org.mozilla.fenix.home.recenttabs.interactor
import org.mozilla.fenix.home.recenttabs.RecentTab
/**
* Interface for recent tab related actions in the Home screen.
*/
@ -27,4 +29,12 @@ interface RecentTabInteractor {
* tabs.
*/
fun onRecentTabShowAllClicked()
/**
* Removes a bookmark from the recent bookmark list. Called when a user clicks the "Remove"
* button for recently saved bookmarks on the home screen.
*
* @param tab The tab that has been removed.
*/
fun onRemoveRecentTab(tab: RecentTab.Tab)
}

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.recenttabs.view
import org.mozilla.fenix.home.recenttabs.RecentTab
/**
* A menu item in the recent tab dropdown menu.
*
* @property title The menu item title.
* @property onClick Invoked when the user clicks on the menu item.
*/
class RecentTabMenuItem(
val title: String,
val onClick: (RecentTab.Tab) -> Unit
)

@ -7,6 +7,7 @@ package org.mozilla.fenix.home.recenttabs.view
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.LifecycleOwner
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
@ -45,7 +46,13 @@ class RecentTabViewHolder(
RecentTabs(
recentTabs = recentTabs.value ?: emptyList(),
onRecentTabClick = { interactor.onRecentTabClicked(it) },
onRecentSearchGroupClicked = { interactor.onRecentSearchGroupClicked(it) }
onRecentSearchGroupClicked = { interactor.onRecentSearchGroupClicked(it) },
menuItems = listOf(
RecentTabMenuItem(
title = stringResource(id = R.string.recent_tab_menu_item_remove),
onClick = { tab -> interactor.onRemoveRecentTab(tab) }
)
)
)
}
}

@ -7,15 +7,18 @@
package org.mozilla.fenix.home.recenttabs.view
import android.graphics.Bitmap
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -24,12 +27,17 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -37,6 +45,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
@ -60,12 +69,14 @@ import org.mozilla.fenix.theme.FirefoxTheme
* A list of recent tabs to jump back to.
*
* @param recentTabs List of [RecentTab] to display.
* @param menuItems List of [RecentTabMenuItem] shown long clicking a [RecentTab].
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
* @param onRecentSearchGroupClicked Invoked when the user clicks on a recent search group.
*/
@Composable
fun RecentTabs(
recentTabs: List<RecentTab>,
menuItems: List<RecentTabMenuItem>,
onRecentTabClick: (String) -> Unit = {},
onRecentSearchGroupClicked: (String) -> Unit = {}
) {
@ -78,6 +89,7 @@ fun RecentTabs(
is RecentTab.Tab -> {
RecentTabItem(
tab = tab,
menuItems = menuItems,
onRecentTabClick = onRecentTabClick
)
}
@ -102,17 +114,25 @@ fun RecentTabs(
* @param tab [RecentTab.Tab] that was recently viewed.
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
*/
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongParameterList")
@Composable
private fun RecentTabItem(
tab: RecentTab.Tab,
menuItems: List<RecentTabMenuItem>,
onRecentTabClick: (String) -> Unit = {}
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.height(112.dp)
.clickable { onRecentTabClick(tab.state.id) },
.combinedClickable(
enabled = true,
onClick = { onRecentTabClick(tab.state.id) },
onLongClick = { isMenuExpanded = true }
),
shape = RoundedCornerShape(8.dp),
backgroundColor = FirefoxTheme.colors.layer2,
elevation = 6.dp
@ -149,6 +169,13 @@ private fun RecentTabItem(
RecentTabSubtitle(subtitle = tab.state.content.url)
}
}
RecentTabMenu(
showMenu = isMenuExpanded,
menuItems = menuItems,
tab = tab,
onDismissRequest = { isMenuExpanded = false }
)
}
}
}
@ -300,6 +327,53 @@ private fun RecentTabImage(
}
}
/**
* Menu shown for a [RecentTab.Tab].
*
* @see [DropdownMenu]
*
* @param showMenu Whether this is currently open and visible to the user.
* @param menuItems List of options shown.
* @param tab The [RecentTab.Tab] for which this menu is shown.
* @param onDismissRequest Called when the user chooses a menu option or requests to dismiss the menu.
*/
@Composable
private fun RecentTabMenu(
showMenu: Boolean,
menuItems: List<RecentTabMenuItem>,
tab: RecentTab.Tab,
onDismissRequest: () -> Unit,
) {
DisposableEffect(LocalConfiguration.current.orientation) {
onDispose { onDismissRequest() }
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { onDismissRequest() },
modifier = Modifier
.background(color = FirefoxTheme.colors.layer2)
) {
for (item in menuItems) {
DropdownMenuItem(
onClick = {
onDismissRequest()
item.onClick(tab)
},
) {
Text(
text = item.title,
color = FirefoxTheme.colors.textPrimary,
maxLines = 1,
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterVertically)
)
}
}
}
}
/**
* A recent tab icon.
*

@ -17,6 +17,7 @@ import org.mozilla.fenix.home.pocket.PocketStoriesInteractor
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
@ -393,6 +394,10 @@ class SessionControlInteractor(
recentTabController.handleRecentTabShowAllClicked()
}
override fun onRemoveRecentTab(tab: RecentTab.Tab) {
recentTabController.handleRecentTabRemoved(tab)
}
override fun onRecentBookmarkClicked(bookmark: RecentBookmark) {
recentBookmarksController.handleBookmarkClicked(bookmark)
}
@ -401,6 +406,10 @@ class SessionControlInteractor(
recentBookmarksController.handleShowAllBookmarksClicked()
}
override fun onRecentBookmarkRemoved(bookmark: RecentBookmark) {
recentBookmarksController.handleBookmarkRemoved(bookmark)
}
override fun onHistoryShowAllClicked() {
recentVisitsController.handleHistoryShowAllClicked()
}

@ -25,6 +25,7 @@ import mozilla.components.support.ktx.android.content.floatPreference
import mozilla.components.support.ktx.android.content.intPreference
import mozilla.components.support.ktx.android.content.longPreference
import mozilla.components.support.ktx.android.content.stringPreference
import mozilla.components.support.ktx.android.content.stringSetPreference
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
@ -1308,4 +1309,12 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false,
featureFlag = FeatureFlags.taskContinuityFeature,
)
/**
* Blocklist used to filter items from the home screen that have previously been removed.
*/
var homescreenBlocklist by stringSetPreference(
appContext.getPreferenceKey(R.string.pref_key_home_blocklist),
default = setOf()
)
}

@ -67,6 +67,7 @@
<string name="pref_key_should_show_default_browser_notification" translatable="false">pref_key_should_show_default_browser_notification</string>
<string name="pref_key_is_first_run" translatable="false">pref_key_is_first_run</string>
<string name="pref_key_has_shown_home_onboarding" translatable="false">pref_key_has_shown_home_onboarding</string>
<string name="pref_key_home_blocklist">pref_key_home_blocklist</string>
<!-- Data Choices -->
<string name="pref_key_telemetry" translatable="false">pref_key_telemetry</string>

@ -46,6 +46,8 @@
<string name="recently_saved_show_all_content_description_2">Show all saved bookmarks</string>
<!-- Content description for the button which navigates the user to show all of their saved bookmarks. -->
<string name="recently_saved_show_all_content_description" moz:removedIn="97" tools:ignore="UnusedResources">Show all saved bookmarks button</string>
<!-- Text for the menu button to remove a recently saved bookmark from the user's home screen -->
<string name="recently_saved_menu_item_remove">Remove</string>
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
<string name="about_content">%1$s is produced by Mozilla.</string>
@ -129,6 +131,9 @@
<!-- Text for the number of tabs in a group in the 'Jump back in' section of the new tab
%d is a placeholder for the number of sites in the group. This number will always be more than one. -->
<string name="recent_tabs_search_term_count_2">%d sites</string>
<!-- Text for the menu button to remove a grouped highlight from the user's browsing history
in the Recently visited section -->
<string name="recent_tab_menu_item_remove">Remove</string>
<!-- History Metadata -->
<!-- Header text for a section on the home screen that displays grouped highlights from the

@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ext
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.state.state.TabSessionState
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.home.recenttabs.RecentTab
class RecentTabsTest {
@Test
fun `Test filtering out tab`() {
val filteredId = "id"
val mockSessionState: TabSessionState = mockk()
every { mockSessionState.id } returns filteredId
val tab = RecentTab.Tab(mockSessionState)
val searchGroup = RecentTab.SearchGroup(
tabId = filteredId,
searchTerm = "",
url = "",
thumbnail = null,
count = 0
)
val recentTabs = listOf(tab, searchGroup)
val result = recentTabs.filterOutTab(tab)
assertEquals(listOf(searchGroup), result)
}
}

@ -0,0 +1,120 @@
package org.mozilla.fenix.home.blocklist
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.utils.Settings
class BlocklistHandlerTest {
private val mockSettings: Settings = mockk()
private lateinit var blocklistHandler: BlocklistHandler
@Before
fun setup() {
blocklistHandler = BlocklistHandler(mockSettings)
}
@Test
fun `WHEN url added to blocklist THEN settings updated with hash`() {
val addedUrl = "url"
val updateSlot = slot<Set<String>>()
every { mockSettings.homescreenBlocklist } returns setOf()
every { mockSettings.homescreenBlocklist = capture(updateSlot) } returns Unit
blocklistHandler.addUrlToBlocklist(addedUrl)
assertEquals(setOf(addedUrl.stripAndHash()), updateSlot.captured)
}
@Test
fun `GIVEN bookmark is not in blocklist THEN will not be filtered`() {
val bookmarks = listOf(RecentBookmark(url = "test"))
every { mockSettings.homescreenBlocklist } returns setOf()
val filtered = with(blocklistHandler) {
bookmarks.filteredByBlocklist()
}
assertEquals(bookmarks, filtered)
}
@Test
fun `GIVEN bookmark is in blocklist THEN will be filtered`() {
val blockedUrl = "test"
val bookmarks = listOf(RecentBookmark(url = blockedUrl))
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
val filtered = with(blocklistHandler) {
bookmarks.filteredByBlocklist()
}
assertEquals(listOf<String>(), filtered)
}
@Test
fun `GIVEN recent history is not in blocklist THEN will not be filtered`() {
val recentHistory = listOf(RecentlyVisitedItem.RecentHistoryHighlight(url = "test", title = ""))
every { mockSettings.homescreenBlocklist } returns setOf()
val filtered = with(blocklistHandler) {
recentHistory.filteredByBlocklist()
}
assertEquals(recentHistory, filtered)
}
@Test
fun `GIVEN recent history is in blocklist THEN will be filtered`() {
val blockedUrl = "test"
val recentHistory = listOf(RecentlyVisitedItem.RecentHistoryHighlight(url = blockedUrl, title = ""))
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
val filtered = with(blocklistHandler) {
recentHistory.filteredByBlocklist()
}
assertEquals(listOf<String>(), filtered)
}
@Test
fun `GIVEN recent tab is not in blocklist THEN will not be filtered`() {
val mockSessionState: TabSessionState = mockk()
val mockContent: ContentState = mockk()
val tabs = listOf(RecentTab.Tab(mockSessionState))
every { mockSessionState.content } returns mockContent
every { mockContent.url } returns "test"
every { mockSettings.homescreenBlocklist } returns setOf()
val filtered = with(blocklistHandler) {
tabs.filteredByBlocklist()
}
assertEquals(tabs, filtered)
}
@Test
fun `GIVEN recent tab is in blocklist THEN will be filtered`() {
val blockedUrl = "test"
val mockSessionState: TabSessionState = mockk()
val mockContent: ContentState = mockk()
val tabs = listOf(RecentTab.Tab(mockSessionState))
every { mockSessionState.content } returns mockContent
every { mockContent.url } returns blockedUrl
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
val filtered = with(blocklistHandler) {
tabs.filteredByBlocklist()
}
assertEquals(listOf<String>(), filtered)
}
}

@ -0,0 +1,308 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.blocklist
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.utils.Settings
class BlocklistMiddlewareTest {
private val mockSettings: Settings = mockk()
private val blocklistHandler = BlocklistHandler(mockSettings)
@Test
fun `GIVEN empty blocklist WHEN action intercepted THEN unchanged by middleware`() {
val updatedBookmark = RecentBookmark(url = "https://www.mozilla.org/")
every { mockSettings.homescreenBlocklist } returns setOf()
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertEquals(updatedBookmark, store.state.recentBookmarks[0])
}
@Test
fun `GIVEN non-empty blocklist WHEN action intercepted with no matching elements THEN unchanged by middleware`() {
val updatedBookmark = RecentBookmark(url = "https://www.mozilla.org/")
every { mockSettings.homescreenBlocklist } returns setOf("https://www.github.org/".stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertEquals(updatedBookmark, store.state.recentBookmarks[0])
}
@Test
fun `GIVEN non-empty blocklist with specific pages WHEN action intercepted with matching host THEN unchanged by middleware`() {
val updatedBookmark = RecentBookmark(url = "https://github.com/")
every { mockSettings.homescreenBlocklist } returns setOf("https://github.com/mozilla-mobile/fenix".stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertEquals(updatedBookmark, store.state.recentBookmarks[0])
}
@Test
fun `GIVEN non-empty blocklist WHEN action intercepted with matching elements THEN filtered by middleware`() {
val updatedBookmark = RecentBookmark(url = "https://www.mozilla.org/")
every { mockSettings.homescreenBlocklist } returns setOf("https://www.mozilla.org/".stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertTrue(store.state.recentBookmarks.isEmpty())
}
@Test
fun `GIVEN non-empty blocklist WHEN action intercepted with matching elements THEN all relevant sections filtered by middleware`() {
val blockedUrl = "https://www.mozilla.org/"
val updatedBookmarks = listOf(RecentBookmark(url = blockedUrl))
val updatedRecentTabs = listOf(RecentTab.Tab(createTab(url = blockedUrl)))
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = updatedRecentTabs,
recentBookmarks = updatedBookmarks,
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertTrue(store.state.recentBookmarks.isEmpty())
assertTrue(store.state.recentTabs.isEmpty())
}
@Test
fun `GIVEN non-empty blocklist WHEN action intercepted with matching elements THEN only matching urls removed`() {
val blockedUrl = "https://www.mozilla.org/"
val unblockedUrl = "https://www.github.org/"
val unblockedBookmark = RecentBookmark(unblockedUrl)
val updatedBookmarks = listOf(
RecentBookmark(url = blockedUrl), unblockedBookmark
)
val unblockedRecentTab = RecentTab.Tab(createTab(url = unblockedUrl))
val updatedRecentTabs =
listOf(RecentTab.Tab(createTab(url = blockedUrl)), unblockedRecentTab)
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = updatedRecentTabs,
recentBookmarks = updatedBookmarks,
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertEquals(unblockedBookmark, store.state.recentBookmarks[0])
assertEquals(unblockedRecentTab, store.state.recentTabs[0])
}
@Test
fun `WHEN remove action intercepted THEN hashed url added to blocklist and Change action dispatched`() {
val captureMiddleware = CaptureActionsMiddleware<HomeFragmentState, HomeFragmentAction>()
val removedUrl = "https://www.mozilla.org/"
val removedBookmark = RecentBookmark(url = removedUrl)
val updateSlot = slot<Set<String>>()
every { mockSettings.homescreenBlocklist } returns setOf() andThen setOf(removedUrl.stripAndHash())
every { mockSettings.homescreenBlocklist = capture(updateSlot) } returns Unit
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(recentBookmarks = listOf(removedBookmark)),
middlewares = listOf(middleware, captureMiddleware)
)
store.dispatch(
HomeFragmentAction.RemoveRecentBookmark(removedBookmark)
).joinBlocking()
val capturedAction = captureMiddleware.findFirstAction(HomeFragmentAction.Change::class)
assertEquals(emptyList<RecentBookmark>(), capturedAction.recentBookmarks)
assertEquals(setOf(removedUrl.stripAndHash()), updateSlot.captured)
}
@Test
fun `WHEN urls are compared to blocklist THEN protocols are stripped`() {
val host = "www.mozilla.org/"
val updatedBookmark = RecentBookmark(url = "http://$host")
every { mockSettings.homescreenBlocklist } returns setOf("https://$host".stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertTrue(store.state.recentBookmarks.isEmpty())
}
@Test
fun `WHEN urls are compared to blocklist THEN common subdomains are stripped`() {
val host = "mozilla.org/"
val updatedBookmark = RecentBookmark(url = host)
every { mockSettings.homescreenBlocklist } returns setOf(host.stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertTrue(store.state.recentBookmarks.isEmpty())
}
@Test
fun `WHEN urls are compared to blocklist THEN trailing slashes are stripped`() {
val host = "www.mozilla.org"
val updatedBookmark = RecentBookmark(url = "http://$host/")
every { mockSettings.homescreenBlocklist } returns setOf("https://$host".stripAndHash())
val middleware = BlocklistMiddleware(blocklistHandler)
val store = HomeFragmentStore(
HomeFragmentState(),
middlewares = listOf(middleware)
)
store.dispatch(
HomeFragmentAction.Change(
topSites = store.state.topSites,
mode = store.state.mode,
collections = store.state.collections,
tip = store.state.tip,
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
recentTabs = store.state.recentTabs,
recentBookmarks = listOf(updatedBookmark),
recentHistory = store.state.recentHistory
)
).joinBlocking()
assertTrue(store.state.recentBookmarks.isEmpty())
}
}

@ -27,6 +27,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController
@OptIn(ExperimentalCoroutinesApi::class)
@ -38,6 +39,7 @@ class DefaultRecentBookmarksControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val navController: NavController = mockk(relaxUnitFun = true)
private val metrics: MetricController = mockk(relaxed = true)
private val homeStore: HomeFragmentStore = mockk()
private lateinit var controller: DefaultRecentBookmarksController
@ -54,8 +56,9 @@ class DefaultRecentBookmarksControllerTest {
controller = spyk(
DefaultRecentBookmarksController(
activity = activity,
navController = navController
)
navController = navController,
homeStore = mockk()
),
)
}

@ -24,6 +24,7 @@ import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.HomeFragmentStore
@OptIn(ExperimentalCoroutinesApi::class)
class RecentTabControllerTest {
@ -34,6 +35,7 @@ class RecentTabControllerTest {
private val navController: NavController = mockk(relaxed = true)
private val selectTabUseCase: TabsUseCases = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
private val homeStore: HomeFragmentStore = mockk()
private lateinit var store: BrowserStore
@ -50,6 +52,7 @@ class RecentTabControllerTest {
navController = navController,
metrics = metrics,
store = store,
homeStore = homeStore,
)
)
every { navController.navigateUp() } returns true

Loading…
Cancel
Save