Add toast option to dismiss temporarily or permanently
The non-actionable toasts will show a 3-dot menu that has an option
to dismiss it for now or to stop showing the non-actionable toasts.
Additionally a setting is added to control this option.
Screenshot: https://screenshot.googleplex.com/5tQ7V6cFMVBvVf2
Bug: 383135477
Change-Id: I87415454038ec8169831a34bedc7a5fbf18bf4df
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6090300
Commit-Queue: Alison Gale <[email protected]>
Reviewed-by: Lei Zhang <[email protected]>
Reviewed-by: Steven Luong <[email protected]>
Code-Coverage: [email protected] <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1396134}
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index bff0f19..8424b56 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -1989,6 +1989,8 @@
glic::GlicConfiguration::RegisterPrefs(registry);
#endif
+ registry->RegisterIntegerPref(prefs::kToastAlertLevel, 0);
+
// This is intentionally last.
RegisterLocalStatePrefsForMigration(registry);
}
diff --git a/chrome/browser/ui/toasts/BUILD.gn b/chrome/browser/ui/toasts/BUILD.gn
index c73d0e2..82b1596 100644
--- a/chrome/browser/ui/toasts/BUILD.gn
+++ b/chrome/browser/ui/toasts/BUILD.gn
@@ -9,6 +9,7 @@
source_set("toasts") {
sources = [
"toast_controller.h",
+ "toast_dismiss_menu_model.h",
"toast_features.h",
"toast_metrics.h",
"toast_service.h",
@@ -34,6 +35,7 @@
source_set("impl") {
sources = [
"toast_controller.cc",
+ "toast_dismiss_menu_model.cc",
"toast_features.cc",
"toast_metrics.cc",
"toast_service.cc",
@@ -45,6 +47,7 @@
"//chrome/app:generated_resources",
"//chrome/app:generated_resources_grit",
"//chrome/app/vector_icons",
+ "//chrome/browser:browser_process",
"//chrome/browser:browser_public_dependencies",
"//chrome/browser/profiles:profile",
"//chrome/browser/ui:browser_element_identifiers",
diff --git a/chrome/browser/ui/toasts/toast_controller.cc b/chrome/browser/ui/toasts/toast_controller.cc
index dc39e579..23c7c4c 100644
--- a/chrome/browser/ui/toasts/toast_controller.cc
+++ b/chrome/browser/ui/toasts/toast_controller.cc
@@ -4,6 +4,7 @@
#include "chrome/browser/ui/toasts/toast_controller.h"
+#include <memory>
#include <optional>
#include <string>
#include <utility>
@@ -17,6 +18,7 @@
#include "base/location.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
+#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
@@ -25,10 +27,13 @@
#include "chrome/browser/ui/toasts/api/toast_id.h"
#include "chrome/browser/ui/toasts/api/toast_registry.h"
#include "chrome/browser/ui/toasts/api/toast_specification.h"
+#include "chrome/browser/ui/toasts/toast_dismiss_menu_model.h"
#include "chrome/browser/ui/toasts/toast_features.h"
#include "chrome/browser/ui/toasts/toast_metrics.h"
#include "chrome/browser/ui/toasts/toast_view.h"
+#include "chrome/common/pref_names.h"
#include "components/omnibox/common/omnibox_focus_state.h"
+#include "components/prefs/pref_service.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
@@ -65,8 +70,20 @@
return GetCurrentToastId().has_value();
}
-bool ToastController::CanShowToast(ToastId id) const {
- return base::FeatureList::IsEnabled(toast_features::kToastFramework);
+bool ToastController::CanShowToast(ToastId toast_id) const {
+ if (!base::FeatureList::IsEnabled(toast_features::kToastFramework)) {
+ return false;
+ }
+ if (base::FeatureList::IsEnabled(toast_features::kToastRefinements) &&
+ static_cast<toasts::ToastAlertLevel>(
+ g_browser_process->local_state()->GetInteger(
+ prefs::kToastAlertLevel)) ==
+ toasts::ToastAlertLevel::kActionable) {
+ const ToastSpecification* toast_spec =
+ toast_registry_->GetToastSpecification(toast_id);
+ return toast_spec->has_close_button() || toast_spec->has_menu();
+ }
+ return true;
}
std::optional<ToastId> ToastController::GetCurrentToastId() const {
@@ -75,6 +92,7 @@
bool ToastController::MaybeShowToast(ToastParams params) {
if (!CanShowToast(params.toast_id)) {
+ RecordToastFailedToShow(params.toast_id);
return false;
}
@@ -266,6 +284,10 @@
if (params.menu_model) {
toast_view->AddMenu(std::move(params.menu_model));
+ } else if (base::FeatureList::IsEnabled(toast_features::kToastRefinements) &&
+ !spec->has_close_button()) {
+ toast_view->AddMenu(
+ std::make_unique<ToastDismissMenuModel>(params.toast_id));
}
toast_view_ = toast_view.get();
diff --git a/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc b/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc
index 9bbc23b..258708e 100644
--- a/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc
+++ b/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc
@@ -11,6 +11,7 @@
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window.h"
@@ -21,11 +22,14 @@
#include "chrome/browser/ui/omnibox/omnibox_tab_helper.h"
#include "chrome/browser/ui/toasts/api/toast_id.h"
#include "chrome/browser/ui/toasts/toast_controller.h"
+#include "chrome/browser/ui/toasts/toast_dismiss_menu_model.h"
#include "chrome/browser/ui/toasts/toast_features.h"
+#include "chrome/browser/ui/toasts/toast_metrics.h"
#include "chrome/browser/ui/toasts/toast_view.h"
#include "chrome/browser/ui/views/frame/app_menu_button.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/star_view.h"
+#include "chrome/common/pref_names.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
@@ -112,8 +116,9 @@
public:
void SetUp() override {
feature_list_.InitWithFeatures(
- {toast_features::kToastFramework, toast_features::kLinkCopiedToast,
- toast_features::kImageCopiedToast, toast_features::kReadingListToast,
+ {toast_features::kToastFramework, toast_features::kToastRefinements,
+ toast_features::kLinkCopiedToast, toast_features::kImageCopiedToast,
+ toast_features::kReadingListToast,
plus_addresses::features::kPlusAddressesEnabled,
plus_addresses::features::kPlusAddressFullFormFill},
{});
@@ -375,6 +380,33 @@
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
+ DismissingToastPermanently) {
+ RunTestSequence(
+ ShowToast(ToastParams(ToastId::kLinkCopied)),
+ WaitForShow(toasts::ToastView::kToastViewId),
+ EnsurePresent(toasts::ToastView::kToastMenuButton),
+ PressButton(toasts::ToastView::kToastMenuButton),
+ WaitForShow(ToastDismissMenuModel::kToastDontShowAgainMenuItem),
+ SelectMenuItem(ToastDismissMenuModel::kToastDontShowAgainMenuItem),
+ WaitForHide(toasts::ToastView::kToastViewId),
+ ShowToast(ToastParams(ToastId::kLinkCopied)),
+ EnsureNotPresent(toasts::ToastView::kToastViewId));
+}
+
+IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
+ DismissingToastTemporarily) {
+ RunTestSequence(ShowToast(ToastParams(ToastId::kLinkCopied)),
+ WaitForShow(toasts::ToastView::kToastViewId),
+ EnsurePresent(toasts::ToastView::kToastMenuButton),
+ PressButton(toasts::ToastView::kToastMenuButton),
+ WaitForShow(ToastDismissMenuModel::kToastDismissMenuItem),
+ SelectMenuItem(ToastDismissMenuModel::kToastDismissMenuItem),
+ WaitForHide(toasts::ToastView::kToastViewId),
+ ShowToast(ToastParams(ToastId::kLinkCopied)),
+ WaitForShow(toasts::ToastView::kToastViewId));
+}
+
+IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
ToastReactToOmniboxFocus) {
LocationBar* const location_bar = browser()->window()->GetLocationBar();
ASSERT_TRUE(location_bar);
diff --git a/chrome/browser/ui/toasts/toast_controller_unittest.cc b/chrome/browser/ui/toasts/toast_controller_unittest.cc
index 5c0511ab..19502e74 100644
--- a/chrome/browser/ui/toasts/toast_controller_unittest.cc
+++ b/chrome/browser/ui/toasts/toast_controller_unittest.cc
@@ -6,6 +6,7 @@
#include <memory>
+#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
@@ -13,7 +14,12 @@
#include "chrome/browser/ui/toasts/api/toast_registry.h"
#include "chrome/browser/ui/toasts/api/toast_specification.h"
#include "chrome/browser/ui/toasts/toast_features.h"
+#include "chrome/browser/ui/toasts/toast_metrics.h"
#include "chrome/browser/ui/toasts/toast_view.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "components/prefs/pref_service.h"
#include "components/vector_icons/vector_icons.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -170,3 +176,46 @@
task_environment().FastForwardBy(toast_features::kToastTimeout.Get() / 2);
EXPECT_TRUE(controller->IsShowingToast());
}
+
+class ToastControllerWithRefinementsUnitTest : public testing::Test {
+ public:
+ void SetUp() override {
+ feature_list_.InitAndEnableFeatureWithParameters(
+ toast_features::kToastRefinements, {});
+ toast_registry_ = std::make_unique<ToastRegistry>();
+ }
+
+ ToastRegistry* toast_registry() { return toast_registry_.get(); }
+ TestingPrefServiceSimple* local_state() { return local_state_.Get(); }
+ base::HistogramTester* histogram() { return &histogram_; }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ base::test::SingleThreadTaskEnvironment task_environment_{
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+ std::unique_ptr<ToastRegistry> toast_registry_;
+ base::HistogramTester histogram_;
+ ScopedTestingLocalState local_state_{TestingBrowserProcess::GetGlobal()};
+};
+
+TEST_F(ToastControllerWithRefinementsUnitTest, DoesNotShowToastWhenDisabled) {
+ ToastRegistry* const registry = toast_registry();
+ registry->RegisterToast(
+ ToastId::kLinkCopied,
+ ToastSpecification::Builder(vector_icons::kEmailIcon, 0).Build());
+
+ auto controller = std::make_unique<TestToastController>(registry);
+
+ local_state()->SetInteger(
+ prefs::kToastAlertLevel,
+ static_cast<int>(toasts::ToastAlertLevel::kActionable));
+ EXPECT_FALSE(controller->CanShowToast(ToastId::kLinkCopied));
+ EXPECT_FALSE(controller->MaybeShowToast(ToastParams(ToastId::kLinkCopied)));
+
+ histogram()->ExpectBucketCount("Toast.FailedToShow", ToastId::kLinkCopied, 1);
+
+ local_state()->SetInteger(prefs::kToastAlertLevel,
+ static_cast<int>(toasts::ToastAlertLevel::kAll));
+ EXPECT_TRUE(controller->CanShowToast(ToastId::kLinkCopied));
+ EXPECT_TRUE(controller->MaybeShowToast(ToastParams(ToastId::kLinkCopied)));
+}
diff --git a/chrome/browser/ui/toasts/toast_dismiss_menu_model.cc b/chrome/browser/ui/toasts/toast_dismiss_menu_model.cc
new file mode 100644
index 0000000..ebea2b42
--- /dev/null
+++ b/chrome/browser/ui/toasts/toast_dismiss_menu_model.cc
@@ -0,0 +1,49 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/toasts/toast_dismiss_menu_model.h"
+
+#include "base/notreached.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/ui/toasts/toast_metrics.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/prefs/pref_service.h"
+#include "ui/base/interaction/element_identifier.h"
+#include "ui/base/l10n/l10n_util.h"
+
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastDismissMenuModel,
+ kToastDismissMenuItem);
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastDismissMenuModel,
+ kToastDontShowAgainMenuItem);
+
+ToastDismissMenuModel::ToastDismissMenuModel(ToastId toast_id)
+ : ui::SimpleMenuModel(/*delegate=*/this), toast_id_(toast_id) {
+ AddItem(static_cast<int>(toasts::ToastDismissMenuEntries::kDismiss),
+ l10n_util::GetStringUTF16(IDS_TOAST_MENU_ITEM_DISMISS));
+ SetElementIdentifierAt(0, kToastDismissMenuItem);
+ AddItem(static_cast<int>(toasts::ToastDismissMenuEntries::kDontShowAgain),
+ l10n_util::GetStringUTF16(IDS_TOAST_MENU_ITEM_DONT_SHOW_AGAIN));
+ SetElementIdentifierAt(1, kToastDontShowAgainMenuItem);
+}
+
+ToastDismissMenuModel::~ToastDismissMenuModel() = default;
+
+void ToastDismissMenuModel::ExecuteCommand(int command_id, int event_flags) {
+ const toasts::ToastDismissMenuEntries command =
+ static_cast<toasts::ToastDismissMenuEntries>(command_id);
+ RecordToastDismissMenuClicked(toast_id_, command);
+
+ switch (command) {
+ case toasts::ToastDismissMenuEntries::kDismiss:
+ // Toast will dismiss automatically when a menu item is clicked.
+ return;
+ case toasts::ToastDismissMenuEntries::kDontShowAgain:
+ g_browser_process->local_state()->SetInteger(
+ prefs::kToastAlertLevel,
+ static_cast<int>(toasts::ToastAlertLevel::kActionable));
+ return;
+ }
+ NOTREACHED();
+}
diff --git a/chrome/browser/ui/toasts/toast_dismiss_menu_model.h b/chrome/browser/ui/toasts/toast_dismiss_menu_model.h
new file mode 100644
index 0000000..b8fe585
--- /dev/null
+++ b/chrome/browser/ui/toasts/toast_dismiss_menu_model.h
@@ -0,0 +1,28 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_TOASTS_TOAST_DISMISS_MENU_MODEL_H_
+#define CHROME_BROWSER_UI_TOASTS_TOAST_DISMISS_MENU_MODEL_H_
+
+#include "chrome/browser/ui/toasts/api/toast_id.h"
+#include "ui/base/interaction/element_identifier.h"
+#include "ui/menus/simple_menu_model.h"
+
+class ToastDismissMenuModel : public ui::SimpleMenuModel,
+ public ui::SimpleMenuModel::Delegate {
+ public:
+ DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kToastDismissMenuItem);
+ DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kToastDontShowAgainMenuItem);
+
+ explicit ToastDismissMenuModel(ToastId toast_id);
+ ~ToastDismissMenuModel() override;
+
+ // ui::SimpleMenuModel::Delegate:
+ void ExecuteCommand(int command_id, int event_flags) override;
+
+ private:
+ const ToastId toast_id_;
+};
+
+#endif // CHROME_BROWSER_UI_TOASTS_TOAST_DISMISS_MENU_MODEL_H_
diff --git a/chrome/browser/ui/toasts/toast_features.cc b/chrome/browser/ui/toasts/toast_features.cc
index bff63b4c..acb5e97 100644
--- a/chrome/browser/ui/toasts/toast_features.cc
+++ b/chrome/browser/ui/toasts/toast_features.cc
@@ -16,6 +16,12 @@
"ToastFramework",
base::FEATURE_ENABLED_BY_DEFAULT);
+// Enables refinements of the toast framework that allow for controlling the
+// visibility of non-actionable toasts.
+BASE_FEATURE(kToastRefinements,
+ "ToastRefinements",
+ base::FEATURE_DISABLED_BY_DEFAULT);
+
const base::FeatureParam<bool> kToastDemoMode{&kToastFramework,
"toast_demo_mode", false};
diff --git a/chrome/browser/ui/toasts/toast_features.h b/chrome/browser/ui/toasts/toast_features.h
index 628d382..5902d1b 100644
--- a/chrome/browser/ui/toasts/toast_features.h
+++ b/chrome/browser/ui/toasts/toast_features.h
@@ -14,6 +14,8 @@
// Base feature
BASE_DECLARE_FEATURE(kToastFramework);
+BASE_DECLARE_FEATURE(kToastRefinements);
+
// Enables all toast features queried through `toast_features::IsEnabled` which
// is used for demo mode.
extern const base::FeatureParam<bool> kToastDemoMode;
diff --git a/chrome/browser/ui/toasts/toast_metrics.cc b/chrome/browser/ui/toasts/toast_metrics.cc
index 88fe034..af5b23efd 100644
--- a/chrome/browser/ui/toasts/toast_metrics.cc
+++ b/chrome/browser/ui/toasts/toast_metrics.cc
@@ -14,6 +14,10 @@
base::UmaHistogramEnumeration("Toast.TriggeredToShow", toast_id);
}
+void RecordToastFailedToShow(ToastId toast_id) {
+ base::UmaHistogramEnumeration("Toast.FailedToShow", toast_id);
+}
+
void RecordToastActionButtonClicked(ToastId toast_id) {
base::RecordComputedAction(
base::StrCat({"Toast.ActionButtonClicked.", GetToastName(toast_id)}));
@@ -30,3 +34,10 @@
base::StrCat({"Toast.", GetToastName(toast_id), ".Dismissed"}),
close_reason);
}
+
+void RecordToastDismissMenuClicked(ToastId toast_id,
+ toasts::ToastDismissMenuEntries command_id) {
+ base::UmaHistogramEnumeration(
+ base::StrCat({"Toast.", GetToastName(toast_id), ".DismissMenuClicked"}),
+ command_id);
+}
diff --git a/chrome/browser/ui/toasts/toast_metrics.h b/chrome/browser/ui/toasts/toast_metrics.h
index a2a151a2..b3bef94 100644
--- a/chrome/browser/ui/toasts/toast_metrics.h
+++ b/chrome/browser/ui/toasts/toast_metrics.h
@@ -9,10 +9,30 @@
namespace toasts {
enum class ToastCloseReason;
-}
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class ToastAlertLevel {
+ kAll = 0,
+ kActionable = 1,
+ kMaxValue = kActionable
+};
+
+// LINT.IfChange(ToastDismissMenuEntries)
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class ToastDismissMenuEntries {
+ kDismiss = 0,
+ kDontShowAgain = 1,
+ kMaxValue = kDontShowAgain
+};
+// LINT.ThenChange(/tools/metrics/histograms/metadata/toasts/enums.xml:ToastDismissMenuEntries)
+} // namespace toasts
void RecordToastTriggeredToShow(ToastId toast_id);
+void RecordToastFailedToShow(ToastId toast_id);
+
void RecordToastActionButtonClicked(ToastId toast_id);
void RecordToastCloseButtonClicked(ToastId toast_id);
@@ -20,4 +40,7 @@
void RecordToastDismissReason(ToastId toast_id,
toasts::ToastCloseReason close_reason);
+void RecordToastDismissMenuClicked(ToastId toast_id,
+ toasts::ToastDismissMenuEntries command_id);
+
#endif // CHROME_BROWSER_UI_TOASTS_TOAST_METRICS_H_