Index: ios/chrome/browser/ui/history/history_collection_view_controller.mm |
diff --git a/ios/chrome/browser/ui/history/history_collection_view_controller.mm b/ios/chrome/browser/ui/history/history_collection_view_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c2c010360832627a6b280f0861faedca1db39484 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/history/history_collection_view_controller.mm |
@@ -0,0 +1,791 @@ |
+// 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. |
+ |
+#include "ios/chrome/browser/ui/history/history_collection_view_controller.h" |
+ |
+#import <MobileCoreServices/MobileCoreServices.h> |
+ |
+#include <memory> |
+ |
+#import "base/ios/weak_nsobject.h" |
+#include "base/mac/foundation_util.h" |
+#import "base/mac/objc_property_releaser.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "base/strings/utf_string_conversions.h" |
+#include "components/browsing_data/core/history_notice_utils.h" |
+#include "components/strings/grit/components_strings.h" |
+#include "components/url_formatter/url_formatter.h" |
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
+#include "ios/chrome/browser/chrome_url_constants.h" |
+#import "ios/chrome/browser/signin/authentication_service.h" |
+#include "ios/chrome/browser/signin/authentication_service_factory.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrome.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/activity_indicator_cell.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/collection_view_text_item.h" |
+#import "ios/chrome/browser/ui/collection_view/collection_view_model.h" |
+#import "ios/chrome/browser/ui/context_menu/context_menu_coordinator.h" |
+#include "ios/chrome/browser/ui/history/history_entries_status_item.h" |
+#include "ios/chrome/browser/ui/history/history_entry.h" |
+#include "ios/chrome/browser/ui/history/history_entry_inserter.h" |
+#import "ios/chrome/browser/ui/history/history_entry_item.h" |
+#include "ios/chrome/browser/ui/history/history_service_facade.h" |
+#include "ios/chrome/browser/ui/history/history_service_facade_delegate.h" |
+#include "ios/chrome/browser/ui/history/history_util.h" |
+#import "ios/chrome/browser/ui/url_loader.h" |
+#include "ios/chrome/grit/ios_strings.h" |
+#import "ios/third_party/material_components_ios/src/components/Collections/src/MaterialCollections.h" |
+#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h" |
+#import "ios/web/public/referrer.h" |
+#import "ios/web/public/web_state/context_menu_params.h" |
+#import "net/base/mac/url_conversions.h" |
+#include "ui/base/l10n/l10n_util.h" |
+#include "ui/base/l10n/l10n_util_mac.h" |
+ |
+namespace { |
+// Section identifier for the header (sync information) section. |
+const NSInteger kEntriesStatusSectionIdentifier = kSectionIdentifierEnumZero; |
+// Maximum number of entries to retrieve in a single query to history service. |
+const int kMaxFetchCount = 100; |
+// Horizontal inset for item separators. |
+const CGFloat kSeparatorInset = 10; |
+} |
+ |
+@interface HistoryCollectionViewController ()<HistoryEntriesStatusItemDelegate, |
+ HistoryEntryInserterDelegate, |
+ HistoryEntryItemDelegate, |
+ HistoryServiceFacadeDelegate> { |
+ base::mac::ObjCPropertyReleaser |
+ _propertyReleaser_HistoryCollectionViewController; |
+ // Facade for communicating with HistoryService and WebHistoryService. |
+ std::unique_ptr<HistoryServiceFacade> _historyServiceFacade; |
+ // The main browser state. Not owned by HistoryCollectionViewController. |
+ ios::ChromeBrowserState* _browserState; |
+ // Backing ivar for delegate property. |
+ base::WeakNSProtocol<id<HistoryCollectionViewControllerDelegate>> _delegate; |
+ // Backing ivar for URLLoader property. |
+ base::WeakNSProtocol<id<UrlLoader>> _URLLoader; |
+} |
+ |
+// Object to manage insertion of history entries into the collection view model. |
+@property(nonatomic, retain) HistoryEntryInserter* entryInserter; |
+// Delegate for the history collection view. |
+@property(nonatomic, assign, readonly) |
+ id<HistoryCollectionViewControllerDelegate> |
+ delegate; |
+// UrlLoader for navigating to history entries. |
+@property(nonatomic, assign, readonly) id<UrlLoader> URLLoader; |
+// The current query for visible history entries. |
+@property(nonatomic, copy) NSString* currentQuery; |
+// Coordinator for displaying context menus for history entries. |
+@property(nonatomic, assign) ContextMenuCoordinator* contextMenuCoordinator; |
+// Type of displayed history entries. Entries can be synced or local, or there |
+// may be no history entries. |
+@property(nonatomic, assign) HistoryEntriesStatus entriesType; |
+// YES if the history panel should show a notice about additional forms of |
+// browsing history. |
+@property(nonatomic, assign) |
+ BOOL shouldShowNoticeAboutOtherFormsOfBrowsingHistory; |
+// YES if there is an outstanding history query. |
+@property(nonatomic, assign, getter=isLoading) BOOL loading; |
+// YES if there are no more history entries to load. |
+@property(nonatomic, assign, getter=hasFinishedLoading) BOOL finishedLoading; |
+// YES if the collection should be filtered by the next received query result. |
+@property(nonatomic, assign) BOOL filterForNextQueryResult; |
+ |
+// Fetches history prior to |time| for search text |query|. If |query| is nil or |
+// the empty string, all history is fetched. |
+- (void)fetchHistoryForQuery:(NSString*)query |
+ priorToTime:(const base::Time&)time; |
+// Updates header section to provide relevant information about the currently |
+// displayed history entries. |
+- (void)updateEntriesStatusMessage; |
+// Removes selected items from the visible collection, but does not delete them |
+// from browser history. |
+- (void)removeSelectedItemsFromCollection; |
+// Removes all items in the collection that are not included in entries. |
+- (void)filterForHistoryEntries:(NSArray*)entries; |
+// Displays context menu on cell pressed with gestureRecognizer. |
+- (void)displayContextMenuInvokedByGestureRecognizer: |
+ (UILongPressGestureRecognizer*)gestureRecognizer; |
+// Opens URL in the current tab and dismisses the history view. |
+- (void)openURL:(const GURL&)URL; |
+// Opens URL in a new non-incognito tab and dismisses the history view. |
+- (void)openURLInNewTab:(const GURL&)URL; |
+// Opens URL in a new incognito tab and dismisses the history view. |
+- (void)openURLInNewIncognitoTab:(const GURL&)URL; |
+// Copies URL to the clipboard. |
+- (void)copyURL:(const GURL&)URL; |
+@end |
+ |
+@implementation HistoryCollectionViewController |
+ |
+@synthesize searching = _searching; |
+@synthesize entryInserter = _entryInserter; |
+@synthesize currentQuery = _currentQuery; |
+@synthesize contextMenuCoordinator = _contextMenuCoordinator; |
+@synthesize entriesType = _entriesType; |
+@synthesize shouldShowNoticeAboutOtherFormsOfBrowsingHistory = |
+ _shouldShowNoticeAboutOtherFormsOfBrowsingHistory; |
+@synthesize loading = _loading; |
+@synthesize finishedLoading = _finishedLoading; |
+@synthesize filterForNextQueryResult = _filterForNextQueryResult; |
+ |
+- (instancetype)initWithLoader:(id<UrlLoader>)loader |
+ browserState:(ios::ChromeBrowserState*)browserState |
+ delegate:(id<HistoryCollectionViewControllerDelegate>) |
+ delegate { |
+ self = [super initWithStyle:CollectionViewControllerStyleDefault]; |
+ if (self) { |
+ _propertyReleaser_HistoryCollectionViewController.Init( |
+ self, [HistoryCollectionViewController class]); |
+ _historyServiceFacade.reset(new HistoryServiceFacade(browserState, self)); |
+ _browserState = browserState; |
+ _delegate.reset(delegate); |
+ _URLLoader.reset(loader); |
+ [self loadModel]; |
+ // Add initial info section as header. |
+ [self.collectionViewModel |
+ addSectionWithIdentifier:kEntriesStatusSectionIdentifier]; |
+ _entryInserter = |
+ [[HistoryEntryInserter alloc] initWithModel:self.collectionViewModel]; |
+ _entryInserter.delegate = self; |
+ _entriesType = NO_ENTRIES; |
+ [self showHistoryMatchingQuery:nil]; |
+ } |
+ return self; |
+} |
+ |
+- (void)viewDidLoad { |
+ [super viewDidLoad]; |
+ self.styler.cellLayoutType = MDCCollectionViewCellLayoutTypeList; |
+ self.styler.separatorInset = |
+ UIEdgeInsetsMake(0, kSeparatorInset, 0, kSeparatorInset); |
+ self.styler.allowsItemInlay = NO; |
+ |
+ self.clearsSelectionOnViewWillAppear = NO; |
+ self.collectionView.keyboardDismissMode = |
+ UIScrollViewKeyboardDismissModeOnDrag; |
+ |
+ base::scoped_nsobject<UILongPressGestureRecognizer> longPressRecognizer([ |
+ [UILongPressGestureRecognizer alloc] |
+ initWithTarget:self |
+ action:@selector(displayContextMenuInvokedByGestureRecognizer:)]); |
+ [self.collectionView addGestureRecognizer:longPressRecognizer]; |
+} |
+ |
+- (BOOL)isEditing { |
+ return self.editor.isEditing; |
+} |
+ |
+- (void)setEditing:(BOOL)editing { |
+ [self.editor setEditing:editing animated:YES]; |
+} |
+ |
+- (void)setSearching:(BOOL)searching { |
+ _searching = searching; |
+ [self updateEntriesStatusMessage]; |
+} |
+ |
+- (BOOL)hasHistoryEntries { |
+ return self.entriesType != NO_ENTRIES; |
+} |
+ |
+- (BOOL)hasSelectedEntries { |
+ return self.collectionView.indexPathsForSelectedItems.count; |
+} |
+ |
+- (void)showHistoryMatchingQuery:(NSString*)query { |
+ self.finishedLoading = NO; |
+ self.currentQuery = query; |
+ [self fetchHistoryForQuery:query priorToTime:base::Time::Now()]; |
+} |
+ |
+- (void)deleteSelectedItemsFromHistory { |
+ NSArray* deletedIndexPaths = self.collectionView.indexPathsForSelectedItems; |
+ std::vector<HistoryServiceFacade::RemovedEntry> entries; |
+ for (NSIndexPath* indexPath in deletedIndexPaths) { |
+ HistoryEntryItem* object = base::mac::ObjCCastStrict<HistoryEntryItem>( |
+ [self.collectionViewModel itemAtIndexPath:indexPath]); |
+ entries.push_back( |
+ HistoryServiceFacade::RemovedEntry(object.URL, object.timestamp)); |
+ } |
+ _historyServiceFacade->RemoveHistoryEntries(entries); |
+ [self removeSelectedItemsFromCollection]; |
+} |
+ |
+- (id<HistoryCollectionViewControllerDelegate>)delegate { |
+ return _delegate; |
+} |
+ |
+- (id<UrlLoader>)URLLoader { |
+ return _URLLoader; |
+} |
+ |
+#pragma mark - MDCollectionViewController |
+ |
+// TODO(crbug.com/653547): Remove this once the MDC adds an option for |
+// preventing the infobar from showing. |
+- (void)updateFooterInfoBarIfNecessary { |
+ // No-op. This prevents the default infobar from showing. |
+} |
+ |
+#pragma mark - HistoryEntriesStatusItemDelegate |
+ |
+- (void)historyEntriesStatusItem:(HistoryEntriesStatusItem*)item |
+ didRequestOpenURL:(const GURL&)URL { |
+ [self openURL:URL]; |
+} |
+ |
+#pragma mark - HistoryEntryInserterDelegate |
+ |
+- (void)historyEntryInserter:(HistoryEntryInserter*)inserter |
+ didInsertItemAtIndexPath:(NSIndexPath*)indexPath { |
+ [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]]; |
+} |
+ |
+- (void)historyEntryInserter:(HistoryEntryInserter*)inserter |
+ didInsertSectionAtIndex:(NSInteger)sectionIndex { |
+ [self.collectionView |
+ insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]]; |
+} |
+ |
+- (void)historyEntryInserter:(HistoryEntryInserter*)inserter |
+ didRemoveSectionAtIndex:(NSInteger)sectionIndex { |
+ [self.collectionView |
+ deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]]; |
+} |
+ |
+#pragma mark - HistoryEntryItemDelegate |
+ |
+- (void)historyEntryItemDidRequestOpen:(HistoryEntryItem*)item { |
+ [self openURL:item.URL]; |
+} |
+ |
+- (void)historyEntryItemDidRequestDelete:(HistoryEntryItem*)item { |
+ NSInteger sectionIdentifier = |
+ [self.entryInserter sectionIdentifierForTimestamp:item.timestamp]; |
+ if ([self.collectionViewModel |
+ hasSectionForSectionIdentifier:sectionIdentifier] && |
+ [self.collectionViewModel hasItem:item |
+ inSectionWithIdentifier:sectionIdentifier]) { |
+ NSIndexPath* indexPath = |
+ [self.collectionViewModel indexPathForItem:item |
+ inSectionWithIdentifier:sectionIdentifier]; |
+ [self.collectionView |
+ selectItemAtIndexPath:indexPath |
+ animated:NO |
+ scrollPosition:UICollectionViewScrollPositionNone]; |
+ [self deleteSelectedItemsFromHistory]; |
+ } |
+} |
+ |
+- (void)historyEntryItemDidRequestCopy:(HistoryEntryItem*)item { |
+ [self copyURL:item.URL]; |
+} |
+ |
+- (void)historyEntryItemDidRequestOpenInNewTab:(HistoryEntryItem*)item { |
+ [self openURLInNewTab:item.URL]; |
+} |
+ |
+- (void)historyEntryItemDidRequestOpenInNewIncognitoTab: |
+ (HistoryEntryItem*)item { |
+ [self openURLInNewIncognitoTab:item.URL]; |
+} |
+ |
+- (void)historyEntryItemShouldUpdateView:(HistoryEntryItem*)item { |
+ NSInteger sectionIdentifier = |
+ [self.entryInserter sectionIdentifierForTimestamp:item.timestamp]; |
+ // If the item is still in the model, reconfigure it. |
+ if ([self.collectionViewModel |
+ hasSectionForSectionIdentifier:sectionIdentifier] && |
+ [self.collectionViewModel hasItem:item |
+ inSectionWithIdentifier:sectionIdentifier]) { |
+ [self |
+ reconfigureCellsForItems:@[ item ] |
+ inSectionWithIdentifier: |
+ [self.entryInserter sectionIdentifierForTimestamp:item.timestamp]]; |
+ } |
+} |
+ |
+#pragma mark - HistoryServiceFacadeDelegate |
+ |
+- (void)historyServiceFacade:(HistoryServiceFacade*)facade |
+ didReceiveQueryResult:(HistoryServiceFacade::QueryResult)result { |
+ self.loading = NO; |
+ // Remove loading indicator. |
+ CollectionViewItem* headerItem = [self.collectionViewModel |
+ itemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; |
+ if ([headerItem.cellClass isSubclassOfClass:[ActivityIndicatorCell class]]) { |
+ [self.collectionViewModel removeItemWithType:kItemTypeEnumZero |
+ fromSectionWithIdentifier:kSectionIdentifierEnumZero]; |
+ [self.collectionView |
+ deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 |
+ inSection:0] ]]; |
+ } |
+ |
+ // If there are no results and no URLs have been loaded, report that no |
+ // history entries were found. |
+ if (result.entries.empty() && !self.hasHistoryEntries) { |
+ DCHECK(self.entriesType == NO_ENTRIES); |
+ [self updateEntriesStatusMessage]; |
+ [self.delegate historyCollectionViewControllerDidChangeEntries:self]; |
+ return; |
+ } |
+ |
+ self.finishedLoading = result.has_synced_results |
+ ? result.finished && result.sync_finished |
+ : result.finished; |
+ self.entriesType = result.has_synced_results ? SYNCED_ENTRIES : LOCAL_ENTRIES; |
+ std::vector<history::HistoryEntry> entries = result.entries; |
+ |
+ // Header section should be updated outside of batch updates, otherwise |
+ // loading indicator removal will not be observed. |
+ [self updateEntriesStatusMessage]; |
+ |
+ __block base::scoped_nsobject<NSMutableArray> searchResults( |
+ [[NSMutableArray array] retain]); |
+ __block base::scoped_nsobject<NSString> searchQuery( |
+ [base::SysUTF16ToNSString(result.query) copy]); |
+ [self.collectionView performBatchUpdates:^{ |
+ // There should always be at least a header section present. |
+ DCHECK([[self collectionViewModel] numberOfSections]); |
+ for (const history::HistoryEntry& entry : entries) { |
+ HistoryEntryItem* item = |
+ [[[HistoryEntryItem alloc] initWithType:kItemTypeEnumZero |
+ historyEntry:entry |
+ browserState:_browserState |
+ delegate:self] autorelease]; |
+ [self.entryInserter insertHistoryEntryItem:item]; |
+ if ([self isSearching]) { |
+ [searchResults addObject:item]; |
+ } |
+ } |
+ [self.delegate historyCollectionViewControllerDidChangeEntries:self]; |
+ } |
+ completion:^(BOOL) { |
+ if (([self isSearching] && [searchQuery length] > 0 && |
+ [self.currentQuery isEqualToString:searchQuery]) || |
+ self.filterForNextQueryResult) { |
+ // If in search mode, filter out entries that are not |
+ // part of the search result. |
+ [self filterForHistoryEntries:searchResults]; |
+ self.filterForNextQueryResult = NO; |
+ } |
+ }]; |
+} |
+ |
+- (void)historyServiceFacade:(HistoryServiceFacade*)facade |
+ shouldShowNoticeAboutOtherFormsOfBrowsingHistory:(BOOL)shouldShowNotice { |
+ self.shouldShowNoticeAboutOtherFormsOfBrowsingHistory = shouldShowNotice; |
+} |
+ |
+- (void)historyServiceFacadeDidObserveHistoryDeletion: |
+ (HistoryServiceFacade*)facade { |
+ // If history has been deleted, reload history filtering for the current |
+ // results. This only observes local changes to history, i.e. removing |
+ // history via the clear browsing data page. |
+ self.filterForNextQueryResult = YES; |
+ [self showHistoryMatchingQuery:nil]; |
+} |
+ |
+#pragma mark - MDCCollectionViewEditingDelegate |
+ |
+- (BOOL)collectionViewAllowsEditing:(UICollectionView*)collectionView { |
+ return YES; |
+} |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ canEditItemAtIndexPath:(NSIndexPath*)indexPath { |
+ // All items except those in the header section may be edited. |
+ return indexPath.section; |
+} |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ canSelectItemDuringEditingAtIndexPath:(NSIndexPath*)indexPath { |
+ // All items except those in the header section may be edited. |
+ return indexPath.section; |
+} |
+ |
+#pragma mark - MDCCollectionViewStylingDelegate |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath { |
+ // Display the entries status section (always the first section) without any |
+ // background image or shadowing. |
+ return !indexPath.section; |
+} |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ hidesInkViewAtIndexPath:(NSIndexPath*)indexPath { |
+ return [indexPath isEqual:[NSIndexPath indexPathForItem:0 inSection:0]]; |
+} |
+ |
+- (CGFloat)collectionView:(UICollectionView*)collectionView |
+ cellHeightAtIndexPath:(NSIndexPath*)indexPath { |
+ if (indexPath.section) { |
+ return MDCCellDefaultTwoLineHeight; |
+ } else { |
+ DCHECK([indexPath isEqual:[NSIndexPath indexPathForItem:0 inSection:0]]); |
+ // Configure size for loading indicator and entries status cells. |
+ CollectionViewItem* item = |
+ [self.collectionViewModel itemAtIndexPath:indexPath]; |
+ if ([item isKindOfClass:[CollectionViewTextItem class]]) { |
+ return MDCCellDefaultOneLineHeight; |
+ } |
+ CGFloat height = [[item cellClass] |
+ cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds) |
+ forItem:item]; |
+ return height; |
+ } |
+} |
+ |
+- (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView |
+ cellStyleForSection:(NSInteger)section { |
+ return section ? MDCCollectionViewCellStyleCard |
+ : MDCCollectionViewCellStyleDefault; |
+} |
+ |
+#pragma mark - UICollectionViewDelegate |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ shouldSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
+ // The first section is not selectable. |
+ return indexPath.section && [super collectionView:collectionView |
+ shouldSelectItemAtIndexPath:indexPath]; |
+} |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ didSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
+ [super collectionView:collectionView didSelectItemAtIndexPath:indexPath]; |
+ |
+ if (self.isEditing) { |
+ [self.delegate historyCollectionViewControllerDidChangeEntrySelection:self]; |
+ } else { |
+ HistoryEntryItem* item = base::mac::ObjCCastStrict<HistoryEntryItem>( |
+ [self.collectionViewModel itemAtIndexPath:indexPath]); |
+ [self openURL:item.URL]; |
+ } |
+} |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ didDeselectItemAtIndexPath:(NSIndexPath*)indexPath { |
+ [super collectionView:collectionView didDeselectItemAtIndexPath:indexPath]; |
+ [self.delegate historyCollectionViewControllerDidChangeEntrySelection:self]; |
+} |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ didEndDisplayingCell:(UICollectionViewCell*)cell |
+ forItemAtIndexPath:(NSIndexPath*)indexPath { |
+ if ([cell isKindOfClass:[ActivityIndicatorCell class]]) { |
+ [[base::mac::ObjCCast<ActivityIndicatorCell>(cell) activityIndicator] |
+ stopAnimating]; |
+ } |
+} |
+#pragma mark - UIScrollViewDelegate |
+ |
+- (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
+ [super scrollViewDidScroll:scrollView]; |
+ // Adjust header shadow. |
+ [self.delegate historyCollectionViewController:self |
+ didScrollToOffset:scrollView.contentOffset]; |
+ |
+ if (self.hasFinishedLoading) |
+ return; |
+ |
+ CGFloat insetHeight = |
+ scrollView.contentInset.top + scrollView.contentInset.bottom; |
+ CGFloat contentViewHeight = scrollView.bounds.size.height - insetHeight; |
+ CGFloat contentHeight = scrollView.contentSize.height; |
+ CGFloat contentOffset = scrollView.contentOffset.y; |
+ CGFloat buffer = contentViewHeight; |
+ // If the scroll view is approaching the end of loaded history, try to fetch |
+ // more history. Do so when the content offset is greater than the content |
+ // height minus the view height, minus a buffer to start the fetch early. |
+ if (contentOffset > (contentHeight - contentViewHeight) - buffer && |
+ !self.isLoading) { |
+ // If at end, try to grab more history. |
+ NSInteger lastSection = [self.collectionViewModel numberOfSections] - 1; |
+ NSInteger lastItemIndex = |
+ [self.collectionViewModel numberOfItemsInSection:lastSection] - 1; |
+ if (lastSection == 0 || lastItemIndex < 0) { |
+ return; |
+ } |
+ NSIndexPath* indexPath = |
+ [NSIndexPath indexPathForItem:lastItemIndex inSection:lastSection]; |
+ HistoryEntryItem* lastItem = base::mac::ObjCCastStrict<HistoryEntryItem>( |
+ [self.collectionViewModel itemAtIndexPath:indexPath]); |
+ [self fetchHistoryForQuery:_currentQuery priorToTime:lastItem.timestamp]; |
+ } |
+} |
+ |
+#pragma mark - Private methods |
+ |
+- (void)fetchHistoryForQuery:(NSString*)query |
+ priorToTime:(const base::Time&)time { |
+ self.loading = YES; |
+ // Add loading indicator if nothing else is shown. |
+ if (!self.hasHistoryEntries && !self.isSearching) { |
+ [self.collectionView performBatchUpdates:^{ |
+ NSIndexPath* indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; |
+ if ([self.collectionViewModel hasItemAtIndexPath:indexPath]) { |
+ [self.collectionViewModel |
+ removeItemWithType:kItemTypeEnumZero |
+ fromSectionWithIdentifier:kSectionIdentifierEnumZero]; |
+ [self.collectionView deleteItemsAtIndexPaths:@[ indexPath ]]; |
+ } |
+ CollectionViewItem* loadingIndicatorItem = [[[CollectionViewItem alloc] |
+ initWithType:kItemTypeEnumZero] autorelease]; |
+ loadingIndicatorItem.cellClass = [ActivityIndicatorCell class]; |
+ [self.collectionViewModel addItem:loadingIndicatorItem |
+ toSectionWithIdentifier:kEntriesStatusSectionIdentifier]; |
+ [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]]; |
+ } |
+ completion:nil]; |
+ } |
+ |
+ BOOL fetchAllHistory = !query || [query isEqualToString:@""]; |
+ base::string16 queryString = |
+ fetchAllHistory ? base::string16() : base::SysNSStringToUTF16(query); |
+ history::QueryOptions options; |
+ options.end_time = time; |
+ options.duplicate_policy = |
+ fetchAllHistory ? history::QueryOptions::REMOVE_DUPLICATES_PER_DAY |
+ : history::QueryOptions::REMOVE_ALL_DUPLICATES; |
+ options.max_count = kMaxFetchCount; |
+ options.matching_algorithm = |
+ query_parser::MatchingAlgorithm::ALWAYS_PREFIX_SEARCH; |
+ _historyServiceFacade->QueryOtherFormsOfBrowsingHistory(); |
+ _historyServiceFacade->QueryHistory(queryString, options); |
+ // Also determine whether notice regarding other forms of browsing history |
+ // should be shown. |
+ _historyServiceFacade->QueryOtherFormsOfBrowsingHistory(); |
+} |
+ |
+- (void)updateEntriesStatusMessage { |
+ CollectionViewItem* entriesStatusItem = nil; |
+ if (!self.hasHistoryEntries) { |
+ CollectionViewTextItem* noResultsItem = [[[CollectionViewTextItem alloc] |
+ initWithType:kItemTypeEnumZero] autorelease]; |
+ noResultsItem.text = |
+ self.isSearching ? l10n_util::GetNSString(IDS_HISTORY_NO_SEARCH_RESULTS) |
+ : l10n_util::GetNSString(IDS_HISTORY_NO_RESULTS); |
+ entriesStatusItem = noResultsItem; |
+ } else { |
+ HistoryEntriesStatusItem* historyEntriesStatusItem = |
+ [[[HistoryEntriesStatusItem alloc] initWithType:kItemTypeEnumZero] |
+ autorelease]; |
+ historyEntriesStatusItem.delegate = self; |
+ AuthenticationService* authService = |
+ AuthenticationServiceFactory::GetForBrowserState(_browserState); |
+ BOOL signedIn = authService->IsAuthenticated(); |
+ |
+ historyEntriesStatusItem.hidden = |
+ self.isSearching || (!signedIn && self.hasHistoryEntries); |
+ historyEntriesStatusItem.entriesStatus = self.entriesType; |
+ historyEntriesStatusItem.showsOtherBrowsingDataNotice = |
+ _shouldShowNoticeAboutOtherFormsOfBrowsingHistory; |
+ entriesStatusItem = historyEntriesStatusItem; |
+ } |
+ // Replace the item in the first section, which is always present. |
+ NSArray* items = [self.collectionViewModel |
+ itemsInSectionWithIdentifier:kEntriesStatusSectionIdentifier]; |
+ if ([items count]) { |
+ // There should only ever be one item in this section. |
+ DCHECK([items count] == 1); |
+ // Only update if the item has changed. |
+ if ([items[0] isEqual:entriesStatusItem]) { |
+ return; |
+ } |
+ } |
+ [self.collectionView performBatchUpdates:^{ |
+ NSIndexPath* indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; |
+ if ([items count]) { |
+ [self.collectionViewModel |
+ removeItemWithType:kItemTypeEnumZero |
+ fromSectionWithIdentifier:kEntriesStatusSectionIdentifier]; |
+ [self.collectionView deleteItemsAtIndexPaths:@[ indexPath ]]; |
+ } |
+ [self.collectionViewModel addItem:entriesStatusItem |
+ toSectionWithIdentifier:kEntriesStatusSectionIdentifier]; |
+ [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]]; |
+ } |
+ completion:nil]; |
+} |
+ |
+- (void)removeSelectedItemsFromCollection { |
+ NSArray* deletedIndexPaths = self.collectionView.indexPathsForSelectedItems; |
+ [self.collectionView performBatchUpdates:^{ |
+ [self collectionView:self.collectionView |
+ willDeleteItemsAtIndexPaths:deletedIndexPaths]; |
+ [self.collectionView deleteItemsAtIndexPaths:deletedIndexPaths]; |
+ |
+ // Remove any empty sections, except the header section. |
+ for (int section = self.collectionView.numberOfSections - 1; section > 0; |
+ --section) { |
+ if (![self.collectionViewModel numberOfItemsInSection:section]) { |
+ [self.entryInserter removeSection:section]; |
+ } |
+ } |
+ } |
+ completion:^(BOOL) { |
+ // If only the header section remains, there are no history entries. |
+ if ([self.collectionViewModel numberOfSections] == 1) { |
+ self.entriesType = NO_ENTRIES; |
+ } |
+ [self updateEntriesStatusMessage]; |
+ }]; |
+} |
+ |
+- (void)filterForHistoryEntries:(NSArray*)entries { |
+ self.collectionView.allowsMultipleSelection = YES; |
+ for (int section = 1; section < [self.collectionViewModel numberOfSections]; |
+ ++section) { |
+ NSInteger sectionIdentifier = |
+ [self.collectionViewModel sectionIdentifierForSection:section]; |
+ if ([self.collectionViewModel |
+ hasSectionForSectionIdentifier:sectionIdentifier]) { |
+ NSArray* items = [self.collectionViewModel |
+ itemsInSectionWithIdentifier:sectionIdentifier]; |
+ for (id item in items) { |
+ HistoryEntryItem* historyItem = |
+ base::mac::ObjCCastStrict<HistoryEntryItem>(item); |
+ if (![entries containsObject:historyItem]) { |
+ NSIndexPath* indexPath = |
+ [self.collectionViewModel indexPathForItem:historyItem |
+ inSectionWithIdentifier:sectionIdentifier]; |
+ [self.collectionView |
+ selectItemAtIndexPath:indexPath |
+ animated:NO |
+ scrollPosition:UICollectionViewScrollPositionNone]; |
+ } |
+ } |
+ } |
+ } |
+ [self removeSelectedItemsFromCollection]; |
+} |
+ |
+#pragma mark Context Menu |
+ |
+- (void)displayContextMenuInvokedByGestureRecognizer: |
+ (UILongPressGestureRecognizer*)gestureRecognizer { |
+ if (gestureRecognizer.numberOfTouches != 1 || self.editing || |
+ gestureRecognizer.state != UIGestureRecognizerStateBegan) { |
+ return; |
+ } |
+ |
+ CGPoint touchLocation = |
+ [gestureRecognizer locationOfTouch:0 inView:self.collectionView]; |
+ NSIndexPath* touchedItemIndexPath = |
+ [self.collectionView indexPathForItemAtPoint:touchLocation]; |
+ // If there's no index path, or the index path is for the header item, do not |
+ // display a contextual menu. |
+ if (!touchedItemIndexPath || |
+ [touchedItemIndexPath |
+ isEqual:[NSIndexPath indexPathForItem:0 inSection:0]]) |
+ return; |
+ |
+ HistoryEntryItem* entry = base::mac::ObjCCastStrict<HistoryEntryItem>( |
+ [self.collectionViewModel itemAtIndexPath:touchedItemIndexPath]); |
+ |
+ base::WeakNSObject<HistoryCollectionViewController> weakSelf(self); |
+ web::ContextMenuParams params; |
+ params.location = touchLocation; |
+ params.view.reset([self.collectionView retain]); |
+ NSString* menuTitle = |
+ base::SysUTF16ToNSString(url_formatter::FormatUrl(entry.URL)); |
+ params.menu_title.reset([menuTitle copy]); |
+ |
+ // Present sheet/popover using controller that is added to view hierarchy. |
+ UIViewController* topController = [params.view window].rootViewController; |
+ while (topController.presentedViewController) |
+ topController = topController.presentedViewController; |
+ |
+ self.contextMenuCoordinator = |
+ [[ContextMenuCoordinator alloc] initWithBaseViewController:topController |
+ params:params]; |
+ |
+ // TODO(crbug.com/606503): Refactor context menu creation code to be shared |
+ // with BrowserViewController. |
+ // Add "Open in New Tab" option. |
+ NSString* openInNewTabTitle = |
+ l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB); |
+ ProceduralBlock openInNewTabAction = ^{ |
+ [weakSelf openURLInNewTab:entry.URL]; |
+ }; |
+ [self.contextMenuCoordinator addItemWithTitle:openInNewTabTitle |
+ action:openInNewTabAction]; |
+ |
+ // Add "Open in New Incognito Tab" option. |
+ NSString* openInNewIncognitoTabTitle = l10n_util::GetNSStringWithFixup( |
+ IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB); |
+ ProceduralBlock openInNewIncognitoTabAction = ^{ |
+ [weakSelf openURLInNewIncognitoTab:entry.URL]; |
+ }; |
+ [self.contextMenuCoordinator addItemWithTitle:openInNewIncognitoTabTitle |
+ action:openInNewIncognitoTabAction]; |
+ |
+ // Add "Copy URL" option. |
+ NSString* copyURLTitle = |
+ l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_COPY); |
+ ProceduralBlock copyURLAction = ^{ |
+ [weakSelf copyURL:entry.URL]; |
+ }; |
+ [self.contextMenuCoordinator addItemWithTitle:copyURLTitle |
+ action:copyURLAction]; |
+ [self.parentViewController.view endEditing:YES]; |
+ [self.contextMenuCoordinator start]; |
+} |
+ |
+- (void)openURL:(const GURL&)URL { |
+ GURL copiedURL(URL); |
+ [self.delegate historyCollectionViewController:self |
+ shouldCloseWithCompletion:^{ |
+ [self.URLLoader |
+ loadURL:copiedURL |
+ referrer:web::Referrer() |
+ transition:ui::PAGE_TRANSITION_AUTO_BOOKMARK |
+ rendererInitiated:NO]; |
+ }]; |
+} |
+ |
+- (void)openURLInNewTab:(const GURL&)URL { |
+ GURL copiedURL(URL); |
+ [self.delegate historyCollectionViewController:self |
+ shouldCloseWithCompletion:^{ |
+ [self.URLLoader webPageOrderedOpen:copiedURL |
+ referrer:web::Referrer() |
+ windowName:nil |
+ inIncognito:NO |
+ inBackground:NO |
+ appendTo:kLastTab]; |
+ }]; |
+} |
+ |
+- (void)openURLInNewIncognitoTab:(const GURL&)URL { |
+ GURL copiedURL(URL); |
+ [self.delegate historyCollectionViewController:self |
+ shouldCloseWithCompletion:^{ |
+ [self.URLLoader webPageOrderedOpen:copiedURL |
+ referrer:web::Referrer() |
+ windowName:nil |
+ inIncognito:YES |
+ inBackground:NO |
+ appendTo:kLastTab]; |
+ }]; |
+} |
+ |
+- (void)copyURL:(const GURL&)URL { |
+ DCHECK(URL.is_valid()); |
+ NSData* plainText = [base::SysUTF8ToNSString(URL.spec()) |
+ dataUsingEncoding:NSUTF8StringEncoding]; |
+ NSDictionary* copiedItem = @{ |
+ (NSString*)kUTTypeURL : net::NSURLWithGURL(URL), |
+ (NSString*)kUTTypeUTF8PlainText : plainText, |
+ }; |
+ [[UIPasteboard generalPasteboard] setItems:@[ copiedItem ]]; |
+} |
+ |
+@end |