1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.h"
6
7#include "base/mac/foundation_util.h"
8#include "base/metrics/user_metrics.h"
9#include "base/metrics/user_metrics_action.h"
10#include "base/scoped_observer.h"
11#import "components/feature_engagement/public/event_constants.h"
12#import "components/feature_engagement/public/tracker.h"
13#include "components/feed/core/shared_prefs/pref_names.h"
14#include "components/ntp_snippets/content_suggestions_service.h"
15#include "components/ntp_snippets/pref_names.h"
16#include "components/ntp_snippets/remote/remote_suggestions_scheduler.h"
17#include "components/ntp_tiles/most_visited_sites.h"
18#include "components/prefs/pref_service.h"
19#import "components/search_engines/template_url.h"
20#import "components/search_engines/template_url_service.h"
21#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
22#include "ios/chrome/browser/discover_feed/discover_feed_service.h"
23#include "ios/chrome/browser/discover_feed/discover_feed_service_factory.h"
24#include "ios/chrome/browser/drag_and_drop/drag_and_drop_flag.h"
25#import "ios/chrome/browser/drag_and_drop/url_drag_drop_handler.h"
26#include "ios/chrome/browser/favicon/ios_chrome_large_icon_cache_factory.h"
27#include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h"
28#include "ios/chrome/browser/favicon/large_icon_cache.h"
29#import "ios/chrome/browser/feature_engagement/tracker_factory.h"
30#import "ios/chrome/browser/main/browser.h"
31#include "ios/chrome/browser/ntp_snippets/ios_chrome_content_suggestions_service_factory.h"
32#include "ios/chrome/browser/ntp_tiles/ios_most_visited_sites_factory.h"
33#include "ios/chrome/browser/pref_names.h"
34#include "ios/chrome/browser/reading_list/reading_list_model_factory.h"
35#import "ios/chrome/browser/search_engines/template_url_service_factory.h"
36#import "ios/chrome/browser/signin/authentication_service.h"
37#import "ios/chrome/browser/signin/authentication_service_factory.h"
38#include "ios/chrome/browser/signin/identity_manager_factory.h"
39#import "ios/chrome/browser/ui/activity_services/activity_params.h"
40#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h"
41#import "ios/chrome/browser/ui/commands/application_commands.h"
42#import "ios/chrome/browser/ui/commands/browser_commands.h"
43#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
44#import "ios/chrome/browser/ui/commands/open_new_tab_command.h"
45#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h"
46#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_action_handler.h"
47#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_data_sink.h"
48#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_feature.h"
49#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_synchronizer.h"
50#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_view_controller.h"
51#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h"
52#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_menu_provider.h"
53#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_recorder.h"
54#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.h"
55#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h"
56#import "ios/chrome/browser/ui/content_suggestions/discover_feed_delegate.h"
57#import "ios/chrome/browser/ui/content_suggestions/discover_feed_header_changing.h"
58#import "ios/chrome/browser/ui/content_suggestions/discover_feed_menu_commands.h"
59#import "ios/chrome/browser/ui/content_suggestions/discover_feed_metrics_recorder.h"
60#import "ios/chrome/browser/ui/content_suggestions/ntp_home_constant.h"
61#import "ios/chrome/browser/ui/content_suggestions/ntp_home_mediator.h"
62#import "ios/chrome/browser/ui/content_suggestions/ntp_home_metrics.h"
63#import "ios/chrome/browser/ui/content_suggestions/theme_change_delegate.h"
64#import "ios/chrome/browser/ui/menu/action_factory.h"
65#import "ios/chrome/browser/ui/menu/menu_histograms.h"
66#import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h"
67#import "ios/chrome/browser/ui/ntp/notification_promo_whats_new.h"
68#import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.h"
69#import "ios/chrome/browser/ui/settings/utils/pref_backed_boolean.h"
70#import "ios/chrome/browser/ui/sharing/sharing_coordinator.h"
71#import "ios/chrome/browser/ui/util/multi_window_support.h"
72#import "ios/chrome/browser/ui/util/named_guide.h"
73#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
74#import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
75#import "ios/chrome/browser/url_loading/url_loading_params.h"
76#import "ios/chrome/browser/voice/voice_search_availability.h"
77#include "ios/chrome/grit/ios_strings.h"
78#import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
79#import "ios/public/provider/chrome/browser/discover_feed/discover_feed_provider.h"
80#import "ios/web/public/navigation/navigation_item.h"
81#import "ios/web/public/navigation/navigation_manager.h"
82#import "ios/web/public/web_state.h"
83#import "ui/base/l10n/l10n_util_mac.h"
84
85#if !defined(__has_feature) || !__has_feature(objc_arc)
86#error "This file requires ARC support."
87#endif
88
89@interface ContentSuggestionsCoordinator () <
90    ContentSuggestionsActionHandler,
91    ContentSuggestionsMenuProvider,
92    ContentSuggestionsViewControllerAudience,
93    DiscoverFeedDelegate,
94    DiscoverFeedMenuCommands,
95    OverscrollActionsControllerDelegate,
96    ThemeChangeDelegate,
97    URLDropDelegate> {
98  // Helper object managing the availability of the voice search feature.
99  VoiceSearchAvailability _voiceSearchAvailability;
100}
101
102@property(nonatomic, strong)
103    ContentSuggestionsViewController* suggestionsViewController;
104@property(nonatomic, strong)
105    ContentSuggestionsMediator* contentSuggestionsMediator;
106@property(nonatomic, strong)
107    ContentSuggestionsHeaderSynchronizer* headerCollectionInteractionHandler;
108@property(nonatomic, strong) ContentSuggestionsMetricsRecorder* metricsRecorder;
109@property(nonatomic, strong)
110    DiscoverFeedMetricsRecorder* discoverFeedMetricsRecorder;
111@property(nonatomic, strong) NTPHomeMediator* NTPMediator;
112@property(nonatomic, strong) UIViewController* discoverFeedViewController;
113@property(nonatomic, strong) UIView* discoverFeedHeaderMenuButton;
114@property(nonatomic, strong) URLDragDropHandler* dragDropHandler;
115@property(nonatomic, strong) ActionSheetCoordinator* alertCoordinator;
116// Redefined as readwrite.
117@property(nonatomic, strong, readwrite)
118    ContentSuggestionsHeaderViewController* headerController;
119@property(nonatomic, strong) PrefBackedBoolean* contentSuggestionsVisible;
120// Delegate for handling Discover feed header UI changes.
121@property(nonatomic, weak) id<DiscoverFeedHeaderChanging>
122    discoverFeedHeaderDelegate;
123// Authentication Service for the user's signed-in state.
124@property(nonatomic, assign) AuthenticationService* authService;
125// Coordinator in charge of handling sharing use cases.
126@property(nonatomic, strong) SharingCoordinator* sharingCoordinator;
127// YES if the feedShown method has already been called.
128// TODO(crbug.com/1126940): The coordinator shouldn't be keeping track of this
129// for its |self.discoverFeedViewController| remove once we have an appropriate
130// callback.
131@property(nonatomic, assign) BOOL feedShownWasCalled;
132
133@end
134
135@implementation ContentSuggestionsCoordinator
136
137- (void)start {
138  DCHECK(self.browser);
139  if (self.visible) {
140    // Prevent this coordinator from being started twice in a row
141    return;
142  }
143
144  _visible = YES;
145
146  self.authService = AuthenticationServiceFactory::GetForBrowserState(
147      self.browser->GetBrowserState());
148  self.authService->WaitUntilCacheIsPopulated();
149
150  ntp_snippets::ContentSuggestionsService* contentSuggestionsService =
151      IOSChromeContentSuggestionsServiceFactory::GetForBrowserState(
152          self.browser->GetBrowserState());
153  contentSuggestionsService->remote_suggestions_scheduler()
154      ->OnSuggestionsSurfaceOpened();
155  contentSuggestionsService->user_classifier()->OnEvent(
156      ntp_snippets::UserClassifier::Metric::NTP_OPENED);
157  contentSuggestionsService->user_classifier()->OnEvent(
158      ntp_snippets::UserClassifier::Metric::SUGGESTIONS_SHOWN);
159  PrefService* prefs =
160      ChromeBrowserState::FromBrowserState(self.browser->GetBrowserState())
161          ->GetPrefs();
162  bool contentSuggestionsEnabled =
163      prefs->GetBoolean(prefs::kArticlesForYouEnabled);
164  self.contentSuggestionsVisible = [[PrefBackedBoolean alloc]
165      initWithPrefService:prefs
166                 prefName:feed::prefs::kArticlesListVisible];
167  if (contentSuggestionsEnabled) {
168    if ([self.contentSuggestionsVisible value]) {
169      ntp_home::RecordNTPImpression(ntp_home::REMOTE_SUGGESTIONS);
170    } else {
171      ntp_home::RecordNTPImpression(ntp_home::REMOTE_COLLAPSED);
172    }
173  } else {
174    ntp_home::RecordNTPImpression(ntp_home::LOCAL_SUGGESTIONS);
175  }
176
177  TemplateURLService* templateURLService =
178      ios::TemplateURLServiceFactory::GetForBrowserState(
179          self.browser->GetBrowserState());
180
181  self.NTPMediator = [[NTPHomeMediator alloc]
182             initWithWebState:self.webState
183           templateURLService:templateURLService
184                    URLLoader:UrlLoadingBrowserAgent::FromBrowser(self.browser)
185                  authService:self.authService
186              identityManager:IdentityManagerFactory::GetForBrowserState(
187                                  self.browser->GetBrowserState())
188                   logoVendor:ios::GetChromeBrowserProvider()->CreateLogoVendor(
189                                  self.browser, self.webState)
190      voiceSearchAvailability:&_voiceSearchAvailability];
191  self.NTPMediator.browser = self.browser;
192
193  self.headerController = [[ContentSuggestionsHeaderViewController alloc] init];
194  // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
195  // clean up.
196  self.headerController.dispatcher =
197      static_cast<id<ApplicationCommands, BrowserCommands, OmniboxCommands,
198                     FakeboxFocuser>>(self.browser->GetCommandDispatcher());
199  self.headerController.commandHandler = self.NTPMediator;
200  self.headerController.delegate = self.NTPMediator;
201
202  self.headerController.readingListModel =
203      ReadingListModelFactory::GetForBrowserState(
204          self.browser->GetBrowserState());
205  self.headerController.toolbarDelegate = self.toolbarDelegate;
206
207  favicon::LargeIconService* largeIconService =
208      IOSChromeLargeIconServiceFactory::GetForBrowserState(
209          self.browser->GetBrowserState());
210  LargeIconCache* cache = IOSChromeLargeIconCacheFactory::GetForBrowserState(
211      self.browser->GetBrowserState());
212  std::unique_ptr<ntp_tiles::MostVisitedSites> mostVisitedFactory =
213      IOSMostVisitedSitesFactory::NewForBrowserState(
214          self.browser->GetBrowserState());
215  ReadingListModel* readingListModel =
216      ReadingListModelFactory::GetForBrowserState(
217          self.browser->GetBrowserState());
218
219  if (IsDiscoverFeedEnabled()) {
220    // Creating the DiscoverFeedService will start the DiscoverFeed.
221    DiscoverFeedService* discoverFeedService =
222        DiscoverFeedServiceFactory::GetForBrowserState(
223            self.browser->GetBrowserState());
224    self.discoverFeedMetricsRecorder =
225        discoverFeedService->GetDiscoverFeedMetricsRecorder();
226  }
227  self.discoverFeedViewController = [self discoverFeed];
228
229  BOOL isGoogleDefaultSearchProvider =
230      templateURLService->GetDefaultSearchProvider()->GetEngineType(
231          templateURLService->search_terms_data()) == SEARCH_ENGINE_GOOGLE;
232
233  self.contentSuggestionsMediator = [[ContentSuggestionsMediator alloc]
234             initWithContentService:contentSuggestionsService
235                   largeIconService:largeIconService
236                     largeIconCache:cache
237                    mostVisitedSite:std::move(mostVisitedFactory)
238                   readingListModel:readingListModel
239                        prefService:prefs
240                       discoverFeed:self.discoverFeedViewController
241      isGoogleDefaultSearchProvider:isGoogleDefaultSearchProvider];
242  self.contentSuggestionsMediator.commandHandler = self.NTPMediator;
243  self.contentSuggestionsMediator.headerProvider = self.headerController;
244  self.contentSuggestionsMediator.contentArticlesExpanded =
245      self.contentSuggestionsVisible;
246  self.contentSuggestionsMediator.discoverFeedDelegate = self;
247
248  self.headerController.promoCanShow =
249      [self.contentSuggestionsMediator notificationPromo]->CanShow();
250
251  self.metricsRecorder = [[ContentSuggestionsMetricsRecorder alloc] init];
252  self.metricsRecorder.delegate = self.contentSuggestionsMediator;
253
254
255  // Offset to maintain Discover feed scroll position.
256  CGFloat offset = 0;
257  if (IsDiscoverFeedEnabled() && contentSuggestionsEnabled) {
258    web::NavigationManager* navigationManager =
259        self.webState->GetNavigationManager();
260    web::NavigationItem* item = navigationManager->GetVisibleItem();
261    if (item) {
262      offset = item->GetPageDisplayState().scroll_state().content_offset().y;
263    }
264  }
265
266  self.suggestionsViewController = [[ContentSuggestionsViewController alloc]
267      initWithStyle:CollectionViewControllerStyleDefault
268             offset:offset];
269  [self.suggestionsViewController
270      setDataSource:self.contentSuggestionsMediator];
271  self.suggestionsViewController.suggestionCommandHandler = self.NTPMediator;
272  self.suggestionsViewController.audience = self;
273  self.suggestionsViewController.overscrollDelegate = self;
274  self.suggestionsViewController.themeChangeDelegate = self;
275  self.suggestionsViewController.metricsRecorder = self.metricsRecorder;
276  id<SnackbarCommands> dispatcher =
277      static_cast<id<SnackbarCommands>>(self.browser->GetCommandDispatcher());
278  self.suggestionsViewController.dispatcher = dispatcher;
279  self.suggestionsViewController.discoverFeedMenuHandler = self;
280  self.suggestionsViewController.discoverFeedMetricsRecorder =
281      self.discoverFeedMetricsRecorder;
282
283  self.discoverFeedHeaderDelegate =
284      self.suggestionsViewController.discoverFeedHeaderDelegate;
285  [self.discoverFeedHeaderDelegate
286      changeDiscoverFeedHeaderVisibility:[self.contentSuggestionsVisible
287                                                 value]];
288  self.suggestionsViewController.contentSuggestionsEnabled =
289      prefs->FindPreference(prefs::kArticlesForYouEnabled);
290  self.suggestionsViewController.handler = self;
291  self.contentSuggestionsMediator.consumer = self.suggestionsViewController;
292
293  if (@available(iOS 13.0, *)) {
294    self.suggestionsViewController.menuProvider = self;
295  }
296
297  self.NTPMediator.consumer = self.headerController;
298  // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
299  // clean up.
300  self.NTPMediator.dispatcher =
301      static_cast<id<ApplicationCommands, BrowserCommands, OmniboxCommands,
302                     SnackbarCommands>>(self.browser->GetCommandDispatcher());
303  self.NTPMediator.NTPMetrics = [[NTPHomeMetrics alloc]
304      initWithBrowserState:self.browser->GetBrowserState()
305                  webState:self.webState];
306  self.NTPMediator.metricsRecorder = self.metricsRecorder;
307  self.NTPMediator.suggestionsViewController = self.suggestionsViewController;
308  self.NTPMediator.suggestionsMediator = self.contentSuggestionsMediator;
309  self.NTPMediator.suggestionsService = contentSuggestionsService;
310  [self.NTPMediator setUp];
311  self.NTPMediator.discoverFeedMetrics = self.discoverFeedMetricsRecorder;
312
313  [self.suggestionsViewController addChildViewController:self.headerController];
314  [self.headerController
315      didMoveToParentViewController:self.suggestionsViewController];
316
317  self.headerCollectionInteractionHandler =
318      [[ContentSuggestionsHeaderSynchronizer alloc]
319          initWithCollectionController:self.suggestionsViewController
320                      headerController:self.headerController];
321  self.NTPMediator.headerCollectionInteractionHandler =
322      self.headerCollectionInteractionHandler;
323
324  if (DragAndDropIsEnabled()) {
325    self.dragDropHandler = [[URLDragDropHandler alloc] init];
326    self.dragDropHandler.dropDelegate = self;
327    [self.suggestionsViewController.collectionView
328        addInteraction:[[UIDropInteraction alloc]
329                           initWithDelegate:self.dragDropHandler]];
330  }
331}
332
333- (void)stop {
334  [self.NTPMediator shutdown];
335  self.NTPMediator = nil;
336  [self.contentSuggestionsMediator disconnect];
337  self.contentSuggestionsMediator = nil;
338  [self.sharingCoordinator stop];
339  self.sharingCoordinator = nil;
340  self.headerController = nil;
341  if (IsDiscoverFeedEnabled()) {
342    ios::GetChromeBrowserProvider()
343        ->GetDiscoverFeedProvider()
344        ->RemoveFeedViewController(self.discoverFeedViewController);
345  }
346  _visible = NO;
347}
348
349- (UIViewController*)viewController {
350  return self.suggestionsViewController;
351}
352
353- (void)constrainDiscoverHeaderMenuButtonNamedGuide {
354  NamedGuide* menuButtonGuide =
355      [NamedGuide guideWithName:kDiscoverFeedHeaderMenuGuide
356                           view:self.discoverFeedHeaderMenuButton];
357
358  menuButtonGuide.constrainedView = self.discoverFeedHeaderMenuButton;
359}
360
361#pragma mark - ContentSuggestionsViewControllerAudience
362
363- (void)promoShown {
364  NotificationPromoWhatsNew* notificationPromo =
365      [self.contentSuggestionsMediator notificationPromo];
366  notificationPromo->HandleViewed();
367  [self.headerController setPromoCanShow:notificationPromo->CanShow()];
368}
369
370- (void)discoverHeaderMenuButtonShown:(UIView*)menuButton {
371  _discoverFeedHeaderMenuButton = menuButton;
372}
373
374- (void)discoverFeedShown {
375  if (IsDiscoverFeedEnabled() && !self.feedShownWasCalled) {
376    ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider()->FeedWasShown();
377    self.feedShownWasCalled = YES;
378  }
379}
380
381#pragma mark - OverscrollActionsControllerDelegate
382
383- (void)overscrollActionsController:(OverscrollActionsController*)controller
384                   didTriggerAction:(OverscrollAction)action {
385  // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
386  // clean up.
387  id<ApplicationCommands, BrowserCommands, OmniboxCommands, SnackbarCommands>
388      handler = static_cast<id<ApplicationCommands, BrowserCommands,
389                               OmniboxCommands, SnackbarCommands>>(
390          self.browser->GetCommandDispatcher());
391  switch (action) {
392    case OverscrollAction::NEW_TAB: {
393      [handler openURLInNewTab:[OpenNewTabCommand command]];
394    } break;
395    case OverscrollAction::CLOSE_TAB: {
396      [handler closeCurrentTab];
397      base::RecordAction(base::UserMetricsAction("OverscrollActionCloseTab"));
398    } break;
399    case OverscrollAction::REFRESH:
400      [self reload];
401      break;
402    case OverscrollAction::NONE:
403      NOTREACHED();
404      break;
405  }
406}
407
408- (BOOL)shouldAllowOverscrollActionsForOverscrollActionsController:
409    (OverscrollActionsController*)controller {
410  return YES;
411}
412
413- (UIView*)toolbarSnapshotViewForOverscrollActionsController:
414    (OverscrollActionsController*)controller {
415  return
416      [[self.headerController toolBarView] snapshotViewAfterScreenUpdates:NO];
417}
418
419- (UIView*)headerViewForOverscrollActionsController:
420    (OverscrollActionsController*)controller {
421  return self.suggestionsViewController.view;
422}
423
424- (CGFloat)headerInsetForOverscrollActionsController:
425    (OverscrollActionsController*)controller {
426  return 0;
427}
428
429- (CGFloat)headerHeightForOverscrollActionsController:
430    (OverscrollActionsController*)controller {
431  CGFloat height = [self.headerController toolBarView].bounds.size.height;
432  CGFloat topInset = self.suggestionsViewController.view.safeAreaInsets.top;
433  return height + topInset;
434}
435
436- (FullscreenController*)fullscreenControllerForOverscrollActionsController:
437    (OverscrollActionsController*)controller {
438  // Fullscreen isn't supported here.
439  return nullptr;
440}
441
442#pragma mark - ThemeChangeDelegate
443
444- (void)handleThemeChange {
445  if (IsDiscoverFeedEnabled()) {
446    ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider()->UpdateTheme();
447  }
448}
449
450#pragma mark - URLDropDelegate
451
452- (BOOL)canHandleURLDropInView:(UIView*)view {
453  return YES;
454}
455
456- (void)view:(UIView*)view didDropURL:(const GURL&)URL atPoint:(CGPoint)point {
457  UrlLoadingBrowserAgent::FromBrowser(self.browser)
458      ->Load(UrlLoadParams::InCurrentTab(URL));
459}
460
461#pragma mark - DiscoverFeedMenuCommands
462
463- (void)openDiscoverFeedMenu {
464  [self.alertCoordinator stop];
465  self.alertCoordinator = nil;
466
467  self.alertCoordinator = [[ActionSheetCoordinator alloc]
468      initWithBaseViewController:self.suggestionsViewController
469                         browser:self.browser
470                           title:nil
471                         message:nil
472                            rect:self.discoverFeedHeaderMenuButton.frame
473                            view:self.discoverFeedHeaderMenuButton.superview];
474  __weak ContentSuggestionsCoordinator* weakSelf = self;
475
476  if ([self.contentSuggestionsVisible value]) {
477    [self.alertCoordinator
478        addItemWithTitle:l10n_util::GetNSString(
479                             IDS_IOS_DISCOVER_FEED_MENU_TURN_OFF_ITEM)
480                  action:^{
481                    [weakSelf setDiscoverFeedVisible:NO];
482                  }
483                   style:UIAlertActionStyleDestructive];
484  } else {
485    [self.alertCoordinator
486        addItemWithTitle:l10n_util::GetNSString(
487                             IDS_IOS_DISCOVER_FEED_MENU_TURN_ON_ITEM)
488                  action:^{
489                    [weakSelf setDiscoverFeedVisible:YES];
490                  }
491                   style:UIAlertActionStyleDefault];
492  }
493
494  if (self.authService->IsAuthenticated()) {
495    [self.alertCoordinator
496        addItemWithTitle:l10n_util::GetNSString(
497                             IDS_IOS_DISCOVER_FEED_MENU_MANAGE_ACTIVITY_ITEM)
498                  action:^{
499                    [weakSelf.NTPMediator handleFeedManageActivityTapped];
500                  }
501                   style:UIAlertActionStyleDefault];
502
503    [self.alertCoordinator
504        addItemWithTitle:l10n_util::GetNSString(
505                             IDS_IOS_DISCOVER_FEED_MENU_MANAGE_INTERESTS_ITEM)
506                  action:^{
507                    [weakSelf.NTPMediator handleFeedManageInterestsTapped];
508                  }
509                   style:UIAlertActionStyleDefault];
510  }
511
512  [self.alertCoordinator
513      addItemWithTitle:l10n_util::GetNSString(
514                           IDS_IOS_DISCOVER_FEED_MENU_LEARN_MORE_ITEM)
515                action:^{
516                  [weakSelf.NTPMediator handleFeedLearnMoreTapped];
517                }
518                 style:UIAlertActionStyleDefault];
519  [self.alertCoordinator start];
520}
521
522- (void)notifyFeedLoadedForHeaderMenu {
523  feature_engagement::TrackerFactory::GetForBrowserState(
524      self.browser->GetBrowserState())
525      ->NotifyEvent(feature_engagement::events::kDiscoverFeedLoaded);
526}
527
528#pragma mark - DiscoverFeedDelegate
529
530- (void)recreateDiscoverFeedViewController {
531  DCHECK(IsDiscoverFeedEnabled());
532
533  // Create and set a new DiscoverFeed since that its model has changed.
534  self.discoverFeedViewController = [self discoverFeed];
535  self.contentSuggestionsMediator.discoverFeed =
536      self.discoverFeedViewController;
537  [self.alertCoordinator stop];
538}
539
540#pragma mark - ContentSuggestionsActionHandler
541
542- (void)loadMoreFeedArticles {
543  ios::GetChromeBrowserProvider()
544      ->GetDiscoverFeedProvider()
545      ->LoadMoreFeedArticles();
546  [self.discoverFeedMetricsRecorder recordInfiniteFeedTriggered];
547}
548
549#pragma mark - Public methods
550
551- (UIView*)view {
552  return self.suggestionsViewController.view;
553}
554
555- (void)dismissModals {
556  [self.NTPMediator dismissModals];
557}
558
559- (UIEdgeInsets)contentInset {
560  return self.suggestionsViewController.collectionView.contentInset;
561}
562
563- (CGPoint)contentOffset {
564  CGPoint collectionOffset =
565      self.suggestionsViewController.collectionView.contentOffset;
566  collectionOffset.y -=
567      self.headerCollectionInteractionHandler.collectionShiftingOffset;
568  return collectionOffset;
569}
570
571- (void)willUpdateSnapshot {
572  [self.suggestionsViewController clearOverscroll];
573}
574
575- (void)reload {
576  if (IsDiscoverFeedEnabled())
577    ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider()->RefreshFeed();
578  [self.contentSuggestionsMediator.dataSink reloadAllData];
579}
580
581- (void)locationBarDidBecomeFirstResponder {
582  [self.NTPMediator locationBarDidBecomeFirstResponder];
583}
584- (void)locationBarDidResignFirstResponder {
585  [self.NTPMediator locationBarDidResignFirstResponder];
586}
587
588#pragma mark - ContentSuggestionsMenuProvider
589
590- (UIContextMenuConfiguration*)contextMenuConfigurationForItem:
591                                   (ContentSuggestionsMostVisitedItem*)item
592                                                      fromView:(UIView*)view
593    API_AVAILABLE(ios(13.0)) {
594  __weak __typeof(self) weakSelf = self;
595
596  UIContextMenuActionProvider actionProvider =
597      ^(NSArray<UIMenuElement*>* suggestedActions) {
598        if (!weakSelf) {
599          // Return an empty menu.
600          return [UIMenu menuWithTitle:@"" children:@[]];
601        }
602
603        ContentSuggestionsCoordinator* strongSelf = weakSelf;
604
605        // Record that this context menu was shown to the user.
606        RecordMenuShown(MenuScenario::kMostVisitedEntry);
607
608        ActionFactory* actionFactory = [[ActionFactory alloc]
609            initWithBrowser:strongSelf.browser
610                   scenario:MenuScenario::kMostVisitedEntry];
611
612        NSMutableArray<UIMenuElement*>* menuElements =
613            [[NSMutableArray alloc] init];
614
615        NSIndexPath* indexPath =
616            [self.suggestionsViewController.collectionViewModel
617                indexPathForItem:item];
618
619        [menuElements addObject:[actionFactory actionToOpenInNewTabWithBlock:^{
620                        [weakSelf.NTPMediator
621                            openNewTabWithMostVisitedItem:item
622                                                incognito:NO
623                                                  atIndex:indexPath.item];
624                      }]];
625
626        [menuElements
627            addObject:[actionFactory actionToOpenInNewIncognitoTabWithBlock:^{
628              [weakSelf.NTPMediator
629                  openNewTabWithMostVisitedItem:item
630                                      incognito:YES
631                                        atIndex:indexPath.item];
632            }]];
633
634        if (IsMultipleScenesSupported()) {
635          [menuElements
636              addObject:
637                  [actionFactory
638                      actionToOpenInNewWindowWithURL:item.URL
639                                      activityOrigin:
640                                          WindowActivityContentSuggestionsOrigin
641                                          completion:nil]];
642        }
643
644        [menuElements addObject:[actionFactory actionToCopyURL:item.URL]];
645
646        [menuElements addObject:[actionFactory actionToShareWithBlock:^{
647                        [weakSelf shareURL:item.URL
648                                     title:item.title
649                                  fromView:view];
650                      }]];
651
652        [menuElements addObject:[actionFactory actionToRemoveWithBlock:^{
653                        [weakSelf.NTPMediator removeMostVisited:item];
654                      }]];
655
656        return [UIMenu menuWithTitle:@"" children:menuElements];
657      };
658  return
659      [UIContextMenuConfiguration configurationWithIdentifier:nil
660                                              previewProvider:nil
661                                               actionProvider:actionProvider];
662}
663
664#pragma mark - Helpers
665
666// Creates, configures and returns a DiscoverFeed ViewController.
667- (UIViewController*)discoverFeed {
668  if (!IsDiscoverFeedEnabled())
669    return nil;
670
671  UIViewController* discoverFeed = ios::GetChromeBrowserProvider()
672                                       ->GetDiscoverFeedProvider()
673                                       ->NewFeedViewController(self.browser);
674  // TODO(crbug.com/1085419): Once the CollectionView is cleanly exposed, remove
675  // this loop.
676  for (UIView* view in discoverFeed.view.subviews) {
677    if ([view isKindOfClass:[UICollectionView class]]) {
678      UICollectionView* feedView = static_cast<UICollectionView*>(view);
679      feedView.bounces = NO;
680      feedView.alwaysBounceVertical = NO;
681      feedView.scrollEnabled = NO;
682    }
683  }
684  return discoverFeed;
685}
686
687// Triggers the URL sharing flow for the given |URL| and |title|, with the
688// origin |view| representing the UI component for that URL.
689- (void)shareURL:(const GURL&)URL
690           title:(NSString*)title
691        fromView:(UIView*)view {
692  ActivityParams* params =
693      [[ActivityParams alloc] initWithURL:URL
694                                    title:title
695                                 scenario:ActivityScenario::MostVisitedEntry];
696  self.sharingCoordinator =
697      [[SharingCoordinator alloc] initWithBaseViewController:self.viewController
698                                                     browser:self.browser
699                                                      params:params
700                                                  originView:view];
701  [self.sharingCoordinator start];
702}
703
704// Toggles Discover feed visibility between hidden or expanded.
705- (void)setDiscoverFeedVisible:(BOOL)visible {
706  [self.contentSuggestionsVisible setValue:visible];
707  [self.discoverFeedHeaderDelegate changeDiscoverFeedHeaderVisibility:visible];
708  [self.contentSuggestionsMediator reloadAllData];
709  [self.discoverFeedMetricsRecorder
710      recordDiscoverFeedVisibilityChanged:visible];
711}
712
713@end
714