| // 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_mediator.h" |
| |
| #include "base/bind.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/favicon/ios/web_favicon_driver.h" |
| #include "components/ntp_snippets/category.h" |
| #include "components/ntp_snippets/category_info.h" |
| #include "components/ntp_tiles/metrics.h" |
| #include "components/ntp_tiles/most_visited_sites.h" |
| #include "components/ntp_tiles/ntp_tile.h" |
| #import "components/pref_registry/pref_registry_syncable.h" |
| #include "components/reading_list/core/reading_list_model.h" |
| #import "components/reading_list/ios/reading_list_model_bridge_observer.h" |
| #include "ios/chrome/browser/application_context.h" |
| #include "ios/chrome/browser/ntp_tiles/most_visited_sites_observer_bridge.h" |
| #import "ios/chrome/browser/policy/policy_util.h" |
| #import "ios/chrome/browser/pref_names.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_action_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_whats_new_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/suggested_content.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_category_wrapper.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_commands.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_data_sink.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_favicon_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_feature.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_provider.h" |
| #import "ios/chrome/browser/ui/content_suggestions/identifier/content_suggestions_section_information.h" |
| #import "ios/chrome/browser/ui/content_suggestions/mediator_util.h" |
| #import "ios/chrome/browser/ui/ntp/discover_feed_delegate.h" |
| #import "ios/chrome/browser/ui/ntp/new_tab_page_feature.h" |
| #import "ios/chrome/browser/ui/ntp/notification_promo_whats_new.h" |
| #include "ios/chrome/browser/ui/ntp/ntp_tile_saver.h" |
| #import "ios/chrome/browser/ui/start_surface/start_surface_features.h" |
| #include "ios/chrome/browser/ui/ui_feature_flags.h" |
| #include "ios/chrome/browser/ui/util/ui_util.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list.h" |
| #include "ios/chrome/common/app_group/app_group_constants.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| using CSCollectionViewItem = CollectionViewItem<SuggestedContent>; |
| |
| // Maximum number of most visited tiles fetched. |
| const NSInteger kMaxNumMostVisitedTiles = 4; |
| |
| } // namespace |
| |
| @interface ContentSuggestionsMediator () <MostVisitedSitesObserving, |
| ReadingListModelBridgeObserver> { |
| std::unique_ptr<ntp_tiles::MostVisitedSites> _mostVisitedSites; |
| std::unique_ptr<ntp_tiles::MostVisitedSitesObserverBridge> _mostVisitedBridge; |
| std::unique_ptr<NotificationPromoWhatsNew> _notificationPromo; |
| std::unique_ptr<ReadingListModelBridge> _readingListModelBridge; |
| } |
| |
| // Whether the contents section should be hidden completely. |
| // Don't use PrefBackedBoolean or PrefMember as this value needs to be checked |
| // when the Preference is updated. |
| @property(nonatomic, assign, readonly) BOOL contentSuggestionsEnabled; |
| |
| // Don't use PrefBackedBoolean or PrefMember as those values needs to be checked |
| // when the Preference is updated. |
| // Whether the suggestions have been disabled in Chrome Settings. |
| @property(nonatomic, assign) |
| const PrefService::Preference* articleForYouEnabled; |
| // Whether the suggestions have been disabled by a policy. |
| @property(nonatomic, assign) |
| const PrefService::Preference* contentSuggestionsPolicyEnabled; |
| |
| // Most visited items from the MostVisitedSites service currently displayed. |
| @property(nonatomic, strong) |
| NSMutableArray<ContentSuggestionsMostVisitedItem*>* mostVisitedItems; |
| @property(nonatomic, strong) |
| NSArray<ContentSuggestionsMostVisitedActionItem*>* actionButtonItems; |
| // Most visited items from the MostVisitedSites service (copied upon receiving |
| // the callback). Those items are up to date with the model. |
| @property(nonatomic, strong) |
| NSMutableArray<ContentSuggestionsMostVisitedItem*>* freshMostVisitedItems; |
| // Section Info for the logo and omnibox section. |
| @property(nonatomic, strong) |
| ContentSuggestionsSectionInformation* logoSectionInfo; |
| // Section Info for the "Return to Recent Tab" section. |
| @property(nonatomic, strong) |
| ContentSuggestionsSectionInformation* returnToRecentTabSectionInfo; |
| // Item for the "Return to Recent Tab" tile. |
| @property(nonatomic, strong) |
| ContentSuggestionsReturnToRecentTabItem* returnToRecentTabItem; |
| // Section Info for the What's New promo section. |
| @property(nonatomic, strong) |
| ContentSuggestionsSectionInformation* promoSectionInfo; |
| // Section Info for the Most Visited section. |
| @property(nonatomic, strong) |
| ContentSuggestionsSectionInformation* mostVisitedSectionInfo; |
| // Section Info for the section containing the Discover feed. |
| @property(nonatomic, strong) |
| ContentSuggestionsSectionInformation* discoverSectionInfo; |
| // Whether the page impression has been recorded. |
| @property(nonatomic, assign) BOOL recordedPageImpression; |
| // Map the section information created to the relevant category. |
| @property(nonatomic, strong, nonnull) |
| NSMutableDictionary<ContentSuggestionsCategoryWrapper*, |
| ContentSuggestionsSectionInformation*>* |
| sectionInformationByCategory; |
| // Mediator fetching the favicons for the items. |
| @property(nonatomic, strong) ContentSuggestionsFaviconMediator* faviconMediator; |
| // Item for the reading list action item. Reference is used to update the |
| // reading list count. |
| @property(nonatomic, strong) |
| ContentSuggestionsMostVisitedActionItem* readingListItem; |
| // Number of unread items in reading list model. |
| @property(nonatomic, assign) NSInteger readingListUnreadCount; |
| // YES if the Return to Recent Tab tile is being shown. |
| @property(nonatomic, assign, getter=mostRecentTabStartSurfaceTileIsShowing) |
| BOOL showMostRecentTabStartSurfaceTile; |
| // Whether the incognito mode is available. |
| @property(nonatomic, assign) BOOL incognitoAvailable; |
| |
| @end |
| |
| @implementation ContentSuggestionsMediator |
| |
| @synthesize dataSink = _dataSink; |
| |
| #pragma mark - Public |
| |
| - (instancetype) |
| initWithLargeIconService:(favicon::LargeIconService*)largeIconService |
| largeIconCache:(LargeIconCache*)largeIconCache |
| mostVisitedSite:(std::unique_ptr<ntp_tiles::MostVisitedSites>) |
| mostVisitedSites |
| readingListModel:(ReadingListModel*)readingListModel |
| prefService:(PrefService*)prefService |
| isGoogleDefaultSearchProvider:(BOOL)isGoogleDefaultSearchProvider { |
| self = [super init]; |
| if (self) { |
| _incognitoAvailable = !IsIncognitoModeDisabled(prefService); |
| _articleForYouEnabled = |
| prefService->FindPreference(prefs::kArticlesForYouEnabled); |
| _contentSuggestionsPolicyEnabled = |
| prefService->FindPreference(prefs::kNTPContentSuggestionsEnabled); |
| |
| _sectionInformationByCategory = [[NSMutableDictionary alloc] init]; |
| |
| _faviconMediator = [[ContentSuggestionsFaviconMediator alloc] |
| initWithLargeIconService:largeIconService |
| largeIconCache:largeIconCache]; |
| |
| _logoSectionInfo = LogoSectionInformation(); |
| _promoSectionInfo = PromoSectionInformation(); |
| _mostVisitedSectionInfo = MostVisitedSectionInformation(); |
| |
| _discoverSectionInfo = |
| DiscoverSectionInformation(isGoogleDefaultSearchProvider); |
| |
| _notificationPromo = std::make_unique<NotificationPromoWhatsNew>( |
| GetApplicationContext()->GetLocalState()); |
| _notificationPromo->Init(); |
| |
| _mostVisitedSites = std::move(mostVisitedSites); |
| _mostVisitedBridge = |
| std::make_unique<ntp_tiles::MostVisitedSitesObserverBridge>(self); |
| _mostVisitedSites->AddMostVisitedURLsObserver(_mostVisitedBridge.get(), |
| kMaxNumMostVisitedTiles); |
| |
| _readingListModelBridge = |
| std::make_unique<ReadingListModelBridge>(self, readingListModel); |
| } |
| return self; |
| } |
| |
| + (void)registerBrowserStatePrefs:(user_prefs::PrefRegistrySyncable*)registry { |
| registry->RegisterInt64Pref(prefs::kIosDiscoverFeedLastRefreshTime, 0); |
| } |
| |
| - (void)disconnect { |
| _mostVisitedBridge.reset(); |
| _mostVisitedSites.reset(); |
| } |
| |
| - (void)reloadAllData { |
| [self.dataSink reloadAllData]; |
| } |
| |
| - (void)blockMostVisitedURL:(GURL)URL { |
| _mostVisitedSites->AddOrRemoveBlockedUrl(URL, true); |
| [self useFreshMostVisited]; |
| } |
| |
| - (void)allowMostVisitedURL:(GURL)URL { |
| _mostVisitedSites->AddOrRemoveBlockedUrl(URL, false); |
| [self useFreshMostVisited]; |
| } |
| |
| - (NotificationPromoWhatsNew*)notificationPromo { |
| return _notificationPromo.get(); |
| } |
| |
| - (void)setDataSink:(id<ContentSuggestionsDataSink>)dataSink { |
| _dataSink = dataSink; |
| self.faviconMediator.dataSink = dataSink; |
| } |
| |
| + (NSUInteger)maxSitesShown { |
| return kMaxNumMostVisitedTiles; |
| } |
| |
| - (void)configureMostRecentTabItemWithWebState:(web::WebState*)webState |
| timeLabel:(NSString*)timeLabel { |
| DCHECK(IsStartSurfaceEnabled()); |
| self.returnToRecentTabSectionInfo = ReturnToRecentTabSectionInformation(); |
| if (!self.returnToRecentTabItem) { |
| self.returnToRecentTabItem = |
| [[ContentSuggestionsReturnToRecentTabItem alloc] initWithType:0]; |
| } |
| |
| // Retrieve favicon associated with the page. |
| favicon::WebFaviconDriver* driver = |
| favicon::WebFaviconDriver::FromWebState(webState); |
| if (driver->FaviconIsValid()) { |
| gfx::Image favicon = driver->GetFavicon(); |
| if (!favicon.IsEmpty()) { |
| self.returnToRecentTabItem.icon = favicon.ToUIImage(); |
| } |
| } |
| if (!self.returnToRecentTabItem.icon) { |
| driver->FetchFavicon(webState->GetLastCommittedURL(), false); |
| } |
| |
| self.returnToRecentTabItem.title = |
| l10n_util::GetNSString(IDS_IOS_RETURN_TO_RECENT_TAB_TITLE); |
| NSString* subtitle = [NSString |
| stringWithFormat:@"%@%@", base::SysUTF16ToNSString(webState->GetTitle()), |
| timeLabel]; |
| self.returnToRecentTabItem.subtitle = subtitle; |
| self.showMostRecentTabStartSurfaceTile = YES; |
| |
| [self.dataSink addSection:self.returnToRecentTabSectionInfo |
| completion:^{ |
| [self.discoverFeedDelegate returnToRecentTabWasAdded]; |
| }]; |
| } |
| |
| - (void)hideRecentTabTile { |
| DCHECK(IsStartSurfaceEnabled()); |
| if (self.showMostRecentTabStartSurfaceTile) { |
| self.showMostRecentTabStartSurfaceTile = NO; |
| [self.dataSink clearSection:self.returnToRecentTabSectionInfo]; |
| } |
| } |
| |
| #pragma mark - StartSurfaceRecentTabObserving |
| |
| - (void)mostRecentTabWasRemoved:(web::WebState*)web_state { |
| DCHECK(IsStartSurfaceEnabled()); |
| [self hideRecentTabTile]; |
| } |
| |
| - (void)mostRecentTabFaviconUpdatedWithImage:(UIImage*)image { |
| if (self.returnToRecentTabItem) { |
| self.returnToRecentTabItem.icon = image; |
| [self.dataSink itemHasChanged:self.returnToRecentTabItem]; |
| } |
| } |
| |
| #pragma mark - ContentSuggestionsDataSource |
| |
| - (NSArray<ContentSuggestionsSectionInformation*>*)sectionsInfo { |
| NSMutableArray<ContentSuggestionsSectionInformation*>* sectionsInfo = |
| [NSMutableArray array]; |
| |
| [sectionsInfo addObject:self.logoSectionInfo]; |
| |
| if (self.showMostRecentTabStartSurfaceTile) { |
| DCHECK(IsStartSurfaceEnabled()); |
| [sectionsInfo addObject:self.returnToRecentTabSectionInfo]; |
| } |
| |
| if (_notificationPromo->CanShow()) { |
| [sectionsInfo addObject:self.promoSectionInfo]; |
| } |
| |
| [sectionsInfo addObject:self.mostVisitedSectionInfo]; |
| |
| if (self.contentSuggestionsEnabled) { |
| [sectionsInfo addObject:self.discoverSectionInfo]; |
| } |
| |
| return sectionsInfo; |
| } |
| |
| - (NSArray<CSCollectionViewItem*>*)itemsForSectionInfo: |
| (ContentSuggestionsSectionInformation*)sectionInfo { |
| NSMutableArray<CSCollectionViewItem*>* convertedSuggestions = |
| [NSMutableArray array]; |
| |
| if (sectionInfo == self.logoSectionInfo) { |
| // Section empty on purpose. |
| } else if (sectionInfo == self.promoSectionInfo) { |
| if (_notificationPromo->CanShow()) { |
| ContentSuggestionsWhatsNewItem* item = |
| [[ContentSuggestionsWhatsNewItem alloc] initWithType:0]; |
| item.icon = _notificationPromo->GetIcon(); |
| item.text = base::SysUTF8ToNSString(_notificationPromo->promo_text()); |
| [convertedSuggestions addObject:item]; |
| } |
| } else if (sectionInfo == self.returnToRecentTabSectionInfo) { |
| DCHECK(IsStartSurfaceEnabled()); |
| [convertedSuggestions addObject:self.returnToRecentTabItem]; |
| } else if (sectionInfo == self.mostVisitedSectionInfo) { |
| [convertedSuggestions addObjectsFromArray:self.mostVisitedItems]; |
| if (!ShouldHideShortcutsForStartSurface()) { |
| [convertedSuggestions addObjectsFromArray:self.actionButtonItems]; |
| } |
| } |
| |
| return convertedSuggestions; |
| } |
| |
| - (UIView*)headerViewForWidth:(CGFloat)width { |
| return [self.headerProvider |
| headerForWidth:width |
| safeAreaInsets:[self.discoverFeedDelegate safeAreaInsetsForDiscoverFeed]]; |
| } |
| |
| #pragma mark - MostVisitedSitesObserving |
| |
| - (void)onMostVisitedURLsAvailable: |
| (const ntp_tiles::NTPTilesVector&)mostVisited { |
| // This is used by the content widget. |
| ntp_tile_saver::SaveMostVisitedToDisk( |
| mostVisited, self.faviconMediator.mostVisitedAttributesProvider, |
| app_group::ContentWidgetFaviconsFolder()); |
| |
| self.freshMostVisitedItems = [NSMutableArray array]; |
| for (const ntp_tiles::NTPTile& tile : mostVisited) { |
| ContentSuggestionsMostVisitedItem* item = |
| ConvertNTPTile(tile, self.mostVisitedSectionInfo); |
| item.commandHandler = self.commandHandler; |
| item.incognitoAvailable = self.incognitoAvailable; |
| [self.faviconMediator fetchFaviconForMostVisited:item]; |
| [self.freshMostVisitedItems addObject:item]; |
| } |
| |
| if ([self.mostVisitedItems count] > 0) { |
| // If some content is already displayed to the user, do not update without a |
| // user action. |
| return; |
| } |
| |
| [self useFreshMostVisited]; |
| |
| if (mostVisited.size() && !self.recordedPageImpression) { |
| self.recordedPageImpression = YES; |
| [self.faviconMediator setMostVisitedDataForLogging:mostVisited]; |
| ntp_tiles::metrics::RecordPageImpression(mostVisited.size()); |
| } |
| } |
| |
| - (void)onIconMadeAvailable:(const GURL&)siteURL { |
| // This is used by the content widget. |
| ntp_tile_saver::UpdateSingleFavicon( |
| siteURL, self.faviconMediator.mostVisitedAttributesProvider, |
| app_group::ContentWidgetFaviconsFolder()); |
| |
| for (ContentSuggestionsMostVisitedItem* item in self.mostVisitedItems) { |
| if (item.URL == siteURL) { |
| [self.faviconMediator fetchFaviconForMostVisited:item]; |
| return; |
| } |
| } |
| } |
| |
| #pragma mark - ContentSuggestionsMetricsRecorderDelegate |
| |
| - (ContentSuggestionsCategoryWrapper*)categoryWrapperForSectionInfo: |
| (ContentSuggestionsSectionInformation*)sectionInfo { |
| return [[self.sectionInformationByCategory allKeysForObject:sectionInfo] |
| firstObject]; |
| } |
| |
| #pragma mark - Private |
| |
| // Replaces the Most Visited items currently displayed by the most recent ones. |
| - (void)useFreshMostVisited { |
| self.mostVisitedItems = self.freshMostVisitedItems; |
| // All data needs to be reloaded in order to force a re-layout, this is |
| // cheaper since the Feed is not part of this ViewController when Discover |
| // is enabled. |
| [self reloadAllData]; |
| // TODO(crbug.com/1170995): Potentially remove once ContentSuggestions can |
| // be added as part of a header. |
| [self.discoverFeedDelegate contentSuggestionsWasUpdated]; |
| } |
| |
| #pragma mark - Properties |
| |
| - (NSArray<ContentSuggestionsMostVisitedActionItem*>*)actionButtonItems { |
| if (!_actionButtonItems) { |
| self.readingListItem = ReadingListActionItem(); |
| self.readingListItem.count = self.readingListUnreadCount; |
| _actionButtonItems = @[ |
| BookmarkActionItem(), self.readingListItem, RecentTabsActionItem(), |
| HistoryActionItem() |
| ]; |
| for (ContentSuggestionsMostVisitedActionItem* item in _actionButtonItems) { |
| item.accessibilityTraits = UIAccessibilityTraitButton; |
| } |
| } |
| return _actionButtonItems; |
| } |
| |
| - (void)setCommandHandler: |
| (id<ContentSuggestionsCommands, ContentSuggestionsGestureCommands>) |
| commandHandler { |
| if (_commandHandler == commandHandler) |
| return; |
| |
| _commandHandler = commandHandler; |
| |
| for (ContentSuggestionsMostVisitedItem* item in self.freshMostVisitedItems) { |
| item.commandHandler = commandHandler; |
| } |
| } |
| |
| - (BOOL)contentSuggestionsEnabled { |
| return self.articleForYouEnabled->GetValue()->GetBool() && |
| self.contentSuggestionsPolicyEnabled->GetValue()->GetBool(); |
| } |
| |
| #pragma mark - ReadingListModelBridgeObserver |
| |
| - (void)readingListModelLoaded:(const ReadingListModel*)model { |
| [self readingListModelDidApplyChanges:model]; |
| } |
| |
| - (void)readingListModelDidApplyChanges:(const ReadingListModel*)model { |
| self.readingListUnreadCount = model->unread_size(); |
| if (self.readingListItem) { |
| self.readingListItem.count = self.readingListUnreadCount; |
| [self.dataSink itemHasChanged:self.readingListItem]; |
| } |
| } |
| |
| @end |