From 6a07646ae07124c4c42e3ff8ecd540954b5fff4d Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Tue, 24 Mar 2026 09:33:47 -0400 Subject: [PATCH] [Shields UI] Add blocked resource favicons to panel (#34798) --- .../shields_panel_data_handler.cc | 30 +++++++- .../shields_panel_data_handler.h | 21 ++++-- .../webui/brave_shields/shields_panel_ui.cc | 6 +- .../core/common/brave_shields_panel.mojom | 4 ++ .../resources/panel_new/api/shields_api.ts | 6 +- .../panel_new/api/shields_api_mock.ts | 4 ++ .../panel_new/components/app.style.ts | 1 + .../panel_new/components/main_card.style.ts | 43 ++++++++++++ .../panel_new/components/main_card.tsx | 41 ++++++++++- .../components/use_resource_favicon_urls.ts | 68 +++++++++++++++++++ .../resources/brave_shields_strings.grdp | 2 +- 11 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 components/brave_shields/resources/panel_new/components/use_resource_favicon_urls.ts diff --git a/browser/ui/webui/brave_shields/shields_panel_data_handler.cc b/browser/ui/webui/brave_shields/shields_panel_data_handler.cc index c075d96b0f0..e75c2fc0da0 100644 --- a/browser/ui/webui/brave_shields/shields_panel_data_handler.cc +++ b/browser/ui/webui/brave_shields/shields_panel_data_handler.cc @@ -8,11 +8,14 @@ #include #include "base/check.h" +#include "base/task/sequenced_task_runner.h" #include "brave/browser/brave_browser_process.h" #include "brave/browser/ui/webui/webcompat_reporter/webcompat_reporter_dialog.h" #include "brave/components/brave_shields/content/browser/ad_block_service.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/webui/top_chrome/top_chrome_web_ui_controller.h" +#include "components/favicon/core/favicon_service.h" +#include "components/favicon_base/favicon_types.h" #include "content/public/browser/navigation_controller.h" using brave_shields::BraveShieldsTabHelper; @@ -22,9 +25,11 @@ ShieldsPanelDataHandler::ShieldsPanelDataHandler( mojo::PendingReceiver data_handler_receiver, TopChromeWebUIController* webui_controller, - TabStripModel* tab_strip_model) + TabStripModel* tab_strip_model, + favicon::FaviconService* favicon_service) : data_handler_receiver_(this, std::move(data_handler_receiver)), - webui_controller_(webui_controller) { + webui_controller_(webui_controller), + favicon_service_(favicon_service) { DCHECK(tab_strip_model); tab_strip_model->AddObserver(this); @@ -226,6 +231,27 @@ void ShieldsPanelDataHandler::AreAnyBlockedElementsPresent( active_shields_data_controller_->web_contents()->GetURL().host())); } +void ShieldsPanelDataHandler::IsResourceFaviconAvailable( + const GURL& url, + IsResourceFaviconAvailableCallback callback) { + if (!favicon_service_) { + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), false)); + return; + } + + favicon_service_->GetRawFaviconForPageURL( + url, {favicon_base::IconType::kFavicon}, + /*desired_size_in_pixel=*/0, /*fallback_to_host=*/true, + base::BindOnce( + [](base::OnceCallback cb, + const favicon_base::FaviconRawBitmapResult& result) { + std::move(cb).Run(result.is_valid()); + }, + std::move(callback)), + &cancelable_task_tracker_); +} + void ShieldsPanelDataHandler::ResetBlockedElements() { webui_controller_->embedder()->CloseUI(); diff --git a/browser/ui/webui/brave_shields/shields_panel_data_handler.h b/browser/ui/webui/brave_shields/shields_panel_data_handler.h index 52066d28df3..0c940c29b9d 100644 --- a/browser/ui/webui/brave_shields/shields_panel_data_handler.h +++ b/browser/ui/webui/brave_shields/shields_panel_data_handler.h @@ -10,6 +10,7 @@ #include #include "base/memory/raw_ptr.h" +#include "base/task/cancelable_task_tracker.h" #include "brave/browser/brave_shields/brave_shields_tab_helper.h" #include "brave/components/brave_shields/core/common/brave_shields_panel.mojom.h" #include "brave/components/brave_shields/core/common/shields_settings.mojom.h" @@ -19,9 +20,12 @@ #include "mojo/public/cpp/bindings/remote.h" class TabStripModel; - class TopChromeWebUIController; +namespace favicon { +class FaviconService; +} + class ShieldsPanelDataHandler : public brave_shields::mojom::DataHandler, public brave_shields::BraveShieldsTabHelper::Observer, @@ -31,7 +35,8 @@ class ShieldsPanelDataHandler mojo::PendingReceiver data_handler_receiver, TopChromeWebUIController* webui_controller, - TabStripModel* browser); + TabStripModel* tab_strip_model, + favicon::FaviconService* favicon_service); ShieldsPanelDataHandler(const ShieldsPanelDataHandler&) = delete; ShieldsPanelDataHandler& operator=(const ShieldsPanelDataHandler&) = delete; @@ -61,6 +66,9 @@ class ShieldsPanelDataHandler void ResetBlockedElements() override; void AreAnyBlockedElementsPresent( AreAnyBlockedElementsPresentCallback callback) override; + void IsResourceFaviconAvailable( + const GURL& url, + IsResourceFaviconAvailableCallback callback) override; private: void UpdateSiteBlockInfo(); @@ -78,11 +86,12 @@ class ShieldsPanelDataHandler mojo::Receiver data_handler_receiver_; mojo::Remote ui_handler_remote_; - raw_ptr const webui_controller_ = nullptr; - raw_ptr - active_shields_data_controller_ = nullptr; - brave_shields::mojom::SiteBlockInfo site_block_info_; + base::CancelableTaskTracker cancelable_task_tracker_; + raw_ptr const webui_controller_; // not owned + raw_ptr + active_shields_data_controller_ = nullptr; // not owned + raw_ptr favicon_service_; // not owned }; #endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_SHIELDS_SHIELDS_PANEL_DATA_HANDLER_H_ diff --git a/browser/ui/webui/brave_shields/shields_panel_ui.cc b/browser/ui/webui/brave_shields/shields_panel_ui.cc index 029ef262bb5..efca2d94adc 100644 --- a/browser/ui/webui/brave_shields/shields_panel_ui.cc +++ b/browser/ui/webui/brave_shields/shields_panel_ui.cc @@ -19,6 +19,7 @@ #include "brave/components/constants/pref_names.h" #include "brave/components/constants/webui_url_constants.h" #include "brave/components/webcompat/core/common/features.h" +#include "chrome/browser/favicon/favicon_service_factory.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_finder.h" @@ -112,8 +113,11 @@ void ShieldsPanelUI::CreatePanelHandler( std::move(panel_receiver), this, profile); auto* browser = webui::GetBrowserWindowInterface(web_ui()->GetWebContents()); CHECK(browser); + auto* favicon_service = FaviconServiceFactory::GetForProfile( + profile, ServiceAccessType::EXPLICIT_ACCESS); data_handler_ = std::make_unique( - std::move(data_handler_receiver), this, browser->GetTabStripModel()); + std::move(data_handler_receiver), this, browser->GetTabStripModel(), + favicon_service); } ShieldsPanelUIConfig::ShieldsPanelUIConfig() diff --git a/components/brave_shields/core/common/brave_shields_panel.mojom b/components/brave_shields/core/common/brave_shields_panel.mojom index 15dc5553b44..80c1fd8b08c 100644 --- a/components/brave_shields/core/common/brave_shields_panel.mojom +++ b/components/brave_shields/core/common/brave_shields_panel.mojom @@ -65,6 +65,10 @@ interface DataHandler { // Checks is there any element blocked (by Element blocker feature) // on the current page AreAnyBlockedElementsPresent() => (bool is_available); + + // Returns a value indicating whether a favicon is available for the + // specified page URL. + IsResourceFaviconAvailable(url.mojom.Url url) => (bool is_available); }; struct SiteBlockInfo { diff --git a/components/brave_shields/resources/panel_new/api/shields_api.ts b/components/brave_shields/resources/panel_new/api/shields_api.ts index 6b4f996799e..e27b8521980 100644 --- a/components/brave_shields/resources/panel_new/api/shields_api.ts +++ b/components/brave_shields/resources/panel_new/api/shields_api.ts @@ -232,7 +232,11 @@ export function createShieldsApi(opts: { actions: { ...actionsFor(panelHandler, ['closeUI', 'showUI']), - ...actionsFor(dataHandler, ['openWebCompatWindow', 'updateFavicon']), + ...actionsFor(dataHandler, [ + 'openWebCompatWindow', + 'updateFavicon', + 'isResourceFaviconAvailable', + ]), openTab, }, }) diff --git a/components/brave_shields/resources/panel_new/api/shields_api_mock.ts b/components/brave_shields/resources/panel_new/api/shields_api_mock.ts index 8229d12745e..66d6edf272c 100644 --- a/components/brave_shields/resources/panel_new/api/shields_api_mock.ts +++ b/components/brave_shields/resources/panel_new/api/shields_api_mock.ts @@ -97,6 +97,10 @@ function createMockDataHandler(): Closable { resetBlockedElements() {}, openWebCompatWindow() {}, updateFavicon() {}, + + isResourceFaviconAvailable(url) { + return Promise.resolve({ isAvailable: true }) + }, }) } diff --git a/components/brave_shields/resources/panel_new/components/app.style.ts b/components/brave_shields/resources/panel_new/components/app.style.ts index fc756054532..00e1e3cc491 100644 --- a/components/brave_shields/resources/panel_new/components/app.style.ts +++ b/components/brave_shields/resources/panel_new/components/app.style.ts @@ -9,6 +9,7 @@ import { color, font } from '@brave/leo/tokens/css/variables' export const style = scoped.css` & { background: ${color.container.background}; + color-scheme: light dark; } ` diff --git a/components/brave_shields/resources/panel_new/components/main_card.style.ts b/components/brave_shields/resources/panel_new/components/main_card.style.ts index 021159f0511..9e5dffa03fa 100644 --- a/components/brave_shields/resources/panel_new/components/main_card.style.ts +++ b/components/brave_shields/resources/panel_new/components/main_card.style.ts @@ -79,6 +79,49 @@ export const style = scoped.css` } } + .blocked-items { + display: flex; + align-items: center; + flex-direction: row-reverse; + } + + .blocked-favicon { + --leo-icon-size: 26px; + + display: flex; + align-items: center; + position: relative; + background: ${color.container.background}; + border-radius: 50%; + padding: 4px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.20); + z-index: 1; + margin: 0 -6px; + + &:first-child { + margin-inline-end: 0; + } + + &:last-child { + margin-inline-start: 0; + } + + .favicon { + height: 18px; + width: 18px; + display: block; + margin: 0 auto; + } + + leo-icon { + color: ${color.neutral[50]}; + position: absolute; + inset: 0; + display: block; + z-index: 1; + } + } + .report-prompt { padding: 12px 16px; border-radius: 16px; diff --git a/components/brave_shields/resources/panel_new/components/main_card.tsx b/components/brave_shields/resources/panel_new/components/main_card.tsx index ea5bcade76f..5eec6f2c5e1 100644 --- a/components/brave_shields/resources/panel_new/components/main_card.tsx +++ b/components/brave_shields/resources/panel_new/components/main_card.tsx @@ -5,10 +5,12 @@ import * as React from 'react' import Button from '@brave/leo/react/button' +import Icon from '@brave/leo/react/icon' import Toggle from '@brave/leo/react/toggle' import { formatString } from '$web-common/formatString' import { useShieldsApi } from '../api/shields_api_context' +import { useResourceFaviconUrls } from './use_resource_favicon_urls' import { getString } from './strings' import { style } from './main_card.style' @@ -39,9 +41,12 @@ export function MainCard() { } return (
-
-
{trackersAndAdsBlocked}
- {getString('BRAVE_SHIELDS_TRACKERS_ADS_BLOCKED')} +
+ +
+ {formatString(getString('BRAVE_SHIELDS_TRACKERS_ADS_BLOCKED'), { + $1: () =>
{trackersAndAdsBlocked}
, + })}
) } @@ -82,3 +87,33 @@ export function MainCard() { ) } + +function BlockedFavicons() { + const api = useShieldsApi() + const { data: siteBlockInfo } = api.useGetSiteBlockInfo() + const adsList = siteBlockInfo?.adsList ?? [] + const urls = adsList.map((entry) => entry.url) + + const faviconUrls = useResourceFaviconUrls(urls, { + maxQueries: 16, + maxResults: 3, + }) + + return ( + <> + {faviconUrls.map((url) => ( +
+ {url} + +
+ ))} + + ) +} diff --git a/components/brave_shields/resources/panel_new/components/use_resource_favicon_urls.ts b/components/brave_shields/resources/panel_new/components/use_resource_favicon_urls.ts new file mode 100644 index 00000000000..4bf5768d158 --- /dev/null +++ b/components/brave_shields/resources/panel_new/components/use_resource_favicon_urls.ts @@ -0,0 +1,68 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * 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 https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { useShieldsApi } from '../api/shields_api_context' + +interface Opts { + maxResults: number + maxQueries: number +} + +export function useResourceFaviconUrls(urls: string[], opts: Opts) { + const api = useShieldsApi() + const [pageUrls, setPageUrls] = React.useState([]) + + // Gets the first `opts.maxQueries` deduped empty-path URLs from `urls`. + const queryUrls = React.useMemo(() => { + const set = new Set() + for (const url of urls) { + if (set.size >= opts.maxQueries) { + break + } + try { + const parsed = new URL(url) + set.add(`${parsed.protocol}//${parsed.host}/`) + } catch {} + } + return [...set.values()] + }, [urls.join('\n')]) + + React.useEffect(() => { + let cancelled = false + + // Finds the first `opts.maxResults` favicon page URLs that have a match in + // the profile's favicon database. + async function getFaviconUrls() { + const results = [] + for (const url of queryUrls) { + if (cancelled || results.length >= opts.maxResults) { + break + } + const { isAvailable } = await api.isResourceFaviconAvailable({ url }) + if (isAvailable) { + results.push(url) + } + } + return results + } + + getFaviconUrls().then((pageUrls) => { + if (!cancelled) { + setPageUrls(pageUrls) + } + }) + + return () => { + cancelled = true + } + }, [queryUrls]) + + return pageUrls.map( + (pageUrl: string) => + `chrome://favicon2/?size=32&pageUrl=${encodeURIComponent(pageUrl)}`, + ) +} diff --git a/components/resources/brave_shields_strings.grdp b/components/resources/brave_shields_strings.grdp index 52b4216c2ef..a7816c0f42c 100644 --- a/components/resources/brave_shields_strings.grdp +++ b/components/resources/brave_shields_strings.grdp @@ -41,7 +41,7 @@ - Trackers, ads, and more blocked. + $1 trackers, ads, and more blocked.