Commit b8aec552 authored by Kristi Park's avatar Kristi Park Committed by Commit Bot

[NTP] Add ability to toggle between Most Visited and custom links

Implement toggleMostVisitedOrCustomLinks for the EmbbededSearchApi.
This will allow users to toggle between Most Visited tiles and custom
links.

Bug: 953822
Change-Id: Ide2d1a62456649a5b6f9ff45d16548ac1212e0b5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1572233Reviewed-by: default avatarKyle Milka <kmilka@chromium.org>
Reviewed-by: default avatarMarc Treib <treib@chromium.org>
Reviewed-by: default avatarGreg Kerr <kerrnel@chromium.org>
Commit-Queue: Kristi Park <kristipark@chromium.org>
Cr-Commit-Position: refs/heads/master@{#652298}
parent faf13c7a
......@@ -8,6 +8,7 @@
#include <string>
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/scoped_observer.h"
......@@ -120,7 +121,7 @@ class InstantService::SearchProviderObserver
: public TemplateURLServiceObserver {
public:
explicit SearchProviderObserver(TemplateURLService* service,
base::RepeatingCallback<void(bool)> callback)
base::RepeatingClosure callback)
: service_(service),
is_google_(search::DefaultSearchProviderIsGoogle(service_)),
callback_(std::move(callback)) {
......@@ -138,7 +139,7 @@ class InstantService::SearchProviderObserver
private:
void OnTemplateURLServiceChanged() override {
is_google_ = search::DefaultSearchProviderIsGoogle(service_);
callback_.Run(is_google_);
callback_.Run();
}
void OnTemplateURLServiceShuttingDown() override {
......@@ -148,7 +149,7 @@ class InstantService::SearchProviderObserver
TemplateURLService* service_;
bool is_google_;
base::RepeatingCallback<void(bool)> callback_;
base::RepeatingClosure callback_;
};
InstantService::InstantService(Profile* profile)
......@@ -173,8 +174,6 @@ InstantService::InstantService(Profile* profile)
most_visited_sites_ = ChromeMostVisitedSitesFactory::NewForProfile(profile_);
if (most_visited_sites_) {
bool custom_links_enabled = true;
// Determine if we are using a third-party NTP. Custom links should only be
// enabled for the default NTP.
TemplateURLService* template_url_service =
......@@ -184,13 +183,12 @@ InstantService::InstantService(Profile* profile)
template_url_service,
base::BindRepeating(&InstantService::OnSearchProviderChanged,
weak_ptr_factory_.GetWeakPtr()));
custom_links_enabled = search_provider_observer_->is_google();
}
// 9 tiles are required for the custom links feature in order to balance the
// Most Visited rows (this is due to an additional "Add" button).
most_visited_sites_->SetMostVisitedURLsObserver(this, 9);
most_visited_sites_->EnableCustomLinks(custom_links_enabled);
most_visited_sites_->EnableCustomLinks(IsCustomLinksEnabled());
}
if (profile_) {
......@@ -282,50 +280,58 @@ void InstantService::UndoAllMostVisitedDeletions() {
}
}
bool InstantService::AddCustomLink(const GURL& url, const std::string& title) {
if (most_visited_sites_)
return most_visited_sites_->AddCustomLink(url, base::UTF8ToUTF16(title));
bool InstantService::ToggleMostVisitedOrCustomLinks() {
// Non-Google NTPs are not supported.
if (!most_visited_sites_ || !search_provider_observer_ ||
!search_provider_observer_->is_google()) {
return false;
}
bool use_most_visited =
pref_service_->GetBoolean(prefs::kNtpUseMostVisitedTiles);
pref_service_->SetBoolean(prefs::kNtpUseMostVisitedTiles, !use_most_visited);
most_visited_sites_->EnableCustomLinks(IsCustomLinksEnabled());
return true;
}
bool InstantService::AddCustomLink(const GURL& url, const std::string& title) {
return most_visited_sites_ &&
most_visited_sites_->AddCustomLink(url, base::UTF8ToUTF16(title));
}
bool InstantService::UpdateCustomLink(const GURL& url,
const GURL& new_url,
const std::string& new_title) {
if (most_visited_sites_) {
return most_visited_sites_->UpdateCustomLink(url, new_url,
base::UTF8ToUTF16(new_title));
}
return false;
return most_visited_sites_ && most_visited_sites_->UpdateCustomLink(
url, new_url, base::UTF8ToUTF16(new_title));
}
bool InstantService::ReorderCustomLink(const GURL& url, int new_pos) {
if (most_visited_sites_)
return most_visited_sites_->ReorderCustomLink(url, new_pos);
return false;
return most_visited_sites_ &&
most_visited_sites_->ReorderCustomLink(url, new_pos);
}
bool InstantService::DeleteCustomLink(const GURL& url) {
if (most_visited_sites_)
return most_visited_sites_->DeleteCustomLink(url);
return false;
return most_visited_sites_ && most_visited_sites_->DeleteCustomLink(url);
}
bool InstantService::UndoCustomLinkAction() {
// Non-Google search providers are not supported.
if (most_visited_sites_ && search_provider_observer_->is_google()) {
// Non-Google NTPs are not supported.
if (!most_visited_sites_ || !search_provider_observer_ ||
!search_provider_observer_->is_google()) {
return false;
}
most_visited_sites_->UndoCustomLinkAction();
return true;
}
return false;
}
bool InstantService::ResetCustomLinks() {
// Non-Google search providers are not supported.
if (most_visited_sites_ && search_provider_observer_->is_google()) {
// Non-Google NTPs are not supported.
if (!most_visited_sites_ || !search_provider_observer_ ||
!search_provider_observer_->is_google()) {
return false;
}
most_visited_sites_->UninitializeCustomLinks();
return true;
}
return false;
}
void InstantService::UpdateThemeInfo() {
......@@ -472,9 +478,9 @@ void InstantService::OnRendererProcessTerminated(int process_id) {
}
}
void InstantService::OnSearchProviderChanged(bool is_google) {
void InstantService::OnSearchProviderChanged() {
DCHECK(most_visited_sites_);
most_visited_sites_->EnableCustomLinks(is_google);
most_visited_sites_->EnableCustomLinks(IsCustomLinksEnabled());
}
void InstantService::OnDarkModeChanged(bool dark_mode) {
......@@ -519,6 +525,11 @@ void InstantService::NotifyAboutThemeInfo() {
observer.ThemeInfoChanged(*theme_info_);
}
bool InstantService::IsCustomLinksEnabled() {
return search_provider_observer_ && search_provider_observer_->is_google() &&
!pref_service_->GetBoolean(prefs::kNtpUseMostVisitedTiles);
}
namespace {
const int kSectionBorderAlphaTransparency = 80;
......@@ -777,4 +788,5 @@ void InstantService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(prefs::kNtpCustomBackgroundLocalToDevice,
false);
registry->RegisterBooleanPref(prefs::kNtpUseMostVisitedTiles, false);
}
......@@ -85,6 +85,10 @@ class InstantService : public KeyedService,
void UndoMostVisitedDeletion(const GURL& url);
// Invoked when the Instant page wants to undo all Most Visited deletions.
void UndoAllMostVisitedDeletions();
// Invoked when the Instant page wants to switch between custom links and Most
// Visited. Toggles between the two options each time it's called. Returns
// false and does nothing if the profile is using a third-party NTP.
bool ToggleMostVisitedOrCustomLinks();
// Invoked when the Instant page wants to add a custom link.
bool AddCustomLink(const GURL& url, const std::string& title);
// Invoked when the Instant page wants to update a custom link.
......@@ -96,12 +100,12 @@ class InstantService : public KeyedService,
// Invoked when the Instant page wants to delete a custom link.
bool DeleteCustomLink(const GURL& url);
// Invoked when the Instant page wants to undo the previous custom link
// action. Returns false and does nothing if the profile is using a non-Google
// search provider.
// action. Returns false and does nothing if the profile is using a third-
// party NTP.
bool UndoCustomLinkAction();
// Invoked when the Instant page wants to delete all custom links and use Most
// Visited sites instead. Returns false and does nothing if the profile is
// using a non-Google search provider. Marked virtual for mocking in tests.
// using a third-party NTP. Marked virtual for mocking in tests.
virtual bool ResetCustomLinks();
// Invoked to update theme information for the NTP.
......@@ -156,6 +160,7 @@ class InstantService : public KeyedService,
FRIEND_TEST_ALL_PREFIXES(InstantExtendedTest, ProcessIsolation);
FRIEND_TEST_ALL_PREFIXES(InstantServiceTest, DeleteThumbnailDataIfExists);
FRIEND_TEST_ALL_PREFIXES(InstantServiceTest, GetNTPTileSuggestion);
FRIEND_TEST_ALL_PREFIXES(InstantServiceTest, IsCustomLinksEnabled);
FRIEND_TEST_ALL_PREFIXES(InstantServiceTest, TestNoThemeInfo);
// KeyedService:
......@@ -171,7 +176,7 @@ class InstantService : public KeyedService,
// Called when the search provider changes. Disables custom links if the
// search provider is not Google.
void OnSearchProviderChanged(bool is_google);
void OnSearchProviderChanged();
// Called when dark mode changes. Updates current theme info as necessary and
// notifies that the theme has changed.
......@@ -186,6 +191,10 @@ class InstantService : public KeyedService,
void NotifyAboutMostVisitedItems();
void NotifyAboutThemeInfo();
// Returns true if this is a Google NTP and the user has chosen to show custom
// links.
bool IsCustomLinksEnabled();
void BuildThemeInfo();
void ApplyOrResetCustomBackgroundThemeInfo();
......
......@@ -106,6 +106,26 @@ TEST_F(InstantServiceTest, DeleteThumbnailDataIfExists) {
EXPECT_FALSE(base::PathExists(database_dir));
}
TEST_F(InstantServiceTest, DoesToggleMostVisitedOrCustomLinks) {
sync_preferences::TestingPrefServiceSyncable* pref_service =
profile()->GetTestingPrefService();
SetUserSelectedDefaultSearchProvider("{google:baseURL}");
ASSERT_FALSE(pref_service->GetBoolean(prefs::kNtpUseMostVisitedTiles));
// Enable most visited tiles.
EXPECT_TRUE(instant_service_->ToggleMostVisitedOrCustomLinks());
EXPECT_TRUE(pref_service->GetBoolean(prefs::kNtpUseMostVisitedTiles));
// Disable most visited tiles.
EXPECT_TRUE(instant_service_->ToggleMostVisitedOrCustomLinks());
EXPECT_FALSE(pref_service->GetBoolean(prefs::kNtpUseMostVisitedTiles));
// Should do nothing if this is a non-Google NTP.
SetUserSelectedDefaultSearchProvider("https://www.search.com");
EXPECT_FALSE(instant_service_->ToggleMostVisitedOrCustomLinks());
EXPECT_FALSE(pref_service->GetBoolean(prefs::kNtpUseMostVisitedTiles));
}
TEST_F(InstantServiceTest,
DisableUndoCustomLinkActionForNonGoogleSearchProvider) {
SetUserSelectedDefaultSearchProvider("{google:baseURL}");
......@@ -123,6 +143,25 @@ TEST_F(InstantServiceTest, DisableResetCustomLinksForNonGoogleSearchProvider) {
EXPECT_FALSE(instant_service_->ResetCustomLinks());
}
TEST_F(InstantServiceTest, IsCustomLinksEnabled) {
sync_preferences::TestingPrefServiceSyncable* pref_service =
profile()->GetTestingPrefService();
// Test that custom links are only enabled when Most Visited is toggled off
// and this is a Google NTP.
pref_service->SetBoolean(prefs::kNtpUseMostVisitedTiles, false);
SetUserSelectedDefaultSearchProvider("{google:baseURL}");
EXPECT_TRUE(instant_service_->IsCustomLinksEnabled());
// All other cases should return false.
SetUserSelectedDefaultSearchProvider("https://www.search.com");
EXPECT_FALSE(instant_service_->IsCustomLinksEnabled());
pref_service->SetBoolean(prefs::kNtpUseMostVisitedTiles, true);
EXPECT_FALSE(instant_service_->IsCustomLinksEnabled());
SetUserSelectedDefaultSearchProvider("{google:baseURL}");
EXPECT_FALSE(instant_service_->IsCustomLinksEnabled());
}
TEST_F(InstantServiceTest, SetCustomBackgroundURL) {
ASSERT_FALSE(instant_service_->IsCustomBackgroundSet());
const GURL kUrl("https://www.foo.com");
......
......@@ -189,6 +189,16 @@ void SearchIPCRouter::UndoAllMostVisitedDeletions(int page_seq_no) {
delegate_->OnUndoAllMostVisitedDeletions();
}
void SearchIPCRouter::ToggleMostVisitedOrCustomLinks(int page_seq_no) {
if (page_seq_no != commit_counter_)
return;
if (!policy_->ShouldProcessToggleMostVisitedOrCustomLinks())
return;
delegate_->OnToggleMostVisitedOrCustomLinks();
}
void SearchIPCRouter::AddCustomLink(int page_seq_no,
const GURL& url,
const std::string& title,
......
......@@ -54,6 +54,10 @@ class SearchIPCRouter : public content::WebContentsObserver,
// Called when the EmbeddedSearch wants to undo all Most Visited deletions.
virtual void OnUndoAllMostVisitedDeletions() = 0;
// Called when the EmbeddedSearch wants to switch between custom links and
// Most Visited.
virtual void OnToggleMostVisitedOrCustomLinks() = 0;
// Called when the EmbeddedSearch wants to add a custom link.
virtual bool OnAddCustomLink(const GURL& url, const std::string& title) = 0;
......@@ -152,6 +156,7 @@ class SearchIPCRouter : public content::WebContentsObserver,
virtual bool ShouldProcessDeleteMostVisitedItem() = 0;
virtual bool ShouldProcessUndoMostVisitedDeletion() = 0;
virtual bool ShouldProcessUndoAllMostVisitedDeletions() = 0;
virtual bool ShouldProcessToggleMostVisitedOrCustomLinks() = 0;
virtual bool ShouldProcessAddCustomLink() = 0;
virtual bool ShouldProcessUpdateCustomLink() = 0;
virtual bool ShouldProcessReorderCustomLink() = 0;
......@@ -221,6 +226,7 @@ class SearchIPCRouter : public content::WebContentsObserver,
void DeleteMostVisitedItem(int page_seq_no, const GURL& url) override;
void UndoMostVisitedDeletion(int page_seq_no, const GURL& url) override;
void UndoAllMostVisitedDeletions(int page_seq_no) override;
void ToggleMostVisitedOrCustomLinks(int page_seq_no) override;
void AddCustomLink(int page_seq_no,
const GURL& url,
const std::string& title,
......
......@@ -37,6 +37,10 @@ bool SearchIPCRouterPolicyImpl::ShouldProcessUndoAllMostVisitedDeletions() {
return !is_incognito_ && search::IsInstantNTP(web_contents_);
}
bool SearchIPCRouterPolicyImpl::ShouldProcessToggleMostVisitedOrCustomLinks() {
return !is_incognito_ && search::IsInstantNTP(web_contents_);
}
bool SearchIPCRouterPolicyImpl::ShouldProcessAddCustomLink() {
return !is_incognito_ && search::IsInstantNTP(web_contents_);
}
......
......@@ -31,6 +31,7 @@ class SearchIPCRouterPolicyImpl : public SearchIPCRouter::Policy {
bool ShouldProcessDeleteMostVisitedItem() override;
bool ShouldProcessUndoMostVisitedDeletion() override;
bool ShouldProcessUndoAllMostVisitedDeletions() override;
bool ShouldProcessToggleMostVisitedOrCustomLinks() override;
bool ShouldProcessAddCustomLink() override;
bool ShouldProcessUpdateCustomLink() override;
bool ShouldProcessReorderCustomLink() override;
......
......@@ -61,6 +61,7 @@ class MockSearchIPCRouterDelegate : public SearchIPCRouter::Delegate {
MOCK_METHOD1(OnDeleteMostVisitedItem, void(const GURL& url));
MOCK_METHOD1(OnUndoMostVisitedDeletion, void(const GURL& url));
MOCK_METHOD0(OnUndoAllMostVisitedDeletions, void());
MOCK_METHOD0(OnToggleMostVisitedOrCustomLinks, void());
MOCK_METHOD2(OnAddCustomLink,
bool(const GURL& url, const std::string& title));
MOCK_METHOD3(OnUpdateCustomLink,
......@@ -104,6 +105,7 @@ class MockSearchIPCRouterPolicy : public SearchIPCRouter::Policy {
MOCK_METHOD0(ShouldProcessDeleteMostVisitedItem, bool());
MOCK_METHOD0(ShouldProcessUndoMostVisitedDeletion, bool());
MOCK_METHOD0(ShouldProcessUndoAllMostVisitedDeletions, bool());
MOCK_METHOD0(ShouldProcessToggleMostVisitedOrCustomLinks, bool());
MOCK_METHOD0(ShouldProcessAddCustomLink, bool());
MOCK_METHOD0(ShouldProcessUpdateCustomLink, bool());
MOCK_METHOD0(ShouldProcessReorderCustomLink, bool());
......@@ -488,6 +490,32 @@ TEST_F(SearchIPCRouterTest, IgnoreUndoAllMostVisitedDeletionsMsg) {
GetSearchIPCRouter().UndoAllMostVisitedDeletions(GetSearchIPCRouterSeqNo());
}
TEST_F(SearchIPCRouterTest, ProcessToggleMostVisitedOrCustomLinksMsg) {
NavigateAndCommitActiveTab(GURL("chrome-search://foo/bar"));
SetupMockDelegateAndPolicy();
MockSearchIPCRouterPolicy* policy = GetSearchIPCRouterPolicy();
EXPECT_CALL(*mock_delegate(), OnToggleMostVisitedOrCustomLinks()).Times(1);
EXPECT_CALL(*policy, ShouldProcessToggleMostVisitedOrCustomLinks())
.Times(1)
.WillOnce(Return(true));
GetSearchIPCRouter().ToggleMostVisitedOrCustomLinks(
GetSearchIPCRouterSeqNo());
}
TEST_F(SearchIPCRouterTest, IgnoreToggleMostVisitedOrCustomLinksMsg) {
NavigateAndCommitActiveTab(GURL("chrome-search://foo/bar"));
SetupMockDelegateAndPolicy();
MockSearchIPCRouterPolicy* policy = GetSearchIPCRouterPolicy();
EXPECT_CALL(*mock_delegate(), OnToggleMostVisitedOrCustomLinks()).Times(0);
EXPECT_CALL(*policy, ShouldProcessToggleMostVisitedOrCustomLinks())
.Times(1)
.WillOnce(Return(false));
GetSearchIPCRouter().ToggleMostVisitedOrCustomLinks(
GetSearchIPCRouterSeqNo());
}
TEST_F(SearchIPCRouterTest, ProcessAddCustomLinkMsg) {
NavigateAndCommitActiveTab(GURL("chrome-search://foo/bar"));
SetupMockDelegateAndPolicy();
......
......@@ -293,6 +293,11 @@ void SearchTabHelper::OnUndoAllMostVisitedDeletions() {
instant_service_->UndoAllMostVisitedDeletions();
}
void SearchTabHelper::OnToggleMostVisitedOrCustomLinks() {
if (instant_service_)
instant_service_->ToggleMostVisitedOrCustomLinks();
}
bool SearchTabHelper::OnAddCustomLink(const GURL& url,
const std::string& title) {
DCHECK(!url.is_empty());
......
......@@ -101,6 +101,7 @@ class SearchTabHelper : public content::WebContentsObserver,
void OnDeleteMostVisitedItem(const GURL& url) override;
void OnUndoMostVisitedDeletion(const GURL& url) override;
void OnUndoAllMostVisitedDeletions() override;
void OnToggleMostVisitedOrCustomLinks() override;
bool OnAddCustomLink(const GURL& url, const std::string& title) override;
bool OnUpdateCustomLink(const GURL& url,
const GURL& new_url,
......
......@@ -1560,6 +1560,9 @@ const char kNtpActivateHideShortcutsFieldTrial[] =
const char kNtpCustomBackgroundDict[] = "ntp.custom_background_dict";
const char kNtpCustomBackgroundLocalToDevice[] =
"ntp.custom_background_local_to_device";
// Tracks whether the user has chosen to use custom links or most visited sites
// for the shortcut tiles on the NTP.
const char kNtpUseMostVisitedTiles[] = "ntp.use_most_visited_tiles";
// Data associated with search suggestions that appear on the NTP.
const char kNtpSearchSuggestionsBlocklist[] =
......
......@@ -538,6 +538,7 @@ extern const char kContentSuggestionsNotificationsSentCount[];
extern const char kNtpActivateHideShortcutsFieldTrial[];
extern const char kNtpCustomBackgroundDict[];
extern const char kNtpCustomBackgroundLocalToDevice[];
extern const char kNtpUseMostVisitedTiles[];
extern const char kNtpSearchSuggestionsBlocklist[];
extern const char kNtpSearchSuggestionsImpressions[];
extern const char kNtpSearchSuggestionsOptOut[];
......
......@@ -45,6 +45,9 @@ interface EmbeddedSearch {
// Tells InstantExtended to undo one most visited item deletion.
UndoMostVisitedDeletion(int32 page_seq_no, url.mojom.Url url);
// Tells InstantExtended to toggle between most visited items or custom links.
ToggleMostVisitedOrCustomLinks(int32 page_seq_no);
// Tells InstantExtended to add a custom link. Returns true if successful.
AddCustomLink(int32 page_seq_no, url.mojom.Url url, string title)
=> (bool success);
......
......@@ -306,6 +306,10 @@ bool SearchBox::IsCustomLinks() const {
return is_custom_links_;
}
void SearchBox::ToggleMostVisitedOrCustomLinks() {
embedded_search_service_->ToggleMostVisitedOrCustomLinks(page_seq_no_);
}
void SearchBox::AddCustomLink(const GURL& url, const std::string& title) {
if (!url.is_valid()) {
AddCustomLinkResult(false);
......
......@@ -112,6 +112,9 @@ class SearchBox : public content::RenderFrameObserver,
// Returns true if the most visited items are custom links.
bool IsCustomLinks() const;
// Sends ToggleMostVisitedOrCustomLinks to the browser.
void ToggleMostVisitedOrCustomLinks();
// Sends AddCustomLink to the browser.
void AddCustomLink(const GURL& url, const std::string& title);
......
......@@ -600,6 +600,7 @@ class NewTabPageBindings : public gin::Wrappable<NewTabPageBindings> {
// custom links iframe, and/or the local NTP.
static v8::Local<v8::Value> GetMostVisitedItemData(v8::Isolate* isolate,
int rid);
static void ToggleMostVisitedOrCustomLinks();
static void UpdateCustomLink(int rid,
const std::string& url,
const std::string& title);
......@@ -667,6 +668,8 @@ gin::ObjectTemplateBuilder NewTabPageBindings::GetObjectTemplateBuilder(
&NewTabPageBindings::UndoMostVisitedDeletion)
.SetMethod("getMostVisitedItemData",
&NewTabPageBindings::GetMostVisitedItemData)
.SetMethod("toggleMostVisitedOrCustomLinks",
&NewTabPageBindings::ToggleMostVisitedOrCustomLinks)
.SetMethod("updateCustomLink", &NewTabPageBindings::UpdateCustomLink)
.SetMethod("reorderCustomLink", &NewTabPageBindings::ReorderCustomLink)
.SetMethod("undoCustomLinkAction",
......@@ -852,6 +855,14 @@ v8::Local<v8::Value> NewTabPageBindings::GetMostVisitedItemData(
return GenerateMostVisitedItemData(isolate, render_view_id, rid, item);
}
// static
void NewTabPageBindings::ToggleMostVisitedOrCustomLinks() {
SearchBox* search_box = GetSearchBoxForCurrentContext();
if (!search_box)
return;
search_box->ToggleMostVisitedOrCustomLinks();
}
// static
void NewTabPageBindings::UpdateCustomLink(int rid,
const std::string& url,
......
......@@ -171,7 +171,8 @@ class MostVisitedSites : public history::TopSitesObserver,
// otherwise.
bool IsCustomLinksInitialized();
// Enables or disables custom links, but does not (un)initialize them. Called
// when a third-party NTP is being used.
// when a third-party NTP is being used, or when the user switches between
// custom links and Most Visited sites.
void EnableCustomLinks(bool enable);
// Adds a custom link. If the number of current links is maxed, returns false
// and does nothing. Will initialize custom links if they have not been
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment