// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/reading_list/model/reading_list_browser_agent.h"
#import <MaterialComponents/MaterialSnackbar.h>
#import "base/i18n/message_formatter.h"
#import "base/location.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/reading_list/core/reading_list_model.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/sync/base/features.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/chrome/browser/ntp/shared/metrics/home_metrics.h"
#import "ios/chrome/browser/reading_list/model/reading_list_constants.h"
#import "ios/chrome/browser/reading_list/model/reading_list_model_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/snackbar_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/web_state.h"
#import "net/base/apple/url_conversions.h"
#import "services/metrics/public/cpp/ukm_builders.h"
#import "ui/base/l10n/l10n_util.h"
BROWSER_USER_DATA_KEY_IMPL(ReadingListBrowserAgent)
ReadingListBrowserAgent::ReadingListBrowserAgent(Browser* browser) {
browser_ = browser;
}
ReadingListBrowserAgent::~ReadingListBrowserAgent() {}
#pragma mark - Public
// Adds the given urls to the reading list.
void ReadingListBrowserAgent::AddURLsToReadingList(
NSArray<URLWithTitle*>* urls) {
DCHECK(urls.count > 0) << "Urls are missing";
for (URLWithTitle* url_with_title in urls) {
AddURLToReadingListwithTitle(url_with_title.URL, url_with_title.title);
}
TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeSuccess);
AccountInfo account_info =
GetAccountInfoFromLastAddedURL(urls.lastObject.URL);
NSString* snackbar_text = nil;
MDCSnackbarMessageAction* snackbar_action = nil;
if (!account_info.IsEmpty()) {
std::u16string pattern = l10n_util::GetStringUTF16(
IDS_IOS_READING_LIST_SNACKBAR_MESSAGE_FOR_ACCOUNT);
std::u16string utf16Text =
base::i18n::MessageFormatter::FormatWithNamedArgs(
pattern, "count", (int)urls.count, "email", account_info.email);
snackbar_text = base::SysUTF16ToNSString(utf16Text);
static_assert(syncer::IsReadingListAccountStorageEnabled());
snackbar_action = CreateUndoActionWithReadingListURLs(urls);
} else {
snackbar_text =
l10n_util::GetNSString(IDS_IOS_READING_LIST_SNACKBAR_MESSAGE);
}
MDCSnackbarMessage* message = CreateSnackbarMessage(snackbar_text);
message.accessibilityLabel = snackbar_text;
message.action = snackbar_action;
CommandDispatcher* dispatcher = browser_->GetCommandDispatcher();
id<SnackbarCommands> snackbar_commands_handler =
HandlerForProtocol(dispatcher, SnackbarCommands);
[snackbar_commands_handler showSnackbarMessage:message];
}
void ReadingListBrowserAgent::BulkAddURLsToReadingListWithViewSnackbar(
NSArray<NSURL*>* urls) {
DCHECK([urls count] > 0);
base::RecordAction(base::UserMetricsAction("IOSReadingListItemsAddedInBulk"));
ReadingListModel* reading_list_model =
ReadingListModelFactory::GetInstance()->GetForBrowserState(
browser_->GetBrowserState());
if (!reading_list_model->loaded()) {
return;
}
// Add reading list items and keep track of successful additions.
int successfully_added_reading_list_items = 0;
GURL last_valid_url;
for (NSURL* ns_url in urls) {
GURL url = net::GURLWithNSURL(ns_url);
if (!url.is_valid() || !reading_list_model->IsUrlSupported(url)) {
continue;
}
GURL::Replacements replacements;
replacements.ClearUsername();
replacements.ClearPassword();
replacements.ClearQuery();
replacements.ClearRef();
NSString* title =
base::SysUTF8ToNSString(url.ReplaceComponents(replacements).spec());
AddURLToReadingListwithTitle(url, title);
successfully_added_reading_list_items++;
last_valid_url = url;
}
base::UmaHistogramCounts100("IOS.ReadingList.BulkAddURLsCount",
successfully_added_reading_list_items);
NSString* result;
if (successfully_added_reading_list_items > 0 &&
!GetAccountInfoFromLastAddedURL(last_valid_url).IsEmpty()) {
result = base::SysUTF16ToNSString(
base::i18n::MessageFormatter::FormatWithNamedArgs(
l10n_util::GetStringUTF16(
IDS_IOS_READING_LIST_SNACKBAR_MESSAGE_FOR_ACCOUNT_WITH_COUNT),
"count", successfully_added_reading_list_items, "email",
GetAccountInfoFromLastAddedURL(last_valid_url).email));
} else {
result = base::SysUTF16ToNSString(
base::i18n::MessageFormatter::FormatWithNamedArgs(
l10n_util::GetStringUTF16(
IDS_IOS_READING_LIST_SNACKBAR_MESSAGE_NO_ACCOUNT_WITH_COUNT),
"count", successfully_added_reading_list_items));
}
// Create and show snackbar message.
MDCSnackbarMessageAction* action = CreateViewAction();
TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeSuccess);
MDCSnackbarMessage* message = CreateSnackbarMessage(result);
message.action = action;
CommandDispatcher* dispatcher = browser_->GetCommandDispatcher();
id<SnackbarCommands> snackbar_commands_handler =
HandlerForProtocol(dispatcher, SnackbarCommands);
[snackbar_commands_handler showSnackbarMessage:message];
}
#pragma mark - Private
AccountInfo ReadingListBrowserAgent::GetAccountInfoFromLastAddedURL(
const GURL& url) {
ReadingListModel* reading_model =
ReadingListModelFactory::GetInstance()->GetForBrowserState(
browser_->GetBrowserState());
CoreAccountId account_id = reading_model->GetAccountWhereEntryIsSavedTo(url);
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForBrowserState(
browser_->GetBrowserState()->GetOriginalChromeBrowserState());
AccountInfo account_info =
identity_manager->FindExtendedAccountInfoByAccountId(account_id);
return account_info;
}
void ReadingListBrowserAgent::AddURLToReadingListwithTitle(const GURL& url,
NSString* title) {
web::WebState* current_web_state =
browser_->GetWebStateList()->GetActiveWebState();
if (current_web_state &&
current_web_state->GetVisibleURL().spec() == url.spec()) {
// Log UKM if the current page is being added to Reading List.
ukm::SourceId source_id =
ukm::GetSourceIdForWebStateDocument(current_web_state);
if (source_id != ukm::kInvalidSourceId) {
ukm::builders::IOS_PageAddedToReadingList(source_id)
.SetAddedFromMessages(false)
.Record(ukm::UkmRecorder::Get());
}
}
RecordModuleFreshnessSignal(ContentSuggestionsModuleType::kShortcuts);
base::RecordAction(base::UserMetricsAction("MobileReadingListAdd"));
ReadingListModel* reading_model =
ReadingListModelFactory::GetInstance()->GetForBrowserState(
browser_->GetBrowserState());
reading_model->AddOrReplaceEntry(url, base::SysNSStringToUTF8(title),
reading_list::ADDED_VIA_CURRENT_APP,
/*estimated_read_time=*/base::TimeDelta());
}
MDCSnackbarMessageAction*
ReadingListBrowserAgent::CreateUndoActionWithReadingListURLs(
NSArray<URLWithTitle*>* urls) {
MDCSnackbarMessageAction* action = [[MDCSnackbarMessageAction alloc] init];
base::WeakPtr<ReadingListBrowserAgent> weak_agent =
weak_ptr_factory_.GetWeakPtr();
action.handler = ^{
base::RecordAction(
base::UserMetricsAction("MobileReadingListDeleteFromSnackbarUndo"));
ReadingListBrowserAgent* agent = weak_agent.get();
if (agent) {
agent->RemoveURLsFromReadingList(urls);
}
};
action.accessibilityIdentifier = kReadingListAddedToAccountSnackbarUndoID;
action.title =
l10n_util::GetNSString(IDS_IOS_READING_LIST_SNACKBAR_UNDO_ACTION);
action.accessibilityLabel =
l10n_util::GetNSString(IDS_IOS_READING_LIST_SNACKBAR_UNDO_ACTION);
return action;
}
void ReadingListBrowserAgent::RemoveURLsFromReadingList(
NSArray<URLWithTitle*>* urls) {
ReadingListModel* reading_model =
ReadingListModelFactory::GetInstance()->GetForBrowserState(
browser_->GetBrowserState());
for (URLWithTitle* url_with_title in urls) {
reading_model->RemoveEntryByURL(url_with_title.URL, FROM_HERE);
}
}
MDCSnackbarMessageAction* ReadingListBrowserAgent::CreateViewAction() {
MDCSnackbarMessageAction* action = [[MDCSnackbarMessageAction alloc] init];
base::WeakPtr<ReadingListBrowserAgent> weak_agent =
weak_ptr_factory_.GetWeakPtr();
action.handler = ^{
base::RecordAction(
base::UserMetricsAction("IOSReadingListSnackbarViewButtonClicked"));
ReadingListBrowserAgent* agent = weak_agent.get();
if (agent) {
CommandDispatcher* dispatcher = agent->browser_->GetCommandDispatcher();
id<BrowserCoordinatorCommands> browser_coordinator_commands_handler =
HandlerForProtocol(dispatcher, BrowserCoordinatorCommands);
[browser_coordinator_commands_handler showReadingList];
}
};
action.title =
l10n_util::GetNSString(IDS_IOS_READING_LIST_SNACKBAR_VIEW_ACTION);
return action;
}