| // Copyright 2017 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/content_suggestions/content_suggestions_coordinator.h" |
| |
| #include "base/mac/foundation_util.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/scoped_observer.h" |
| #import "components/feature_engagement/public/event_constants.h" |
| #import "components/feature_engagement/public/tracker.h" |
| #include "components/feed/core/shared_prefs/pref_names.h" |
| #include "components/ntp_snippets/content_suggestions_service.h" |
| #include "components/ntp_snippets/pref_names.h" |
| #include "components/ntp_snippets/remote/remote_suggestions_scheduler.h" |
| #include "components/ntp_tiles/most_visited_sites.h" |
| #include "components/prefs/pref_service.h" |
| #import "components/search_engines/template_url.h" |
| #import "components/search_engines/template_url_service.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #include "ios/chrome/browser/discover_feed/discover_feed_service.h" |
| #include "ios/chrome/browser/discover_feed/discover_feed_service_factory.h" |
| #import "ios/chrome/browser/drag_and_drop/url_drag_drop_handler.h" |
| #include "ios/chrome/browser/favicon/ios_chrome_large_icon_cache_factory.h" |
| #include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h" |
| #include "ios/chrome/browser/favicon/large_icon_cache.h" |
| #import "ios/chrome/browser/feature_engagement/tracker_factory.h" |
| #import "ios/chrome/browser/main/browser.h" |
| #include "ios/chrome/browser/ntp_snippets/ios_chrome_content_suggestions_service_factory.h" |
| #include "ios/chrome/browser/ntp_tiles/ios_most_visited_sites_factory.h" |
| #include "ios/chrome/browser/pref_names.h" |
| #include "ios/chrome/browser/reading_list/reading_list_model_factory.h" |
| #import "ios/chrome/browser/search_engines/template_url_service_factory.h" |
| #import "ios/chrome/browser/signin/authentication_service.h" |
| #import "ios/chrome/browser/signin/authentication_service_factory.h" |
| #include "ios/chrome/browser/signin/identity_manager_factory.h" |
| #import "ios/chrome/browser/ui/activity_services/activity_params.h" |
| #import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h" |
| #import "ios/chrome/browser/ui/commands/application_commands.h" |
| #import "ios/chrome/browser/ui/commands/browser_commands.h" |
| #import "ios/chrome/browser/ui/commands/command_dispatcher.h" |
| #import "ios/chrome/browser/ui/commands/omnibox_commands.h" |
| #import "ios/chrome/browser/ui/commands/open_new_tab_command.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_action_handler.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_data_sink.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_feature.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_synchronizer.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_view_controller.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_menu_provider.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_recorder.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h" |
| #import "ios/chrome/browser/ui/content_suggestions/discover_feed_delegate.h" |
| #import "ios/chrome/browser/ui/content_suggestions/discover_feed_header_changing.h" |
| #import "ios/chrome/browser/ui/content_suggestions/discover_feed_menu_commands.h" |
| #import "ios/chrome/browser/ui/content_suggestions/discover_feed_metrics_recorder.h" |
| #import "ios/chrome/browser/ui/content_suggestions/ntp_home_constant.h" |
| #import "ios/chrome/browser/ui/content_suggestions/ntp_home_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/ntp_home_metrics.h" |
| #import "ios/chrome/browser/ui/content_suggestions/theme_change_delegate.h" |
| #import "ios/chrome/browser/ui/main/scene_state.h" |
| #import "ios/chrome/browser/ui/main/scene_state_browser_agent.h" |
| #import "ios/chrome/browser/ui/menu/action_factory.h" |
| #import "ios/chrome/browser/ui/menu/menu_histograms.h" |
| #import "ios/chrome/browser/ui/ntp/new_tab_page_feature.h" |
| #import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h" |
| #import "ios/chrome/browser/ui/ntp/notification_promo_whats_new.h" |
| #import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.h" |
| #import "ios/chrome/browser/ui/settings/utils/pref_backed_boolean.h" |
| #import "ios/chrome/browser/ui/sharing/sharing_coordinator.h" |
| #import "ios/chrome/browser/ui/util/multi_window_support.h" |
| #import "ios/chrome/browser/ui/util/named_guide.h" |
| #import "ios/chrome/browser/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/browser/url_loading/url_loading_browser_agent.h" |
| #import "ios/chrome/browser/url_loading/url_loading_params.h" |
| #import "ios/chrome/browser/voice/voice_search_availability.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #import "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
| #import "ios/public/provider/chrome/browser/discover_feed/discover_feed_provider.h" |
| #import "ios/web/public/navigation/navigation_item.h" |
| #import "ios/web/public/navigation/navigation_manager.h" |
| #import "ios/web/public/web_state.h" |
| #import "ui/base/l10n/l10n_util_mac.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| @interface ContentSuggestionsCoordinator () < |
| ContentSuggestionsActionHandler, |
| ContentSuggestionsMenuProvider, |
| ContentSuggestionsViewControllerAudience, |
| DiscoverFeedDelegate, |
| DiscoverFeedMenuCommands, |
| OverscrollActionsControllerDelegate, |
| ThemeChangeDelegate, |
| URLDropDelegate> { |
| // Helper object managing the availability of the voice search feature. |
| VoiceSearchAvailability _voiceSearchAvailability; |
| } |
| |
| @property(nonatomic, strong) |
| ContentSuggestionsViewController* suggestionsViewController; |
| @property(nonatomic, strong) |
| ContentSuggestionsMediator* contentSuggestionsMediator; |
| @property(nonatomic, strong) |
| ContentSuggestionsHeaderSynchronizer* headerCollectionInteractionHandler; |
| @property(nonatomic, strong) ContentSuggestionsMetricsRecorder* metricsRecorder; |
| @property(nonatomic, strong) |
| DiscoverFeedMetricsRecorder* discoverFeedMetricsRecorder; |
| @property(nonatomic, strong) NTPHomeMediator* NTPMediator; |
| @property(nonatomic, strong) UIViewController* discoverFeedViewController; |
| @property(nonatomic, strong) UIView* discoverFeedHeaderMenuButton; |
| @property(nonatomic, strong) URLDragDropHandler* dragDropHandler; |
| @property(nonatomic, strong) ActionSheetCoordinator* alertCoordinator; |
| // Redefined as readwrite. |
| @property(nonatomic, strong, readwrite) |
| ContentSuggestionsHeaderViewController* headerController; |
| @property(nonatomic, strong) PrefBackedBoolean* contentSuggestionsVisible; |
| // Delegate for handling Discover feed header UI changes. |
| @property(nonatomic, weak) id<DiscoverFeedHeaderChanging> |
| discoverFeedHeaderDelegate; |
| // Authentication Service for the user's signed-in state. |
| @property(nonatomic, assign) AuthenticationService* authService; |
| // Coordinator in charge of handling sharing use cases. |
| @property(nonatomic, strong) SharingCoordinator* sharingCoordinator; |
| // YES if the feedShown method has already been called. |
| // TODO(crbug.com/1126940): The coordinator shouldn't be keeping track of this |
| // for its |self.discoverFeedViewController| remove once we have an appropriate |
| // callback. |
| @property(nonatomic, assign) BOOL feedShownWasCalled; |
| |
| @end |
| |
| @implementation ContentSuggestionsCoordinator |
| |
| - (void)start { |
| DCHECK(self.browser); |
| if (self.visible) { |
| // Prevent this coordinator from being started twice in a row |
| return; |
| } |
| |
| _visible = YES; |
| |
| // Make sure that the omnibox is unfocused to prevent having it visually |
| // focused while the NTP is just created (with the fakebox visible). |
| id<OmniboxCommands> omniboxCommandHandler = |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), OmniboxCommands); |
| [omniboxCommandHandler cancelOmniboxEdit]; |
| |
| self.authService = AuthenticationServiceFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| |
| ntp_snippets::ContentSuggestionsService* contentSuggestionsService = |
| IOSChromeContentSuggestionsServiceFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| contentSuggestionsService->remote_suggestions_scheduler() |
| ->OnSuggestionsSurfaceOpened(); |
| contentSuggestionsService->user_classifier()->OnEvent( |
| ntp_snippets::UserClassifier::Metric::NTP_OPENED); |
| contentSuggestionsService->user_classifier()->OnEvent( |
| ntp_snippets::UserClassifier::Metric::SUGGESTIONS_SHOWN); |
| PrefService* prefs = |
| ChromeBrowserState::FromBrowserState(self.browser->GetBrowserState()) |
| ->GetPrefs(); |
| bool contentSuggestionsEnabled = |
| prefs->GetBoolean(prefs::kArticlesForYouEnabled); |
| self.contentSuggestionsVisible = [[PrefBackedBoolean alloc] |
| initWithPrefService:prefs |
| prefName:feed::prefs::kArticlesListVisible]; |
| if (contentSuggestionsEnabled) { |
| if ([self.contentSuggestionsVisible value]) { |
| ntp_home::RecordNTPImpression(ntp_home::REMOTE_SUGGESTIONS); |
| } else { |
| ntp_home::RecordNTPImpression(ntp_home::REMOTE_COLLAPSED); |
| } |
| } else { |
| ntp_home::RecordNTPImpression(ntp_home::LOCAL_SUGGESTIONS); |
| } |
| |
| TemplateURLService* templateURLService = |
| ios::TemplateURLServiceFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| |
| self.NTPMediator = [[NTPHomeMediator alloc] |
| initWithWebState:self.webState |
| templateURLService:templateURLService |
| URLLoader:UrlLoadingBrowserAgent::FromBrowser(self.browser) |
| authService:self.authService |
| identityManager:IdentityManagerFactory::GetForBrowserState( |
| self.browser->GetBrowserState()) |
| logoVendor:ios::GetChromeBrowserProvider()->CreateLogoVendor( |
| self.browser, self.webState) |
| voiceSearchAvailability:&_voiceSearchAvailability]; |
| self.NTPMediator.browser = self.browser; |
| |
| self.headerController = [[ContentSuggestionsHeaderViewController alloc] init]; |
| // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol |
| // clean up. |
| self.headerController.dispatcher = |
| static_cast<id<ApplicationCommands, BrowserCommands, OmniboxCommands, |
| FakeboxFocuser>>(self.browser->GetCommandDispatcher()); |
| self.headerController.commandHandler = self.NTPMediator; |
| self.headerController.delegate = self.NTPMediator; |
| |
| self.headerController.readingListModel = |
| ReadingListModelFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| self.headerController.toolbarDelegate = self.toolbarDelegate; |
| |
| favicon::LargeIconService* largeIconService = |
| IOSChromeLargeIconServiceFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| LargeIconCache* cache = IOSChromeLargeIconCacheFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| std::unique_ptr<ntp_tiles::MostVisitedSites> mostVisitedFactory = |
| IOSMostVisitedSitesFactory::NewForBrowserState( |
| self.browser->GetBrowserState()); |
| ReadingListModel* readingListModel = |
| ReadingListModelFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| |
| if (IsDiscoverFeedEnabled()) { |
| // Creating the DiscoverFeedService will start the DiscoverFeed. |
| DiscoverFeedService* discoverFeedService = |
| DiscoverFeedServiceFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| self.discoverFeedMetricsRecorder = |
| discoverFeedService->GetDiscoverFeedMetricsRecorder(); |
| } |
| self.discoverFeedViewController = [self discoverFeed]; |
| |
| const TemplateURL* defaultURL = |
| templateURLService->GetDefaultSearchProvider(); |
| BOOL isGoogleDefaultSearchProvider = |
| defaultURL && |
| defaultURL->GetEngineType(templateURLService->search_terms_data()) == |
| SEARCH_ENGINE_GOOGLE; |
| |
| self.contentSuggestionsMediator = [[ContentSuggestionsMediator alloc] |
| initWithContentService:contentSuggestionsService |
| largeIconService:largeIconService |
| largeIconCache:cache |
| mostVisitedSite:std::move(mostVisitedFactory) |
| readingListModel:readingListModel |
| prefService:prefs |
| discoverFeed:self.discoverFeedViewController |
| isGoogleDefaultSearchProvider:isGoogleDefaultSearchProvider]; |
| self.contentSuggestionsMediator.commandHandler = self.NTPMediator; |
| self.contentSuggestionsMediator.headerProvider = self.headerController; |
| self.contentSuggestionsMediator.contentArticlesExpanded = |
| self.contentSuggestionsVisible; |
| self.contentSuggestionsMediator.discoverFeedDelegate = self; |
| |
| self.headerController.promoCanShow = |
| [self.contentSuggestionsMediator notificationPromo]->CanShow(); |
| |
| self.metricsRecorder = [[ContentSuggestionsMetricsRecorder alloc] init]; |
| self.metricsRecorder.delegate = self.contentSuggestionsMediator; |
| |
| |
| // Offset to maintain Discover feed scroll position. |
| CGFloat offset = 0; |
| if (IsDiscoverFeedEnabled() && contentSuggestionsEnabled) { |
| web::NavigationManager* navigationManager = |
| self.webState->GetNavigationManager(); |
| web::NavigationItem* item = navigationManager->GetVisibleItem(); |
| if (item) { |
| offset = item->GetPageDisplayState().scroll_state().content_offset().y; |
| } |
| } |
| |
| self.suggestionsViewController = [[ContentSuggestionsViewController alloc] |
| initWithStyle:CollectionViewControllerStyleDefault |
| offset:offset]; |
| [self.suggestionsViewController |
| setDataSource:self.contentSuggestionsMediator]; |
| self.suggestionsViewController.suggestionCommandHandler = self.NTPMediator; |
| self.suggestionsViewController.audience = self; |
| self.suggestionsViewController.overscrollDelegate = self; |
| self.suggestionsViewController.themeChangeDelegate = self; |
| self.suggestionsViewController.metricsRecorder = self.metricsRecorder; |
| id<SnackbarCommands> dispatcher = |
| static_cast<id<SnackbarCommands>>(self.browser->GetCommandDispatcher()); |
| self.suggestionsViewController.dispatcher = dispatcher; |
| self.suggestionsViewController.discoverFeedMenuHandler = self; |
| self.suggestionsViewController.discoverFeedMetricsRecorder = |
| self.discoverFeedMetricsRecorder; |
| self.suggestionsViewController.panGestureHandler = self.panGestureHandler; |
| |
| self.discoverFeedHeaderDelegate = |
| self.suggestionsViewController.discoverFeedHeaderDelegate; |
| [self.discoverFeedHeaderDelegate |
| changeDiscoverFeedHeaderVisibility:[self.contentSuggestionsVisible |
| value]]; |
| self.suggestionsViewController.contentSuggestionsEnabled = |
| contentSuggestionsEnabled; |
| self.suggestionsViewController.handler = self; |
| self.contentSuggestionsMediator.consumer = self.suggestionsViewController; |
| |
| if (@available(iOS 13.0, *)) { |
| self.suggestionsViewController.menuProvider = self; |
| } |
| |
| self.NTPMediator.consumer = self.headerController; |
| // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol |
| // clean up. |
| self.NTPMediator.dispatcher = |
| static_cast<id<ApplicationCommands, BrowserCommands, OmniboxCommands, |
| SnackbarCommands>>(self.browser->GetCommandDispatcher()); |
| self.NTPMediator.NTPMetrics = [[NTPHomeMetrics alloc] |
| initWithBrowserState:self.browser->GetBrowserState() |
| webState:self.webState]; |
| self.NTPMediator.metricsRecorder = self.metricsRecorder; |
| self.NTPMediator.suggestionsViewController = self.suggestionsViewController; |
| self.NTPMediator.suggestionsMediator = self.contentSuggestionsMediator; |
| self.NTPMediator.suggestionsService = contentSuggestionsService; |
| [self.NTPMediator setUp]; |
| self.NTPMediator.discoverFeedMetrics = self.discoverFeedMetricsRecorder; |
| |
| [self.suggestionsViewController addChildViewController:self.headerController]; |
| [self.headerController |
| didMoveToParentViewController:self.suggestionsViewController]; |
| |
| // TODO(crbug.com/1114792): Remove header provider and use refactored header |
| // synchronizer instead. |
| self.suggestionsViewController.headerProvider = self.headerController; |
| |
| if (!IsRefactoredNTP()) { |
| self.headerCollectionInteractionHandler = |
| [[ContentSuggestionsHeaderSynchronizer alloc] |
| initWithCollectionController:self.suggestionsViewController |
| headerController:self.headerController]; |
| self.NTPMediator.headerCollectionInteractionHandler = |
| self.headerCollectionInteractionHandler; |
| } |
| |
| self.dragDropHandler = [[URLDragDropHandler alloc] init]; |
| self.dragDropHandler.dropDelegate = self; |
| [self.suggestionsViewController.collectionView |
| addInteraction:[[UIDropInteraction alloc] |
| initWithDelegate:self.dragDropHandler]]; |
| } |
| |
| - (void)stop { |
| [self.NTPMediator shutdown]; |
| self.NTPMediator = nil; |
| [self.contentSuggestionsMediator disconnect]; |
| self.contentSuggestionsMediator = nil; |
| [self.sharingCoordinator stop]; |
| self.sharingCoordinator = nil; |
| self.headerController = nil; |
| if (IsDiscoverFeedEnabled()) { |
| ios::GetChromeBrowserProvider() |
| ->GetDiscoverFeedProvider() |
| ->RemoveFeedViewController(self.discoverFeedViewController); |
| } |
| self.contentSuggestionsVisible = nil; |
| _visible = NO; |
| } |
| |
| - (UIViewController*)viewController { |
| return self.suggestionsViewController; |
| } |
| |
| - (void)constrainDiscoverHeaderMenuButtonNamedGuide { |
| NamedGuide* menuButtonGuide = |
| [NamedGuide guideWithName:kDiscoverFeedHeaderMenuGuide |
| view:self.discoverFeedHeaderMenuButton]; |
| |
| menuButtonGuide.constrainedView = self.discoverFeedHeaderMenuButton; |
| } |
| |
| #pragma mark - ContentSuggestionsViewControllerAudience |
| |
| - (void)promoShown { |
| NotificationPromoWhatsNew* notificationPromo = |
| [self.contentSuggestionsMediator notificationPromo]; |
| notificationPromo->HandleViewed(); |
| [self.headerController setPromoCanShow:notificationPromo->CanShow()]; |
| } |
| |
| - (void)discoverHeaderMenuButtonShown:(UIView*)menuButton { |
| _discoverFeedHeaderMenuButton = menuButton; |
| } |
| |
| - (void)discoverFeedShown { |
| if (IsDiscoverFeedEnabled() && !self.feedShownWasCalled) { |
| ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider()->FeedWasShown(); |
| self.feedShownWasCalled = YES; |
| } |
| } |
| |
| #pragma mark - OverscrollActionsControllerDelegate |
| |
| - (void)overscrollActionsController:(OverscrollActionsController*)controller |
| didTriggerAction:(OverscrollAction)action { |
| // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol |
| // clean up. |
| id<ApplicationCommands, BrowserCommands, OmniboxCommands, SnackbarCommands> |
| handler = static_cast<id<ApplicationCommands, BrowserCommands, |
| OmniboxCommands, SnackbarCommands>>( |
| self.browser->GetCommandDispatcher()); |
| switch (action) { |
| case OverscrollAction::NEW_TAB: { |
| [handler openURLInNewTab:[OpenNewTabCommand command]]; |
| } break; |
| case OverscrollAction::CLOSE_TAB: { |
| [handler closeCurrentTab]; |
| base::RecordAction(base::UserMetricsAction("OverscrollActionCloseTab")); |
| } break; |
| case OverscrollAction::REFRESH: |
| [self reload]; |
| break; |
| case OverscrollAction::NONE: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| - (BOOL)shouldAllowOverscrollActionsForOverscrollActionsController: |
| (OverscrollActionsController*)controller { |
| return YES; |
| } |
| |
| - (UIView*)toolbarSnapshotViewForOverscrollActionsController: |
| (OverscrollActionsController*)controller { |
| return |
| [[self.headerController toolBarView] snapshotViewAfterScreenUpdates:NO]; |
| } |
| |
| - (UIView*)headerViewForOverscrollActionsController: |
| (OverscrollActionsController*)controller { |
| return self.suggestionsViewController.view; |
| } |
| |
| - (CGFloat)headerInsetForOverscrollActionsController: |
| (OverscrollActionsController*)controller { |
| return 0; |
| } |
| |
| - (CGFloat)headerHeightForOverscrollActionsController: |
| (OverscrollActionsController*)controller { |
| CGFloat height = [self.headerController toolBarView].bounds.size.height; |
| CGFloat topInset = self.suggestionsViewController.view.safeAreaInsets.top; |
| return height + topInset; |
| } |
| |
| - (FullscreenController*)fullscreenControllerForOverscrollActionsController: |
| (OverscrollActionsController*)controller { |
| // Fullscreen isn't supported here. |
| return nullptr; |
| } |
| |
| #pragma mark - ThemeChangeDelegate |
| |
| - (void)handleThemeChange { |
| if (IsDiscoverFeedEnabled()) { |
| ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider()->UpdateTheme(); |
| } |
| } |
| |
| #pragma mark - URLDropDelegate |
| |
| - (BOOL)canHandleURLDropInView:(UIView*)view { |
| return YES; |
| } |
| |
| - (void)view:(UIView*)view didDropURL:(const GURL&)URL atPoint:(CGPoint)point { |
| UrlLoadingBrowserAgent::FromBrowser(self.browser) |
| ->Load(UrlLoadParams::InCurrentTab(URL)); |
| } |
| |
| #pragma mark - DiscoverFeedMenuCommands |
| |
| - (void)openDiscoverFeedMenu { |
| [self.alertCoordinator stop]; |
| self.alertCoordinator = nil; |
| |
| self.alertCoordinator = [[ActionSheetCoordinator alloc] |
| initWithBaseViewController:self.suggestionsViewController |
| browser:self.browser |
| title:nil |
| message:nil |
| rect:self.discoverFeedHeaderMenuButton.frame |
| view:self.discoverFeedHeaderMenuButton.superview]; |
| __weak ContentSuggestionsCoordinator* weakSelf = self; |
| |
| if ([self.contentSuggestionsVisible value]) { |
| [self.alertCoordinator |
| addItemWithTitle:l10n_util::GetNSString( |
| IDS_IOS_DISCOVER_FEED_MENU_TURN_OFF_ITEM) |
| action:^{ |
| [weakSelf setDiscoverFeedVisible:NO]; |
| } |
| style:UIAlertActionStyleDestructive]; |
| } else { |
| [self.alertCoordinator |
| addItemWithTitle:l10n_util::GetNSString( |
| IDS_IOS_DISCOVER_FEED_MENU_TURN_ON_ITEM) |
| action:^{ |
| [weakSelf setDiscoverFeedVisible:YES]; |
| } |
| style:UIAlertActionStyleDefault]; |
| } |
| |
| if (self.authService->IsAuthenticated()) { |
| [self.alertCoordinator |
| addItemWithTitle:l10n_util::GetNSString( |
| IDS_IOS_DISCOVER_FEED_MENU_MANAGE_ACTIVITY_ITEM) |
| action:^{ |
| [weakSelf.NTPMediator handleFeedManageActivityTapped]; |
| } |
| style:UIAlertActionStyleDefault]; |
| |
| [self.alertCoordinator |
| addItemWithTitle:l10n_util::GetNSString( |
| IDS_IOS_DISCOVER_FEED_MENU_MANAGE_INTERESTS_ITEM) |
| action:^{ |
| [weakSelf.NTPMediator handleFeedManageInterestsTapped]; |
| } |
| style:UIAlertActionStyleDefault]; |
| } |
| |
| [self.alertCoordinator |
| addItemWithTitle:l10n_util::GetNSString( |
| IDS_IOS_DISCOVER_FEED_MENU_LEARN_MORE_ITEM) |
| action:^{ |
| [weakSelf.NTPMediator handleFeedLearnMoreTapped]; |
| } |
| style:UIAlertActionStyleDefault]; |
| [self.alertCoordinator start]; |
| } |
| |
| - (void)notifyFeedLoadedForHeaderMenu { |
| feature_engagement::TrackerFactory::GetForBrowserState( |
| self.browser->GetBrowserState()) |
| ->NotifyEvent(feature_engagement::events::kDiscoverFeedLoaded); |
| } |
| |
| #pragma mark - DiscoverFeedDelegate |
| |
| - (void)recreateDiscoverFeedViewController { |
| DCHECK(IsDiscoverFeedEnabled()); |
| |
| // Create and set a new DiscoverFeed since that its model has changed. |
| self.discoverFeedViewController = [self discoverFeed]; |
| self.contentSuggestionsMediator.discoverFeed = |
| self.discoverFeedViewController; |
| [self.alertCoordinator stop]; |
| } |
| |
| - (UIEdgeInsets)safeAreaInsetsForDiscoverFeed { |
| return [SceneStateBrowserAgent::FromBrowser(self.browser) |
| ->GetSceneState() |
| .window.rootViewController.view safeAreaInsets]; |
| } |
| |
| #pragma mark - ContentSuggestionsActionHandler |
| |
| - (void)loadMoreFeedArticles { |
| ios::GetChromeBrowserProvider() |
| ->GetDiscoverFeedProvider() |
| ->LoadMoreFeedArticles(); |
| [self.discoverFeedMetricsRecorder recordInfiniteFeedTriggered]; |
| } |
| |
| #pragma mark - Public methods |
| |
| - (UIView*)view { |
| return self.suggestionsViewController.view; |
| } |
| |
| - (void)dismissModals { |
| [self.NTPMediator dismissModals]; |
| } |
| |
| - (UIEdgeInsets)contentInset { |
| return self.suggestionsViewController.collectionView.contentInset; |
| } |
| |
| - (CGPoint)contentOffset { |
| CGPoint collectionOffset = |
| self.suggestionsViewController.collectionView.contentOffset; |
| collectionOffset.y -= |
| self.headerCollectionInteractionHandler.collectionShiftingOffset; |
| return collectionOffset; |
| } |
| |
| - (void)willUpdateSnapshot { |
| DCHECK(!IsRefactoredNTP()); |
| [self.suggestionsViewController clearOverscroll]; |
| } |
| |
| - (void)reload { |
| if (IsDiscoverFeedEnabled() && !IsRefactoredNTP()) { |
| DCHECK(!IsRefactoredNTP()); |
| ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider()->RefreshFeed(); |
| } |
| [self.contentSuggestionsMediator.dataSink reloadAllData]; |
| } |
| |
| - (void)locationBarDidBecomeFirstResponder { |
| [self.NTPMediator locationBarDidBecomeFirstResponder]; |
| } |
| - (void)locationBarDidResignFirstResponder { |
| [self.NTPMediator locationBarDidResignFirstResponder]; |
| } |
| |
| #pragma mark - ContentSuggestionsMenuProvider |
| |
| - (UIContextMenuConfiguration*)contextMenuConfigurationForItem: |
| (ContentSuggestionsMostVisitedItem*)item |
| fromView:(UIView*)view |
| API_AVAILABLE(ios(13.0)) { |
| __weak __typeof(self) weakSelf = self; |
| |
| UIContextMenuActionProvider actionProvider = |
| ^(NSArray<UIMenuElement*>* suggestedActions) { |
| if (!weakSelf) { |
| // Return an empty menu. |
| return [UIMenu menuWithTitle:@"" children:@[]]; |
| } |
| |
| ContentSuggestionsCoordinator* strongSelf = weakSelf; |
| |
| // Record that this context menu was shown to the user. |
| RecordMenuShown(MenuScenario::kMostVisitedEntry); |
| |
| ActionFactory* actionFactory = [[ActionFactory alloc] |
| initWithBrowser:strongSelf.browser |
| scenario:MenuScenario::kMostVisitedEntry]; |
| |
| NSMutableArray<UIMenuElement*>* menuElements = |
| [[NSMutableArray alloc] init]; |
| |
| NSIndexPath* indexPath = |
| [self.suggestionsViewController.collectionViewModel |
| indexPathForItem:item]; |
| |
| [menuElements addObject:[actionFactory actionToOpenInNewTabWithBlock:^{ |
| [weakSelf.NTPMediator |
| openNewTabWithMostVisitedItem:item |
| incognito:NO |
| atIndex:indexPath.item]; |
| }]]; |
| |
| [menuElements |
| addObject:[actionFactory actionToOpenInNewIncognitoTabWithBlock:^{ |
| [weakSelf.NTPMediator |
| openNewTabWithMostVisitedItem:item |
| incognito:YES |
| atIndex:indexPath.item]; |
| }]]; |
| |
| if (IsMultipleScenesSupported()) { |
| UIAction* newWindowAction = [actionFactory |
| actionToOpenInNewWindowWithURL:item.URL |
| activityOrigin: |
| WindowActivityContentSuggestionsOrigin]; |
| [menuElements addObject:newWindowAction]; |
| } |
| |
| [menuElements addObject:[actionFactory actionToCopyURL:item.URL]]; |
| |
| [menuElements addObject:[actionFactory actionToShareWithBlock:^{ |
| [weakSelf shareURL:item.URL |
| title:item.title |
| fromView:view]; |
| }]]; |
| |
| [menuElements addObject:[actionFactory actionToRemoveWithBlock:^{ |
| [weakSelf.NTPMediator removeMostVisited:item]; |
| }]]; |
| |
| return [UIMenu menuWithTitle:@"" children:menuElements]; |
| }; |
| return |
| [UIContextMenuConfiguration configurationWithIdentifier:nil |
| previewProvider:nil |
| actionProvider:actionProvider]; |
| } |
| |
| #pragma mark - Helpers |
| |
| // Creates, configures and returns a DiscoverFeed ViewController. |
| - (UIViewController*)discoverFeed { |
| if (!IsDiscoverFeedEnabled()) |
| return nil; |
| |
| UIViewController* discoverFeed = ios::GetChromeBrowserProvider() |
| ->GetDiscoverFeedProvider() |
| ->NewFeedViewController(self.browser); |
| // TODO(crbug.com/1085419): Once the CollectionView is cleanly exposed, remove |
| // this loop. |
| for (UIView* view in discoverFeed.view.subviews) { |
| if ([view isKindOfClass:[UICollectionView class]]) { |
| UICollectionView* feedView = static_cast<UICollectionView*>(view); |
| feedView.bounces = NO; |
| feedView.alwaysBounceVertical = NO; |
| feedView.scrollEnabled = NO; |
| } |
| } |
| return discoverFeed; |
| } |
| |
| // Triggers the URL sharing flow for the given |URL| and |title|, with the |
| // origin |view| representing the UI component for that URL. |
| - (void)shareURL:(const GURL&)URL |
| title:(NSString*)title |
| fromView:(UIView*)view { |
| ActivityParams* params = |
| [[ActivityParams alloc] initWithURL:URL |
| title:title |
| scenario:ActivityScenario::MostVisitedEntry]; |
| self.sharingCoordinator = |
| [[SharingCoordinator alloc] initWithBaseViewController:self.viewController |
| browser:self.browser |
| params:params |
| originView:view]; |
| [self.sharingCoordinator start]; |
| } |
| |
| // Toggles Discover feed visibility between hidden or expanded. |
| - (void)setDiscoverFeedVisible:(BOOL)visible { |
| [self.contentSuggestionsVisible setValue:visible]; |
| [self.discoverFeedHeaderDelegate changeDiscoverFeedHeaderVisibility:visible]; |
| [self.contentSuggestionsMediator reloadAllData]; |
| [self.discoverFeedMetricsRecorder |
| recordDiscoverFeedVisibilityChanged:visible]; |
| } |
| |
| @end |