| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/ui/reading_list/reading_list_coordinator.h" |
| |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/feature_engagement/public/event_constants.h" |
| #include "components/feature_engagement/public/tracker.h" |
| #include "components/reading_list/core/reading_list_entry.h" |
| #include "ios/chrome/browser/chrome_url_constants.h" |
| #include "ios/chrome/browser/favicon/ios_chrome_favicon_loader_factory.h" |
| #include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h" |
| #include "ios/chrome/browser/feature_engagement/tracker_factory.h" |
| #include "ios/chrome/browser/main/browser.h" |
| #import "ios/chrome/browser/metrics/new_tab_page_uma.h" |
| #include "ios/chrome/browser/reading_list/offline_url_utils.h" |
| #include "ios/chrome/browser/reading_list/reading_list_model_factory.h" |
| #import "ios/chrome/browser/ui/commands/application_commands.h" |
| #import "ios/chrome/browser/ui/commands/command_dispatcher.h" |
| #import "ios/chrome/browser/ui/menu/action_factory.h" |
| #import "ios/chrome/browser/ui/menu/menu_histograms.h" |
| #import "ios/chrome/browser/ui/reading_list/context_menu/reading_list_context_menu_coordinator.h" |
| #import "ios/chrome/browser/ui/reading_list/context_menu/reading_list_context_menu_delegate.h" |
| #import "ios/chrome/browser/ui/reading_list/context_menu/reading_list_context_menu_params.h" |
| #import "ios/chrome/browser/ui/reading_list/reading_list_list_item.h" |
| #import "ios/chrome/browser/ui/reading_list/reading_list_list_item_factory.h" |
| #import "ios/chrome/browser/ui/reading_list/reading_list_list_view_controller_audience.h" |
| #import "ios/chrome/browser/ui/reading_list/reading_list_list_view_controller_delegate.h" |
| #import "ios/chrome/browser/ui/reading_list/reading_list_mediator.h" |
| #import "ios/chrome/browser/ui/reading_list/reading_list_menu_provider.h" |
| #import "ios/chrome/browser/ui/reading_list/reading_list_table_view_controller.h" |
| #import "ios/chrome/browser/ui/table_view/feature_flags.h" |
| #import "ios/chrome/browser/ui/table_view/table_view_animator.h" |
| #import "ios/chrome/browser/ui/table_view/table_view_navigation_controller.h" |
| #import "ios/chrome/browser/ui/table_view/table_view_navigation_controller_constants.h" |
| #import "ios/chrome/browser/ui/table_view/table_view_presentation_controller.h" |
| #import "ios/chrome/browser/ui/util/pasteboard_util.h" |
| #import "ios/chrome/browser/url_loading/url_loading_browser_agent.h" |
| #import "ios/chrome/browser/url_loading/url_loading_params.h" |
| #include "ios/chrome/browser/web_state_list/web_state_list.h" |
| #import "ios/chrome/browser/window_activities/window_activity_helpers.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #include "ios/web/public/navigation/referrer.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| @interface ReadingListCoordinator () <ReadingListContextMenuDelegate, |
| ReadingListMenuProvider, |
| ReadingListListViewControllerAudience, |
| ReadingListListViewControllerDelegate, |
| UIViewControllerTransitioningDelegate> |
| |
| // Whether the coordinator is started. |
| @property(nonatomic, assign, getter=isStarted) BOOL started; |
| // The mediator that updates the table for model changes. |
| @property(nonatomic, strong) ReadingListMediator* mediator; |
| // The navigation controller displaying self.tableViewController. |
| @property(nonatomic, strong) |
| TableViewNavigationController* navigationController; |
| // The view controller used to display the reading list. |
| @property(nonatomic, strong) |
| ReadingListTableViewController* tableViewController; |
| // The coordinator used to show the context menu. |
| @property(nonatomic, strong) |
| ReadingListContextMenuCoordinator* contextMenuCoordinator; |
| |
| @end |
| |
| @implementation ReadingListCoordinator |
| @synthesize started = _started; |
| @synthesize mediator = _mediator; |
| @synthesize navigationController = _navigationController; |
| @synthesize tableViewController = _tableViewController; |
| @synthesize contextMenuCoordinator = _contextMenuCoordinator; |
| |
| #pragma mark - Accessors |
| |
| - (void)setContextMenuCoordinator: |
| (ReadingListContextMenuCoordinator*)contextMenuCoordinator { |
| if (_contextMenuCoordinator == contextMenuCoordinator) |
| return; |
| [_contextMenuCoordinator stop]; |
| _contextMenuCoordinator = contextMenuCoordinator; |
| } |
| |
| #pragma mark - ChromeCoordinator |
| |
| - (void)start { |
| if (self.started) |
| return; |
| |
| // Create the mediator. |
| ReadingListModel* model = |
| ReadingListModelFactory::GetInstance()->GetForBrowserState( |
| self.browser->GetBrowserState()); |
| ReadingListListItemFactory* itemFactory = |
| [[ReadingListListItemFactory alloc] init]; |
| FaviconLoader* faviconLoader = |
| IOSChromeFaviconLoaderFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| self.mediator = [[ReadingListMediator alloc] initWithModel:model |
| faviconLoader:faviconLoader |
| listItemFactory:itemFactory]; |
| |
| // Create the table. |
| self.tableViewController = [[ReadingListTableViewController alloc] init]; |
| self.tableViewController.delegate = self; |
| self.tableViewController.audience = self; |
| self.tableViewController.dataSource = self.mediator; |
| self.tableViewController.browser = self.browser; |
| |
| if (@available(iOS 13.0, *)) { |
| self.tableViewController.menuProvider = self; |
| } |
| |
| itemFactory.accessibilityDelegate = self.tableViewController; |
| |
| // Add the "Done" button and hook it up to |stop|. |
| UIBarButtonItem* dismissButton = [[UIBarButtonItem alloc] |
| initWithBarButtonSystemItem:UIBarButtonSystemItemDone |
| target:self |
| action:@selector(dismissButtonTapped)]; |
| [dismissButton |
| setAccessibilityIdentifier:kTableViewNavigationDismissButtonId]; |
| self.tableViewController.navigationItem.rightBarButtonItem = dismissButton; |
| |
| // Present RecentTabsNavigationController. |
| self.navigationController = [[TableViewNavigationController alloc] |
| initWithTable:self.tableViewController]; |
| self.navigationController.toolbarHidden = NO; |
| |
| BOOL useCustomPresentation = YES; |
| if (IsCollectionsCardPresentationStyleEnabled()) { |
| if (@available(iOS 13, *)) { |
| #if defined(__IPHONE_13_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0) |
| [self.navigationController |
| setModalPresentationStyle:UIModalPresentationFormSheet]; |
| self.navigationController.presentationController.delegate = |
| self.tableViewController; |
| useCustomPresentation = NO; |
| #endif |
| } |
| } |
| |
| if (useCustomPresentation) { |
| self.navigationController.transitioningDelegate = self; |
| self.navigationController.modalPresentationStyle = |
| UIModalPresentationCustom; |
| } |
| |
| [self.baseViewController presentViewController:self.navigationController |
| animated:YES |
| completion:nil]; |
| |
| // Send the "Viewed Reading List" event to the feature_engagement::Tracker |
| // when the user opens their reading list. |
| feature_engagement::TrackerFactory::GetForBrowserState( |
| self.browser->GetBrowserState()) |
| ->NotifyEvent(feature_engagement::events::kViewedReadingList); |
| |
| [super start]; |
| self.started = YES; |
| } |
| |
| - (void)dismissButtonTapped { |
| base::RecordAction(base::UserMetricsAction("MobileReadingListClose")); |
| [self stop]; |
| } |
| |
| - (void)stop { |
| if (!self.started) |
| return; |
| self.contextMenuCoordinator = nil; |
| [self.tableViewController willBeDismissed]; |
| [self.navigationController.presentingViewController |
| dismissViewControllerAnimated:YES |
| completion:nil]; |
| self.tableViewController = nil; |
| self.navigationController = nil; |
| [super stop]; |
| self.started = NO; |
| } |
| |
| #pragma mark - ReadingListListViewControllerAudience |
| |
| - (void)readingListHasItems:(BOOL)hasItems { |
| self.navigationController.toolbarHidden = !hasItems; |
| } |
| |
| #pragma mark - ReadingListContextMenuDelegate |
| |
| - (void)openURLInNewTabForContextMenuWithParams: |
| (ReadingListContextMenuParams*)params { |
| [self loadEntryURL:params.entryURL |
| withOfflineURL:GURL::EmptyGURL() |
| inNewTab:YES |
| incognito:NO]; |
| } |
| |
| - (void)openURLInNewIncognitoTabForContextMenuWithParams: |
| (ReadingListContextMenuParams*)params { |
| [self loadEntryURL:params.entryURL |
| withOfflineURL:GURL::EmptyGURL() |
| inNewTab:YES |
| incognito:YES]; |
| } |
| |
| - (void)openURLInNewWindowForContextMenuWithParams: |
| (ReadingListContextMenuParams*)params { |
| id<ApplicationCommands> windowOpener = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), ApplicationCommands); |
| [windowOpener openNewWindowWithActivity:ActivityToLoadURL( |
| WindowActivityReadingListOrigin, |
| params.entryURL)]; |
| } |
| |
| - (void)copyURLForContextMenuWithParams:(ReadingListContextMenuParams*)params { |
| StoreURLInPasteboard(params.entryURL); |
| self.contextMenuCoordinator = nil; |
| } |
| |
| - (void)openOfflineURLInNewTabForContextMenuWithParams: |
| (ReadingListContextMenuParams*)params { |
| [self loadEntryURL:params.entryURL |
| withOfflineURL:params.offlineURL |
| inNewTab:YES |
| incognito:NO]; |
| } |
| |
| #pragma mark - ReadingListTableViewControllerDelegate |
| |
| - (void)dismissReadingListListViewController:(UIViewController*)viewController { |
| DCHECK_EQ(self.tableViewController, viewController); |
| [self.tableViewController willBeDismissed]; |
| [self stop]; |
| } |
| |
| - (void)readingListListViewController:(UIViewController*)viewController |
| displayContextMenuForItem:(id<ReadingListListItem>)item |
| atPoint:(CGPoint)menuLocation { |
| DCHECK_EQ(self.tableViewController, viewController); |
| const ReadingListEntry* entry = [self.mediator entryFromItem:item]; |
| if (!entry) { |
| [self.tableViewController reloadData]; |
| return; |
| } |
| |
| const GURL entryURL = entry->URL(); |
| GURL offlineURL; |
| if (entry->DistilledState() == ReadingListEntry::PROCESSED) { |
| offlineURL = reading_list::OfflineURLForPath( |
| entry->DistilledPath(), entryURL, entry->DistilledURL()); |
| } |
| |
| ReadingListContextMenuParams* params = |
| [[ReadingListContextMenuParams alloc] init]; |
| params.title = base::SysUTF8ToNSString(entry->Title()); |
| params.message = base::SysUTF8ToNSString(entryURL.spec()); |
| params.rect = CGRectMake(menuLocation.x, menuLocation.y, 0, 0); |
| params.view = self.tableViewController.tableView; |
| params.entryURL = entryURL; |
| params.offlineURL = offlineURL; |
| |
| self.contextMenuCoordinator = [[ReadingListContextMenuCoordinator alloc] |
| initWithBaseViewController:self.navigationController |
| browser:self.browser |
| params:params]; |
| self.contextMenuCoordinator.delegate = self; |
| [self.contextMenuCoordinator start]; |
| } |
| |
| - (void)readingListListViewController:(UIViewController*)viewController |
| openItem:(id<ReadingListListItem>)item { |
| DCHECK_EQ(self.tableViewController, viewController); |
| const ReadingListEntry* entry = [self.mediator entryFromItem:item]; |
| if (!entry) { |
| [self.tableViewController reloadData]; |
| return; |
| } |
| [self loadEntryURL:entry->URL() |
| withOfflineURL:GURL::EmptyGURL() |
| inNewTab:NO |
| incognito:NO]; |
| } |
| |
| - (void)readingListListViewController:(UIViewController*)viewController |
| openItemInNewTab:(id<ReadingListListItem>)item |
| incognito:(BOOL)incognito { |
| DCHECK_EQ(self.tableViewController, viewController); |
| const ReadingListEntry* entry = [self.mediator entryFromItem:item]; |
| if (!entry) { |
| [self.tableViewController reloadData]; |
| return; |
| } |
| [self loadEntryURL:entry->URL() |
| withOfflineURL:GURL::EmptyGURL() |
| inNewTab:YES |
| incognito:incognito]; |
| } |
| |
| - (void)readingListListViewController:(UIViewController*)viewController |
| openItemOfflineInNewTab:(id<ReadingListListItem>)item { |
| DCHECK_EQ(self.tableViewController, viewController); |
| const ReadingListEntry* entry = [self.mediator entryFromItem:item]; |
| if (!entry) |
| return; |
| |
| if (entry->DistilledState() == ReadingListEntry::PROCESSED) { |
| const GURL entryURL = entry->URL(); |
| GURL offlineURL = reading_list::OfflineURLForPath( |
| entry->DistilledPath(), entryURL, entry->DistilledURL()); |
| [self loadEntryURL:entry->URL() |
| withOfflineURL:offlineURL |
| inNewTab:YES |
| incognito:NO]; |
| } |
| } |
| |
| #pragma mark - UIViewControllerTransitioningDelegate |
| |
| - (UIPresentationController*) |
| presentationControllerForPresentedViewController:(UIViewController*)presented |
| presentingViewController:(UIViewController*)presenting |
| sourceViewController:(UIViewController*)source { |
| return [[TableViewPresentationController alloc] |
| initWithPresentedViewController:presented |
| presentingViewController:presenting]; |
| } |
| |
| - (id<UIViewControllerAnimatedTransitioning>) |
| animationControllerForPresentedController:(UIViewController*)presented |
| presentingController:(UIViewController*)presenting |
| sourceController:(UIViewController*)source { |
| UITraitCollection* traitCollection = presenting.traitCollection; |
| if (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact && |
| traitCollection.verticalSizeClass != UIUserInterfaceSizeClassCompact) { |
| // Use the default animator for fullscreen presentations. |
| return nil; |
| } |
| |
| TableViewAnimator* animator = [[TableViewAnimator alloc] init]; |
| animator.presenting = YES; |
| return animator; |
| } |
| |
| - (id<UIViewControllerAnimatedTransitioning>) |
| animationControllerForDismissedController:(UIViewController*)dismissed { |
| UITraitCollection* traitCollection = dismissed.traitCollection; |
| if (traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact && |
| traitCollection.verticalSizeClass != UIUserInterfaceSizeClassCompact) { |
| // Use the default animator for fullscreen presentations. |
| return nil; |
| } |
| |
| TableViewAnimator* animator = [[TableViewAnimator alloc] init]; |
| animator.presenting = NO; |
| return animator; |
| } |
| |
| #pragma mark - URL Loading Helpers |
| |
| // Loads reading list URLs. If |offlineURL| is valid, the item will be loaded |
| // offline; otherwise |entryURL| is loaded. |newTab| and |incognito| can be |
| // used to optionally open the URL in a new tab or in incognito. The |
| // coordinator is also stopped after the load is requested. |
| - (void)loadEntryURL:(const GURL&)entryURL |
| withOfflineURL:(const GURL&)offlineURL |
| inNewTab:(BOOL)newTab |
| incognito:(BOOL)incognito { |
| DCHECK(entryURL.is_valid()); |
| base::RecordAction(base::UserMetricsAction("MobileReadingListOpen")); |
| web::WebState* activeWebState = |
| self.browser->GetWebStateList()->GetActiveWebState(); |
| new_tab_page_uma::RecordAction( |
| self.browser->GetBrowserState(), activeWebState, |
| new_tab_page_uma::ACTION_OPENED_READING_LIST_ENTRY); |
| |
| // Load the offline URL if available. |
| GURL loadURL = entryURL; |
| if (offlineURL.is_valid()) { |
| loadURL = offlineURL; |
| // Offline URLs should always be opened in new tabs. |
| newTab = YES; |
| const GURL updateURL = entryURL; |
| [self.mediator markEntryRead:updateURL]; |
| } |
| |
| // Prepare the table for dismissal. |
| [self.tableViewController willBeDismissed]; |
| |
| // Use a referrer with a specific URL to signal that this entry should not be |
| // taken into account for the Most Visited tiles. |
| if (newTab) { |
| UrlLoadParams params = UrlLoadParams::InNewTab(loadURL, entryURL); |
| params.in_incognito = incognito; |
| params.web_params.referrer = web::Referrer(GURL(kReadingListReferrerURL), |
| web::ReferrerPolicyDefault); |
| UrlLoadingBrowserAgent::FromBrowser(self.browser)->Load(params); |
| } else { |
| UrlLoadParams params = UrlLoadParams::InCurrentTab(loadURL); |
| params.web_params.transition_type = ui::PAGE_TRANSITION_AUTO_BOOKMARK; |
| params.web_params.referrer = web::Referrer(GURL(kReadingListReferrerURL), |
| web::ReferrerPolicyDefault); |
| UrlLoadingBrowserAgent::FromBrowser(self.browser)->Load(params); |
| } |
| |
| [self stop]; |
| } |
| |
| #pragma mark - ReadingListMenuProvider |
| |
| - (UIContextMenuConfiguration*)contextMenuConfigurationForItem: |
| (id<ReadingListListItem>)item API_AVAILABLE(ios(13.0)) { |
| return [UIContextMenuConfiguration |
| configurationWithIdentifier:nil |
| previewProvider:nil |
| actionProvider:^(NSArray<UIMenuElement*>* suggestedActions) { |
| // Record that this context menu was shown to the user. |
| RecordMenuShown(MenuScenario::kReadingListEntry); |
| |
| ActionFactory* actionFactory = [[ActionFactory alloc] |
| initWithScenario:MenuScenario::kReadingListEntry]; |
| |
| UIAction* copyAction = |
| [actionFactory actionToCopyURL:item.entryURL]; |
| |
| return [UIMenu menuWithTitle:@"" children:@[ copyAction ]]; |
| }]; |
| } |
| |
| @end |