| // Copyright 2016 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_collection_updater.h" |
| |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/time/time.h" |
| #include "components/strings/grit/components_strings.h" |
| #import "ios/chrome/browser/ui/collection_view/cells/collection_view_item+collection_view_controller.h" |
| #import "ios/chrome/browser/ui/collection_view/cells/collection_view_text_item.h" |
| #import "ios/chrome/browser/ui/collection_view/collection_view_controller.h" |
| #import "ios/chrome/browser/ui/collection_view/collection_view_model.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_footer_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_header_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_text_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/suggested_content.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_utils.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_data_sink.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_data_source.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/identifier/content_suggestion_identifier.h" |
| #import "ios/chrome/browser/ui/content_suggestions/identifier/content_suggestions_section_information.h" |
| #import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h" |
| #import "ios/third_party/material_components_ios/src/components/Snackbar/src/MaterialSnackbar.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| using CSCollectionViewItem = CollectionViewItem<SuggestedContent>; |
| using CSCollectionViewModel = CollectionViewModel<CSCollectionViewItem*>; |
| |
| // Enum defining the ItemType of this ContentSuggestionsCollectionUpdater. |
| typedef NS_ENUM(NSInteger, ItemType) { |
| ItemTypeArticle = kItemTypeEnumZero, |
| ItemTypeFooter, |
| ItemTypeHeader, |
| ItemTypeEmpty, |
| ItemTypeReadingList, |
| ItemTypeMostVisited, |
| ItemTypePromo, |
| ItemTypeLearnMore, |
| ItemTypeUnknown, |
| }; |
| |
| // Enum defining the SectionIdentifier of this |
| // ContentSuggestionsCollectionUpdater. |
| typedef NS_ENUM(NSInteger, SectionIdentifier) { |
| SectionIdentifierArticles = kSectionIdentifierEnumZero, |
| SectionIdentifierReadingList, |
| SectionIdentifierMostVisited, |
| SectionIdentifierLogo, |
| SectionIdentifierPromo, |
| SectionIdentifierLearnMore, |
| SectionIdentifierDefault, |
| }; |
| |
| // Returns the ContentSuggestionType associated with an ItemType |type|. |
| ContentSuggestionType ContentSuggestionTypeForItemType(NSInteger type) { |
| if (type == ItemTypeArticle) |
| return ContentSuggestionTypeArticle; |
| if (type == ItemTypeEmpty) |
| return ContentSuggestionTypeEmpty; |
| if (type == ItemTypeReadingList) |
| return ContentSuggestionTypeReadingList; |
| if (type == ItemTypeMostVisited) |
| return ContentSuggestionTypeMostVisited; |
| if (type == ItemTypePromo) |
| return ContentSuggestionTypePromo; |
| if (type == ItemTypeLearnMore) |
| return ContentSuggestionTypeLearnMore; |
| // Add new type here |
| |
| // Default type. |
| return ContentSuggestionTypeEmpty; |
| } |
| |
| // Returns the item type corresponding to the section |info|. |
| ItemType ItemTypeForInfo(ContentSuggestionsSectionInformation* info) { |
| switch (info.sectionID) { |
| case ContentSuggestionsSectionArticles: |
| return ItemTypeArticle; |
| case ContentSuggestionsSectionReadingList: |
| return ItemTypeReadingList; |
| case ContentSuggestionsSectionMostVisited: |
| return ItemTypeMostVisited; |
| case ContentSuggestionsSectionPromo: |
| return ItemTypePromo; |
| case ContentSuggestionsSectionLearnMore: |
| return ItemTypeLearnMore; |
| |
| case ContentSuggestionsSectionLogo: |
| case ContentSuggestionsSectionUnknown: |
| return ItemTypeUnknown; |
| } |
| } |
| |
| // Returns the section identifier corresponding to the section |info|. |
| SectionIdentifier SectionIdentifierForInfo( |
| ContentSuggestionsSectionInformation* info) { |
| switch (info.sectionID) { |
| case ContentSuggestionsSectionArticles: |
| return SectionIdentifierArticles; |
| case ContentSuggestionsSectionReadingList: |
| return SectionIdentifierReadingList; |
| case ContentSuggestionsSectionMostVisited: |
| return SectionIdentifierMostVisited; |
| case ContentSuggestionsSectionLogo: |
| return SectionIdentifierLogo; |
| case ContentSuggestionsSectionPromo: |
| return SectionIdentifierPromo; |
| case ContentSuggestionsSectionLearnMore: |
| return SectionIdentifierLearnMore; |
| |
| case ContentSuggestionsSectionUnknown: |
| return SectionIdentifierDefault; |
| } |
| } |
| |
| // Returns whether this |sectionIdentifier| comes from ContentSuggestions. |
| BOOL IsFromContentSuggestions(NSInteger sectionIdentifier) { |
| return sectionIdentifier == SectionIdentifierArticles || |
| sectionIdentifier == SectionIdentifierReadingList; |
| } |
| |
| const CGFloat kNumberOfMostVisitedLines = 2; |
| |
| NSString* const kContentSuggestionsCollectionUpdaterSnackbarCategory = |
| @"ContentSuggestionsCollectionUpdaterSnackbarCategory"; |
| |
| } // namespace |
| |
| @interface ContentSuggestionsCollectionUpdater ()<ContentSuggestionsDataSink> |
| |
| @property(nonatomic, weak) id<ContentSuggestionsDataSource> dataSource; |
| @property(nonatomic, strong) |
| NSMutableDictionary<NSNumber*, ContentSuggestionsSectionInformation*>* |
| sectionInfoBySectionIdentifier; |
| // Width of the collection. Upon size change, it reflects the new size. |
| @property(nonatomic, assign) CGFloat collectionWidth; |
| // Whether an item of type ItemTypePromo has already been added to the model. |
| @property(nonatomic, assign) BOOL promoAdded; |
| // All SectionIdentifier from ContentSuggestions. |
| @property(nonatomic, strong) |
| NSMutableSet<NSNumber*>* sectionIdentifiersFromContentSuggestions; |
| |
| @end |
| |
| @implementation ContentSuggestionsCollectionUpdater |
| |
| @synthesize collectionViewController = _collectionViewController; |
| @synthesize dataSource = _dataSource; |
| @synthesize sectionInfoBySectionIdentifier = _sectionInfoBySectionIdentifier; |
| @synthesize collectionWidth = _collectionWidth; |
| @synthesize promoAdded = _promoAdded; |
| @synthesize sectionIdentifiersFromContentSuggestions = |
| _sectionIdentifiersFromContentSuggestions; |
| |
| - (instancetype)initWithDataSource: |
| (id<ContentSuggestionsDataSource>)dataSource { |
| self = [super init]; |
| if (self) { |
| _promoAdded = NO; |
| _dataSource = dataSource; |
| _dataSource.dataSink = self; |
| } |
| return self; |
| } |
| |
| #pragma mark - Properties |
| |
| - (void)setCollectionViewController: |
| (ContentSuggestionsViewController*)collectionViewController { |
| _collectionViewController = collectionViewController; |
| self.collectionWidth = |
| collectionViewController.collectionView.bounds.size.width; |
| |
| [self reloadAllData]; |
| } |
| |
| #pragma mark - ContentSuggestionsDataSink |
| |
| - (void)dataAvailableForSection: |
| (ContentSuggestionsSectionInformation*)sectionInfo { |
| SectionIdentifier sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| if ([model hasSectionForSectionIdentifier:sectionIdentifier]) { |
| NSArray<CSCollectionViewItem*>* items = |
| [model itemsInSectionWithIdentifier:sectionIdentifier]; |
| if (items.count > 0 && items[0].type != ItemTypeEmpty) { |
| // Do not dismiss the presented items. |
| return; |
| } |
| } |
| |
| [self.collectionViewController |
| addSuggestions:[self.dataSource itemsForSectionInfo:sectionInfo] |
| toSectionInfo:sectionInfo]; |
| } |
| |
| - (void)clearSuggestion:(ContentSuggestionIdentifier*)suggestionIdentifier { |
| SectionIdentifier sectionIdentifier = |
| SectionIdentifierForInfo(suggestionIdentifier.sectionInfo); |
| if (![self.collectionViewController.collectionViewModel |
| hasSectionForSectionIdentifier:sectionIdentifier]) { |
| return; |
| } |
| |
| NSArray<CSCollectionViewItem*>* itemsInSection = |
| [self.collectionViewController.collectionViewModel |
| itemsInSectionWithIdentifier:sectionIdentifier]; |
| |
| CSCollectionViewItem* correspondingItem = nil; |
| for (CSCollectionViewItem* item in itemsInSection) { |
| if (item.suggestionIdentifier == suggestionIdentifier) { |
| correspondingItem = item; |
| break; |
| } |
| } |
| |
| if (!correspondingItem) |
| return; |
| |
| NSIndexPath* indexPath = [self.collectionViewController.collectionViewModel |
| indexPathForItem:correspondingItem]; |
| [self.collectionViewController dismissEntryAtIndexPath:indexPath]; |
| } |
| |
| - (void)reloadAllData { |
| [self resetModels]; |
| |
| // The data is reset, add the new data directly in the model then reload the |
| // collection. |
| NSArray<ContentSuggestionsSectionInformation*>* sectionInfos = |
| [self.dataSource sectionsInfo]; |
| [self addSectionsForSectionInfoToModel:sectionInfos]; |
| for (ContentSuggestionsSectionInformation* sectionInfo in sectionInfos) { |
| [self |
| addSuggestionsToModel:[self.dataSource itemsForSectionInfo:sectionInfo] |
| withSectionInfo:sectionInfo]; |
| } |
| [self.collectionViewController.collectionView reloadData]; |
| } |
| |
| - (void)clearSection:(ContentSuggestionsSectionInformation*)sectionInfo { |
| SectionIdentifier sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| |
| if (![model hasSectionForSectionIdentifier:sectionIdentifier]) |
| return; |
| |
| NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier]; |
| |
| [self.collectionViewController dismissSection:section]; |
| } |
| |
| - (void)reloadSection:(ContentSuggestionsSectionInformation*)sectionInfo { |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| SectionIdentifier sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| if (![model hasSectionForSectionIdentifier:sectionIdentifier]) { |
| [self.collectionViewController |
| addSuggestions:[self.dataSource itemsForSectionInfo:sectionInfo] |
| toSectionInfo:sectionInfo]; |
| return; |
| } |
| |
| NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier]; |
| |
| NSMutableArray* oldItems = [NSMutableArray array]; |
| NSInteger numberOfItems = [model numberOfItemsInSection:section]; |
| for (NSInteger i = 0; i < numberOfItems; i++) { |
| [oldItems addObject:[NSIndexPath indexPathForItem:i inSection:section]]; |
| } |
| [self.collectionViewController |
| collectionView:self.collectionViewController.collectionView |
| willDeleteItemsAtIndexPaths:oldItems]; |
| |
| [self addSuggestionsToModel:[self.dataSource itemsForSectionInfo:sectionInfo] |
| withSectionInfo:sectionInfo]; |
| |
| [self.collectionViewController.collectionView |
| reloadSections:[NSIndexSet indexSetWithIndex:section]]; |
| } |
| |
| - (void)itemHasChanged:(CollectionViewItem<SuggestedContent>*)item { |
| if (![self.collectionViewController.collectionViewModel hasItem:item]) { |
| return; |
| } |
| [self.collectionViewController reconfigureCellsForItems:@[ item ]]; |
| } |
| |
| #pragma mark - Public methods |
| |
| - (BOOL)shouldUseCustomStyleForSection:(NSInteger)section { |
| NSNumber* identifier = @([self.collectionViewController.collectionViewModel |
| sectionIdentifierForSection:section]); |
| ContentSuggestionsSectionInformation* sectionInformation = |
| self.sectionInfoBySectionIdentifier[identifier]; |
| return sectionInformation.layout == ContentSuggestionsSectionLayoutCustom; |
| } |
| |
| - (ContentSuggestionType)contentSuggestionTypeForItem: |
| (CollectionViewItem*)item { |
| return ContentSuggestionTypeForItemType(item.type); |
| } |
| |
| - (NSIndexPath*)removeEmptySuggestionsForSectionInfo: |
| (ContentSuggestionsSectionInformation*)sectionInfo { |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| NSInteger sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| NSArray<CSCollectionViewItem*>* existingItems = |
| [model itemsInSectionWithIdentifier:sectionIdentifier]; |
| if (existingItems.count == 1 && existingItems[0].type == ItemTypeEmpty) { |
| [model removeItemWithType:ItemTypeEmpty |
| fromSectionWithIdentifier:sectionIdentifier]; |
| NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier]; |
| return [NSIndexPath indexPathForItem:0 inSection:section]; |
| } |
| return nil; |
| } |
| |
| - (NSArray<NSIndexPath*>*) |
| addSuggestionsToModel:(NSArray<CSCollectionViewItem*>*)suggestions |
| withSectionInfo:(ContentSuggestionsSectionInformation*)sectionInfo { |
| NSMutableArray<NSIndexPath*>* indexPaths = [NSMutableArray array]; |
| |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| NSInteger sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| if (suggestions.count == 0) { |
| if ([model hasSectionForSectionIdentifier:sectionIdentifier] && |
| [model numberOfItemsInSection:[model sectionForSectionIdentifier: |
| sectionIdentifier]] == 0) { |
| NSIndexPath* emptyItemIndexPath = |
| [self addEmptyItemForSection: |
| [model sectionForSectionIdentifier:sectionIdentifier]]; |
| if (emptyItemIndexPath) { |
| [indexPaths addObject:emptyItemIndexPath]; |
| } |
| } |
| return indexPaths; |
| } |
| |
| [suggestions enumerateObjectsUsingBlock:^(CSCollectionViewItem* item, |
| NSUInteger index, BOOL* stop) { |
| NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier]; |
| if ([self isMostVisitedSection:section] && |
| [model numberOfItemsInSection:section] >= |
| [self mostVisitedPlaceCount]) { |
| return; |
| } |
| ItemType type = ItemTypeForInfo(sectionInfo); |
| if (type == ItemTypePromo && !self.promoAdded) { |
| self.promoAdded = YES; |
| [self.collectionViewController.audience promoShown]; |
| } |
| item.type = type; |
| NSIndexPath* addedIndexPath = |
| [self addItem:item toSectionWithIdentifier:sectionIdentifier]; |
| |
| [indexPaths addObject:addedIndexPath]; |
| }]; |
| |
| return indexPaths; |
| } |
| |
| - (NSIndexSet*)addSectionsForSectionInfoToModel: |
| (NSArray<ContentSuggestionsSectionInformation*>*)sectionsInfo { |
| NSMutableIndexSet* addedSectionIdentifiers = [NSMutableIndexSet indexSet]; |
| NSArray<ContentSuggestionsSectionInformation*>* orderedSectionsInfo = |
| [self.dataSource sectionsInfo]; |
| |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| for (ContentSuggestionsSectionInformation* sectionInfo in sectionsInfo) { |
| NSInteger sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| if ([model hasSectionForSectionIdentifier:sectionIdentifier] || |
| (!sectionInfo.showIfEmpty && |
| [self.dataSource itemsForSectionInfo:sectionInfo].count == 0)) { |
| continue; |
| } |
| |
| NSUInteger sectionIndex = 0; |
| for (ContentSuggestionsSectionInformation* orderedSectionInfo in |
| orderedSectionsInfo) { |
| NSInteger orderedSectionIdentifier = |
| SectionIdentifierForInfo(orderedSectionInfo); |
| if (orderedSectionIdentifier == sectionIdentifier) { |
| break; |
| } |
| if ([model hasSectionForSectionIdentifier:orderedSectionIdentifier]) { |
| sectionIndex++; |
| } |
| } |
| [model insertSectionWithIdentifier:sectionIdentifier atIndex:sectionIndex]; |
| |
| self.sectionInfoBySectionIdentifier[@(sectionIdentifier)] = sectionInfo; |
| [addedSectionIdentifiers addIndex:sectionIdentifier]; |
| |
| if (sectionIdentifier == SectionIdentifierLogo) { |
| [self addLogoHeaderIfNeeded]; |
| } else { |
| [self addHeaderIfNeeded:sectionInfo]; |
| } |
| [self addFooterIfNeeded:sectionInfo]; |
| } |
| |
| NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet]; |
| [addedSectionIdentifiers enumerateIndexesUsingBlock:^( |
| NSUInteger sectionIdentifier, |
| BOOL* _Nonnull stop) { |
| [indexSet addIndex:[model sectionForSectionIdentifier:sectionIdentifier]]; |
| }]; |
| return indexSet; |
| } |
| |
| - (NSIndexPath*)addEmptyItemForSection:(NSInteger)section { |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| NSInteger sectionIdentifier = [model sectionIdentifierForSection:section]; |
| ContentSuggestionsSectionInformation* sectionInfo = |
| self.sectionInfoBySectionIdentifier[@(sectionIdentifier)]; |
| |
| CSCollectionViewItem* item = [self emptyItemForSectionInfo:sectionInfo]; |
| if (!item) { |
| return nil; |
| } |
| return [self addItem:item toSectionWithIdentifier:sectionIdentifier]; |
| } |
| |
| - (BOOL)isMostVisitedSection:(NSInteger)section { |
| return |
| [self.collectionViewController.collectionViewModel |
| sectionIdentifierForSection:section] == SectionIdentifierMostVisited; |
| } |
| |
| - (BOOL)isHeaderSection:(NSInteger)section { |
| return [self.collectionViewController.collectionViewModel |
| sectionIdentifierForSection:section] == SectionIdentifierLogo; |
| } |
| |
| - (BOOL)isPromoSection:(NSInteger)section { |
| return [self.collectionViewController.collectionViewModel |
| sectionIdentifierForSection:section] == SectionIdentifierPromo; |
| } |
| |
| - (void)updateMostVisitedForSize:(CGSize)size { |
| self.collectionWidth = size.width; |
| |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| if (![model hasSectionForSectionIdentifier:SectionIdentifierMostVisited]) |
| return; |
| |
| NSInteger mostVisitedSection = |
| [model sectionForSectionIdentifier:SectionIdentifierMostVisited]; |
| ContentSuggestionsSectionInformation* mostVisitedSectionInfo = |
| self.sectionInfoBySectionIdentifier[@(SectionIdentifierMostVisited)]; |
| NSArray<CSCollectionViewItem*>* mostVisited = |
| [self.dataSource itemsForSectionInfo:mostVisitedSectionInfo]; |
| NSInteger newCount = MIN([self mostVisitedPlaceCount], |
| static_cast<NSInteger>(mostVisited.count)); |
| NSInteger currentCount = [model numberOfItemsInSection:mostVisitedSection]; |
| |
| if (currentCount == newCount) |
| return; |
| |
| if (currentCount > newCount) { |
| for (NSInteger i = newCount; i < currentCount; i++) { |
| [self.collectionViewController.collectionViewModel |
| removeItemWithType:ItemTypeMostVisited |
| fromSectionWithIdentifier:SectionIdentifierMostVisited |
| atIndex:newCount]; |
| } |
| } else { |
| for (NSInteger i = currentCount; i < newCount; i++) { |
| CSCollectionViewItem* item = mostVisited[i]; |
| item.type = ItemTypeMostVisited; |
| [self.collectionViewController.collectionViewModel |
| addItem:item |
| toSectionWithIdentifier:SectionIdentifierMostVisited]; |
| } |
| } |
| } |
| |
| - (void)dismissItem:(CSCollectionViewItem*)item { |
| [self.dataSource dismissSuggestion:item.suggestionIdentifier]; |
| } |
| |
| #pragma mark - Private methods |
| |
| // Adds a footer to the section identified by |sectionInfo| if there is none |
| // present and the section info contains a title for it. |
| - (void)addFooterIfNeeded:(ContentSuggestionsSectionInformation*)sectionInfo { |
| NSInteger sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| __weak ContentSuggestionsCollectionUpdater* weakSelf = self; |
| if (sectionInfo.footerTitle && |
| ![self.collectionViewController.collectionViewModel |
| footerForSectionWithIdentifier:sectionIdentifier]) { |
| ContentSuggestionsFooterItem* footer = [[ContentSuggestionsFooterItem alloc] |
| initWithType:ItemTypeFooter |
| title:sectionInfo.footerTitle |
| callback:^(ContentSuggestionsFooterItem* item, |
| ContentSuggestionsFooterCell* cell) { |
| [weakSelf runAdditionalActionForSection:sectionInfo |
| withItem:item |
| cell:cell]; |
| }]; |
| |
| [self.collectionViewController.collectionViewModel |
| setFooter:footer |
| forSectionWithIdentifier:sectionIdentifier]; |
| } |
| } |
| |
| // Adds the header corresponding to |sectionInfo| to the section if there is |
| // none present and the section info contains a title. |
| // In addition to that, if the section is for a content suggestion, only show a |
| // title if there are more than 1 occurence of a content suggestion section. |
| - (void)addHeaderIfNeeded:(ContentSuggestionsSectionInformation*)sectionInfo { |
| NSInteger sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| |
| if (![model headerForSectionWithIdentifier:sectionIdentifier] && |
| sectionInfo.title) { |
| BOOL addHeader = YES; |
| |
| if (IsFromContentSuggestions(sectionIdentifier)) { |
| addHeader = NO; |
| |
| if ([self.sectionIdentifiersFromContentSuggestions |
| containsObject:@(sectionIdentifier)]) { |
| return; |
| } |
| if ([self.sectionIdentifiersFromContentSuggestions count] == 1) { |
| NSNumber* existingSectionIdentifier = |
| [self.sectionIdentifiersFromContentSuggestions anyObject]; |
| ContentSuggestionsSectionInformation* existingSectionInfo = |
| self.sectionInfoBySectionIdentifier[existingSectionIdentifier]; |
| [model setHeader:[self headerForSectionInfo:existingSectionInfo] |
| forSectionWithIdentifier:[existingSectionIdentifier integerValue]]; |
| addHeader = YES; |
| } else if ([self.sectionIdentifiersFromContentSuggestions count] > 1) { |
| addHeader = YES; |
| } |
| |
| [self.sectionIdentifiersFromContentSuggestions |
| addObject:@(sectionIdentifier)]; |
| } |
| |
| if (addHeader) { |
| [model setHeader:[self headerForSectionInfo:sectionInfo] |
| forSectionWithIdentifier:sectionIdentifier]; |
| } |
| } |
| } |
| |
| // Returns the header for this |sectionInfo|. |
| - (CollectionViewItem*)headerForSectionInfo: |
| (ContentSuggestionsSectionInformation*)sectionInfo { |
| CollectionViewTextItem* header = |
| [[CollectionViewTextItem alloc] initWithType:ItemTypeHeader]; |
| header.text = sectionInfo.title; |
| header.textColor = [[MDCPalette greyPalette] tint500]; |
| return header; |
| } |
| |
| // Adds the header for the first section, containing the logo and the omnibox, |
| // if there is no header for the section. |
| - (void)addLogoHeaderIfNeeded { |
| if (![self.collectionViewController.collectionViewModel |
| headerForSectionWithIdentifier:SectionIdentifierLogo]) { |
| ContentSuggestionsHeaderItem* header = |
| [[ContentSuggestionsHeaderItem alloc] initWithType:ItemTypeHeader]; |
| header.view = [self.dataSource headerViewForWidth:self.collectionWidth]; |
| [self.collectionViewController.collectionViewModel |
| setHeader:header |
| forSectionWithIdentifier:SectionIdentifierLogo]; |
| } |
| } |
| |
| // Resets the models, removing the current CollectionViewItem and the |
| // SectionInfo. |
| - (void)resetModels { |
| [self.collectionViewController loadModel]; |
| self.sectionInfoBySectionIdentifier = [[NSMutableDictionary alloc] init]; |
| self.sectionIdentifiersFromContentSuggestions = [[NSMutableSet alloc] init]; |
| } |
| |
| // Runs the additional action for the section identified by |sectionInfo|. |
| - (void)runAdditionalActionForSection: |
| (ContentSuggestionsSectionInformation*)sectionInfo |
| withItem:(ContentSuggestionsFooterItem*)item |
| cell:(ContentSuggestionsFooterCell*)cell { |
| SectionIdentifier sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| |
| item.loading = YES; |
| [item configureCell:cell]; |
| |
| NSMutableArray<ContentSuggestionIdentifier*>* knownSuggestionIdentifiers = |
| [NSMutableArray array]; |
| |
| NSArray<CSCollectionViewItem*>* knownSuggestions = |
| [self.collectionViewController.collectionViewModel |
| itemsInSectionWithIdentifier:sectionIdentifier]; |
| for (CSCollectionViewItem* suggestion in knownSuggestions) { |
| if (suggestion.type != ItemTypeEmpty) { |
| [knownSuggestionIdentifiers addObject:suggestion.suggestionIdentifier]; |
| } |
| } |
| |
| __weak ContentSuggestionsCollectionUpdater* weakSelf = self; |
| __weak ContentSuggestionsFooterItem* weakItem = item; |
| __weak ContentSuggestionsFooterCell* weakCell = cell; |
| [self.dataSource |
| fetchMoreSuggestionsKnowing:knownSuggestionIdentifiers |
| fromSectionInfo:sectionInfo |
| callback:^(NSArray<CSCollectionViewItem*>* suggestions, |
| content_suggestions::StatusCode status) { |
| [weakSelf moreSuggestionsFetched:suggestions |
| inSectionInfo:sectionInfo |
| withItem:weakItem |
| cell:weakCell |
| status:status]; |
| }]; |
| } |
| |
| // Adds the |suggestions| to the collection view. All the suggestions must have |
| // the same |sectionInfo|. The fetch has been started by tapping the |cell| and |
| // its associated |item|. |status| gives the status of the fetch result. |
| - (void)moreSuggestionsFetched:(NSArray<CSCollectionViewItem*>*)suggestions |
| inSectionInfo: |
| (ContentSuggestionsSectionInformation*)sectionInfo |
| withItem:(ContentSuggestionsFooterItem*)item |
| cell:(ContentSuggestionsFooterCell*)cell |
| status:(content_suggestions::StatusCode)status { |
| SectionIdentifier sectionIdentifier = SectionIdentifierForInfo(sectionInfo); |
| item.loading = NO; |
| if (cell && item && cell.delegate == item) { |
| // The cell has not been reconfigured by another item. It should be safe to |
| // update it. |
| [item configureCell:cell]; |
| } |
| |
| if (suggestions) { |
| [self.collectionViewController addSuggestions:suggestions |
| toSectionInfo:sectionInfo]; |
| |
| } else if (status == content_suggestions::StatusCodeSuccess || |
| status == content_suggestions::StatusCodePermanentError) { |
| // No more suggestions. |
| UICollectionView* collectionView = |
| self.collectionViewController.collectionView; |
| |
| [self.collectionViewController.collectionViewModel |
| setFooter:nil |
| forSectionWithIdentifier:sectionIdentifier]; |
| |
| [collectionView performBatchUpdates:^{ |
| [collectionView.collectionViewLayout invalidateLayout]; |
| } |
| completion:nil]; |
| |
| } else if (status == content_suggestions::StatusCodeError) { |
| NSString* text = |
| l10n_util::GetNSString(IDS_NTP_ARTICLE_SUGGESTIONS_NOT_AVAILABLE); |
| MDCSnackbarMessage* message = [MDCSnackbarMessage messageWithText:text]; |
| message.accessibilityLabel = text; |
| message.category = kContentSuggestionsCollectionUpdaterSnackbarCategory; |
| [MDCSnackbarManager showMessage:message]; |
| } |
| } |
| |
| // Returns a item to be displayed when the section identified by |sectionInfo| |
| // is empty. |
| // Returns nil if there is no empty item for this section info. |
| - (CSCollectionViewItem*)emptyItemForSectionInfo: |
| (ContentSuggestionsSectionInformation*)sectionInfo { |
| if (!sectionInfo.emptyText) |
| return nil; |
| ContentSuggestionsTextItem* item = |
| [[ContentSuggestionsTextItem alloc] initWithType:ItemTypeEmpty]; |
| item.text = l10n_util::GetNSString(IDS_NTP_TITLE_NO_SUGGESTIONS); |
| item.detailText = sectionInfo.emptyText; |
| |
| return item; |
| } |
| |
| // Adds |item| to |sectionIdentifier| section of the model of the |
| // CollectionView. Returns the IndexPath of the newly added item. |
| - (NSIndexPath*)addItem:(CSCollectionViewItem*)item |
| toSectionWithIdentifier:(NSInteger)sectionIdentifier { |
| CSCollectionViewModel* model = |
| self.collectionViewController.collectionViewModel; |
| NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier]; |
| NSInteger itemNumber = [model numberOfItemsInSection:section]; |
| [model addItem:item toSectionWithIdentifier:sectionIdentifier]; |
| |
| return [NSIndexPath indexPathForItem:itemNumber inSection:section]; |
| } |
| |
| // Returns the maximum number of Most Visited tiles to be displayed in the |
| // collection. |
| - (NSInteger)mostVisitedPlaceCount { |
| return content_suggestions::numberOfTilesForWidth(self.collectionWidth) * |
| kNumberOfMostVisitedLines; |
| } |
| |
| @end |