chromium/ios/chrome/browser/ui/price_notifications/price_notifications_table_view_controller.mm

// Copyright 2022 The Chromium Authors
// 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/price_notifications/price_notifications_table_view_controller.h"

#import <MaterialComponents/MaterialSnackbar.h>

#import "base/apple/foundation_util.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/ui/list_model/list_item+Controller.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_link_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_link_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_model.h"
#import "ios/chrome/browser/shared/ui/util/snackbar_util.h"
#import "ios/chrome/browser/ui/price_notifications/cells/price_notifications_image_container_view.h"
#import "ios/chrome/browser/ui/price_notifications/cells/price_notifications_table_view_item.h"
#import "ios/chrome/browser/ui/price_notifications/price_notifications_constants.h"
#import "ios/chrome/browser/ui/price_notifications/price_notifications_consumer.h"
#import "ios/chrome/browser/ui/price_notifications/price_notifications_mutator.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/text_view_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

namespace {

const CGFloat kVerticalTableViewSectionSpacing = 30;

// Types of ListItems used by the price notifications UI.
typedef NS_ENUM(NSInteger, ItemType) {
  ItemTypeHeader = kItemTypeEnumZero,
  ItemTypeSectionHeader,
  ItemTypeListItem,
  ItemTypeTableViewHeader,
};

// Identifiers for sections in the price notifications.
typedef NS_ENUM(NSInteger, SectionIdentifier) {
  SectionIdentifierTrackableItemsOnCurrentSite = kSectionIdentifierEnumZero,
  SectionIdentifierTrackedItems,
  SectionIdentifierTableViewHeader
};

const char kBookmarksSettingsURL[] = "settings://open_bookmarks";

}  // namespace

@interface PriceNotificationsTableViewController () <
    TableViewTextHeaderFooterItemDelegate>
// The boolean indicates whether there exists an item on the current site is
// already tracked or the item is already being price tracked.
@property(nonatomic, assign) BOOL itemOnCurrentSiteIsTracked;

@end

@implementation PriceNotificationsTableViewController {
  // The boolean indicates whether the user has Price tracking item
  // subscriptions displayed on the UI.
  BOOL _hasTrackedItems;
  // A boolean value that indicates that data is coming from the shoppingService
  // and that the loading state should be hidden.
  BOOL _shouldHideLoadingState;
  // A boolean value that indicates that the loading state is currently being
  // displayed.
  BOOL _displayedLoadingState;
  // Indicates whether the TableViewController's tableViewModel has been
  // initialized.
  BOOL _hasModelBeenInitialized;
  // Indicates whether the user has tracked an item over the TableView's life
  // time.
  BOOL _hasExecutedItemTracking;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  [self initializeTableViewModelIfNeeded];
  [self addLoadingStateItems];

  self.tableView.accessibilityIdentifier =
      kPriceNotificationsTableViewIdentifier;
  self.tableView.estimatedRowHeight = 100;
  NSUInteger imageWidthWithPadding =
      PriceNotificationsImageView::kPriceNotificationsImageLength +
      kTableViewHorizontalSpacing * 2;
  self.tableView.separatorInset =
      UIEdgeInsetsMake(0, imageWidthWithPadding, 0, 0);

  self.title =
      l10n_util::GetNSString(IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_TITLE);
}

#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  if (section ==
      [self.tableViewModel
          sectionForSectionIdentifier:SectionIdentifierTableViewHeader]) {
    return 0;
  }
  return kVerticalTableViewSectionSpacing;
}

- (NSIndexPath*)tableView:(UITableView*)tableView
    willSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  PriceNotificationsTableViewItem* item =
      base::apple::ObjCCastStrict<PriceNotificationsTableViewItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);

  if (!item.tracking) {
    return nil;
  }

  return [super tableView:tableView willSelectRowAtIndexPath:indexPath];
}

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  if (@available(iOS 16.0, *)) {
    return;
  }

  PriceNotificationsTableViewItem* item =
      base::apple::ObjCCastStrict<PriceNotificationsTableViewItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);
  [self.mutator navigateToWebpageForItem:item];
}

- (BOOL)tableView:(UITableView*)tableView
    canPerformPrimaryActionForRowAtIndexPath:(NSIndexPath*)indexPath {
  PriceNotificationsTableViewItem* item =
      base::apple::ObjCCastStrict<PriceNotificationsTableViewItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);

  return item.tracking;
}

- (void)tableView:(UITableView*)tableView
    performPrimaryActionForRowAtIndexPath:(NSIndexPath*)indexPath {
  PriceNotificationsTableViewItem* item =
      base::apple::ObjCCastStrict<PriceNotificationsTableViewItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);
  [self.mutator navigateToWebpageForItem:item];
}

- (UIView*)tableView:(UITableView*)tableView
    viewForHeaderInSection:(NSInteger)section {
  UIView* header = [super tableView:tableView viewForHeaderInSection:section];
  TableViewTextHeaderFooterView* link =
      base::apple::ObjCCast<TableViewTextHeaderFooterView>(header);
  if (link) {
    link.delegate = self;
  }

  return header;
}

#pragma mark - TableViewTextHeaderFooterItemDelegate

- (void)view:(TableViewTextHeaderFooterView*)view didTapLinkURL:(CrURL*)URL {
  if (URL.gurl == GURL(kBookmarksSettingsURL)) {
    [self.mutator navigateToBookmarks];
  }
}

#pragma mark - PriceNotificationsConsumer

- (void)setTrackableItem:(PriceNotificationsTableViewItem*)trackableItem
       currentlyTracking:(BOOL)currentlyTracking {
  _shouldHideLoadingState = YES;
  [self initializeTableViewModelIfNeeded];
  [self removeLoadingState];
  self.itemOnCurrentSiteIsTracked = currentlyTracking;
  [self.tableViewModel
                     setHeader:
                         [self createHeaderForSectionIndex:
                                   SectionIdentifierTrackableItemsOnCurrentSite
                                                   isEmpty:!trackableItem]
      forSectionWithIdentifier:SectionIdentifierTrackableItemsOnCurrentSite];

  if (trackableItem && !currentlyTracking) {
    [self addItem:trackableItem
             toBeginning:YES
               ofSection:SectionIdentifierTrackableItemsOnCurrentSite
        withRowAnimation:UITableViewRowAnimationAutomatic];
  }

  if (!self.viewIfLoaded.window) {
    return;
  }

  [self.tableView
        reloadSections:[self createIndexSetForSectionIdentifiers:
                                 {SectionIdentifierTrackableItemsOnCurrentSite}]
      withRowAnimation:UITableViewRowAnimationAutomatic];
}

- (void)addTrackedItem:(PriceNotificationsTableViewItem*)trackedItem
           toBeginning:(BOOL)beginning {
  _shouldHideLoadingState = YES;
  [self initializeTableViewModelIfNeeded];
  [self removeLoadingState];
  TableViewModel* model = self.tableViewModel;
  if (!_hasTrackedItems) {
    [model setHeader:
               [self createHeaderForSectionIndex:SectionIdentifierTrackedItems
                                         isEmpty:NO]
        forSectionWithIdentifier:SectionIdentifierTrackedItems];

    trackedItem.type = ItemTypeListItem;
    trackedItem.delegate = self;
    if (beginning) {
      [self.tableViewModel insertItem:trackedItem
              inSectionWithIdentifier:SectionIdentifierTrackedItems
                              atIndex:0];
    } else {
      [self.tableViewModel addItem:trackedItem
           toSectionWithIdentifier:SectionIdentifierTrackedItems];
    }

    if (self.viewIfLoaded.window) {
      [self.tableView reloadSections:[self createIndexSetForSectionIdentifiers:
                                               {SectionIdentifierTrackedItems}]
                    withRowAnimation:UITableViewRowAnimationFade];
    }
    _hasTrackedItems = YES;
    return;
  }

  _hasTrackedItems = YES;
  [self addItem:trackedItem
           toBeginning:beginning
             ofSection:SectionIdentifierTrackedItems
      withRowAnimation:UITableViewRowAnimationTop];
}

- (void)didStopPriceTrackingItem:(PriceNotificationsTableViewItem*)trackedItem
                   onCurrentSite:(BOOL)isViewingProductSite {
  TableViewModel* model = self.tableViewModel;
  SectionIdentifier trackedSection = SectionIdentifierTrackedItems;
  SectionIdentifier trackableSection =
      SectionIdentifierTrackableItemsOnCurrentSite;
  BOOL addItemToTrackableSection =
      isViewingProductSite && ![model hasItemForItemType:ItemTypeListItem
                                       sectionIdentifier:trackableSection];

  trackedItem.tracking = NO;
  NSIndexPath* index = [model indexPathForItem:trackedItem];
  [model removeItemWithType:ItemTypeListItem
      fromSectionWithIdentifier:trackedSection
                        atIndex:index.item];
  BOOL reloadTrackedSection = ![model hasItemForItemType:ItemTypeListItem
                                       sectionIdentifier:trackedSection];

  if (reloadTrackedSection) {
    _hasTrackedItems = NO;
    [model setHeader:
               [self createHeaderForSectionIndex:SectionIdentifierTrackedItems
                                         isEmpty:YES]
        forSectionWithIdentifier:trackedSection];
  }

  if (addItemToTrackableSection) {
    self.itemOnCurrentSiteIsTracked = NO;
    [model setHeader:[self createHeaderForSectionIndex:trackableSection
                                               isEmpty:NO]
        forSectionWithIdentifier:trackableSection];
    [model insertItem:trackedItem
        inSectionWithIdentifier:trackableSection
                        atIndex:0];
  }

  NSString* messageText = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_MENU_ITEM_STOP_TRACKING_SNACKBAR);
  MDCSnackbarMessage* message = CreateSnackbarMessage(messageText);
  [self.snackbarCommandsHandler
      showSnackbarMessage:message
           withHapticType:UINotificationFeedbackTypeSuccess];

  if (!self.viewIfLoaded.window) {
    return;
  }

  [self.tableView
      performBatchUpdates:^{
        UITableViewRowAnimation animation = UITableViewRowAnimationMiddle;
        if (addItemToTrackableSection) {
          [self.tableView
                reloadSections:[self createIndexSetForSectionIdentifiers:
                                         {trackableSection}]
              withRowAnimation:UITableViewRowAnimationFade];
          animation = UITableViewRowAnimationFade;
        }

        if (reloadTrackedSection) {
          [self.tableView
                reloadSections:
                    [self createIndexSetForSectionIdentifiers:{trackedSection}]
              withRowAnimation:UITableViewRowAnimationFade];
          return;
        }

        [self.tableView deleteRowsAtIndexPaths:@[ index ]
                              withRowAnimation:animation];
      }
               completion:^(BOOL completion){
               }];
}

- (void)didStartPriceTrackingForItem:
    (PriceNotificationsTableViewItem*)trackableItem {
  TableViewModel* model = self.tableViewModel;
  SectionIdentifier trackableSectionID =
      SectionIdentifierTrackableItemsOnCurrentSite;
  _hasExecutedItemTracking = YES;

  // This code removes the price trackable item from the trackable section and
  // adds it to the tracked section at the beginning of the list. It assumes
  // that there exists only one trackable item per page.
  [model removeItemWithType:ItemTypeListItem
      fromSectionWithIdentifier:trackableSectionID
                        atIndex:0];
  self.itemOnCurrentSiteIsTracked = YES;
  [model setHeader:[self createHeaderForSectionIndex:trackableSectionID
                                             isEmpty:YES]
      forSectionWithIdentifier:trackableSectionID];

  [self.tableView
        reloadSections:[self createIndexSetForSectionIdentifiers:
                                 {SectionIdentifierTrackableItemsOnCurrentSite}]
      withRowAnimation:UITableViewRowAnimationFade];

  [self addTrackedItem:trackableItem toBeginning:YES];
}

- (void)resetPriceTrackingItem:(PriceNotificationsTableViewItem*)item {
  PriceNotificationsTableViewCell* cell = [self.tableView
      cellForRowAtIndexPath:[self.tableViewModel indexPathForItem:item]];
  [cell.trackButton setUserInteractionEnabled:YES];
}

#pragma mark - PriceNotificationsTableViewCellDelegate

- (void)trackItemForCell:(PriceNotificationsTableViewCell*)cell {
  NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
  PriceNotificationsTableViewItem* item =
      base::apple::ObjCCastStrict<PriceNotificationsTableViewItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);
  [self.mutator trackItem:item];
}

- (void)stopTrackingItemForCell:(PriceNotificationsTableViewCell*)cell {
  NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
  PriceNotificationsTableViewItem* item =
      base::apple::ObjCCastStrict<PriceNotificationsTableViewItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);
  [self.mutator stopTrackingItem:item];
}

#pragma mark - Item Loading Helpers

// Adds an item to `sectionID` and displays it to the UI.
- (void)addItem:(PriceNotificationsTableViewItem*)item
         toBeginning:(BOOL)toBeginning
           ofSection:(SectionIdentifier)sectionID
    withRowAnimation:(UITableViewRowAnimation)animation {
  if (sectionID == SectionIdentifierTrackableItemsOnCurrentSite &&
      [self.tableViewModel
          hasItemForItemType:ItemTypeListItem
           sectionIdentifier:SectionIdentifierTrackableItemsOnCurrentSite]) {
    return;
  }

  DCHECK(item);
  item.type = ItemTypeListItem;
  item.delegate = self;
  if (toBeginning) {
    [self.tableViewModel insertItem:item
            inSectionWithIdentifier:sectionID
                            atIndex:0];
  } else {
    [self.tableViewModel addItem:item toSectionWithIdentifier:sectionID];
  }

  if (!self.viewIfLoaded.window) {
    return;
  }

  [self.tableView
      insertRowsAtIndexPaths:@[ [self.tableViewModel indexPathForItem:item] ]
            withRowAnimation:animation];
}

// Returns a TableViewHeaderFooterItem that displays the title for the section
// designated by `sectionID`.
- (TableViewHeaderFooterItem*)createHeaderForSectionIndex:
                                  (SectionIdentifier)sectionID
                                                  isEmpty:(BOOL)isEmpty {
  switch (sectionID) {
    case SectionIdentifierTrackableItemsOnCurrentSite: {
      return [self createHeaderForTrackableSection:isEmpty];
    }
    case SectionIdentifierTrackedItems: {
      return [self createHeaderForTrackedSection:isEmpty];
    }
    case SectionIdentifierTableViewHeader: {
      return [self createHeaderForTableViewHeaderSection:isEmpty];
    }
  }

  return nil;
}

// Constructs the TableViewModel's sections and section headers and initializes
// their values in accordance with the desired default empty state.
- (void)initializeTableViewModelIfNeeded {
  if (_hasModelBeenInitialized) {
    return;
  }

  _hasModelBeenInitialized = YES;
  [super loadModel];
  SectionIdentifier trackableSectionID =
      SectionIdentifierTrackableItemsOnCurrentSite;
  SectionIdentifier trackedSectionID = SectionIdentifierTrackedItems;
  TableViewModel* model = self.tableViewModel;

  [model addSectionWithIdentifier:SectionIdentifierTableViewHeader];
  [model setHeader:
             [self createHeaderForSectionIndex:SectionIdentifierTableViewHeader
                                       isEmpty:YES]
      forSectionWithIdentifier:SectionIdentifierTableViewHeader];

  [model addSectionWithIdentifier:trackableSectionID];
  [model setHeader:[self createHeaderForSectionIndex:trackableSectionID
                                             isEmpty:NO]
      forSectionWithIdentifier:trackableSectionID];

  [model addSectionWithIdentifier:trackedSectionID];
  [model setHeader:[self createHeaderForSectionIndex:trackedSectionID
                                             isEmpty:YES]
      forSectionWithIdentifier:trackedSectionID];
}

// Adds placeholder items into each section.
- (void)addLoadingStateItems {
  if (_shouldHideLoadingState) {
    return;
  }

  [self.tableViewModel setHeader:[self createHeaderForSectionIndex:
                                           SectionIdentifierTrackedItems
                                                           isEmpty:NO]
        forSectionWithIdentifier:SectionIdentifierTrackedItems];

  PriceNotificationsTableViewItem* emptyTrackableItem =
      [[PriceNotificationsTableViewItem alloc] init];
  PriceNotificationsTableViewItem* emptyTrackedItem =
      [[PriceNotificationsTableViewItem alloc] init];
  emptyTrackableItem.loading = YES;
  emptyTrackedItem.loading = YES;
  emptyTrackedItem.tracking = YES;
  [self addItem:emptyTrackableItem
           toBeginning:YES
             ofSection:SectionIdentifierTrackableItemsOnCurrentSite
      withRowAnimation:UITableViewRowAnimationAutomatic];
  [self addItem:emptyTrackedItem
           toBeginning:YES
             ofSection:SectionIdentifierTrackedItems
      withRowAnimation:UITableViewRowAnimationAutomatic];
  [self addItem:emptyTrackedItem
           toBeginning:YES
             ofSection:SectionIdentifierTrackedItems
      withRowAnimation:UITableViewRowAnimationAutomatic];
  _displayedLoadingState = YES;
}

// Removes the placeholder items from each section.
- (void)removeLoadingState {
  if (!_displayedLoadingState) {
    return;
  }

  _displayedLoadingState = NO;
  TableViewModel* model = self.tableViewModel;

  NSMutableArray<NSIndexPath*>* itemIndexPaths =
      [[model indexPathsForItemType:ItemTypeListItem
                  sectionIdentifier:SectionIdentifierTrackedItems] mutableCopy];
  [itemIndexPaths
      addObject:[model indexPathForItemType:ItemTypeListItem
                          sectionIdentifier:
                              SectionIdentifierTrackableItemsOnCurrentSite]];

  [model removeItemWithType:ItemTypeListItem
      fromSectionWithIdentifier:SectionIdentifierTrackableItemsOnCurrentSite
                        atIndex:0];
  [model setHeader:[self
                       createHeaderForSectionIndex:SectionIdentifierTrackedItems
                                           isEmpty:YES]
      forSectionWithIdentifier:SectionIdentifierTrackedItems];
  [model deleteAllItemsFromSectionWithIdentifier:SectionIdentifierTrackedItems];

  if (!self.viewIfLoaded.window) {
    return;
  }

  [self.tableView deleteRowsAtIndexPaths:itemIndexPaths
                        withRowAnimation:UITableViewRowAnimationAutomatic];
  [self.tableView reloadSections:[self createIndexSetForSectionIdentifiers:
                                           {SectionIdentifierTrackedItems}]
                withRowAnimation:UITableViewRowAnimationAutomatic];
}

// Creates the IndexSet that encapsulate the various sections provided in
// `sectionIDs`
- (NSIndexSet*)createIndexSetForSectionIdentifiers:
    (std::vector<SectionIdentifier>)sectionIDs {
  NSMutableIndexSet* indexSet = [[NSMutableIndexSet alloc] init];
  for (auto& sectionID : sectionIDs) {
    NSUInteger sectionIndex =
        [self.tableViewModel sectionForSectionIdentifier:sectionID];
    [indexSet addIndex:sectionIndex];
  }
  return indexSet;
}

// Creates the TableViewHeaderFooterItem for the section
// `SectionIdentifierTrackableItemsOnCurrentSite`
- (TableViewHeaderFooterItem*)createHeaderForTrackableSection:(BOOL)isEmpty {
  TableViewTextHeaderFooterItem* header =
      [[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader];
  header.text = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_TRACKABLE_SECTION_HEADER);
  if (!self.itemOnCurrentSiteIsTracked && !isEmpty) {
    return header;
  }

  if (self.itemOnCurrentSiteIsTracked && _hasExecutedItemTracking) {
    header.subtitle = l10n_util::GetNSString(
        IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_DESCRIPTION_FOR_TRACKED_ITEM);
    header.URLs = @[ [[CrURL alloc] initWithGURL:GURL(kBookmarksSettingsURL)] ];
    return header;
  }

  if (self.itemOnCurrentSiteIsTracked) {
    header.subtitle = l10n_util::GetNSString(
        IDS_IOS_PRICE_NOTIFICAITONS_PRICE_TRACK_TRACKABLE_ITEM_IS_TRACKED);
    return header;
  }

  header.subtitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_TRACKABLE_EMPTY_LIST);
  return header;
}

// Creates the TableViewHeaderFooterItem for the section
// `SectionIdentifierTrackedItems`
- (TableViewHeaderFooterItem*)createHeaderForTrackedSection:(BOOL)isEmpty {
  TableViewTextHeaderFooterItem* header =
      [[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader];
  header.text = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_TRACKED_SECTION_HEADER);
  if (isEmpty) {
    header.subtitle = l10n_util::GetNSString(
        IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_TRACKING_EMPTY_LIST);
    return header;
  }

  return header;
}

// Creates the TableViewHeaderFooterItem for the section
// `SectionIdentifierTableViewHeader`
- (TableViewTextHeaderFooterItem*)createHeaderForTableViewHeaderSection:
    (BOOL)isEmpty {
  TableViewTextHeaderFooterItem* header = [[TableViewTextHeaderFooterItem alloc]
      initWithType:ItemTypeTableViewHeader];

  if (isEmpty && !self.hasPreviouslyViewed) {
    header.subtitle = l10n_util::GetNSString(
        IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_DESCRIPTION_EMPTY_STATE);
    return header;
  }

  header.subtitle = l10n_util::GetNSString(
      IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_DESCRIPTION);
  return header;
}

@end