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