// 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/cells/price_notifications_table_view_item.h"
#import "base/strings/sys_string_conversions.h"
#import "components/url_formatter/elide_url.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_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_menu_button.h"
#import "ios/chrome/browser/ui/price_notifications/cells/price_notifications_price_chip_view.h"
#import "ios/chrome/browser/ui/price_notifications/cells/price_notifications_table_view_cell_delegate.h"
#import "ios/chrome/browser/ui/price_notifications/cells/price_notifications_track_button.h"
#import "ios/chrome/browser/ui/price_notifications/price_notifications_constants.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/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"
namespace {
const CGFloat kCellContentHeight = 64.0;
const CGFloat kCellContentSpacing = 14;
const CGFloat kTableViewColumnSpacing = 8;
// Notification icon's point size.
const CGFloat kNotificationIconPointSize = 20;
// The space in between elements in the vertical UIStackView element.
const CGFloat kVerticalStackViewElementSpacing = 7;
// The following properties define the dimensions of the various placeholder
// elements that compose the loading screen.
const CGFloat kTitlePlaceholderHeight = 16;
const CGFloat kTitlePlaceholderWidth = 120;
const CGFloat kURLPlaceholderWidth = 94;
const CGFloat kPriceChipPlaceholderHeight = 24;
const CGFloat kPriceChipPlaceholderWidth = 50;
const CGFloat kTrackButtonPlaceholderHeight = 28;
const CGFloat kTrackButtonPlaceholderWidth = 70;
// Identifier for the stop price tracking action item.
NSString* kActionMenuIdentifier = @"priceTrackingActionMenu";
// A container for the UIView elements that will be added to the UIStackView.
struct TableViewItemStackContent {
UIView* title;
UIView* url;
UIView* price_chip;
UIView* track_button;
UIView* menu_button;
};
// Creates an action menu for stopping a product's subscription to price
// tracking events.
UIMenu* CreateOptionMenu(void (^completion_handler)(UIAction* action)) {
UIImageConfiguration* configuration = [UIImageSymbolConfiguration
configurationWithPointSize:kNotificationIconPointSize
weight:UIImageSymbolWeightSemibold
scale:UIImageSymbolScaleMedium];
UIImage* icon = DefaultSymbolWithConfiguration(kBellSymbol, configuration);
UIAction* stop_tracking = [UIAction
actionWithTitle:
l10n_util::GetNSString(
IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_MENU_ITEM_STOP_TRACKING)
image:icon
identifier:kActionMenuIdentifier
handler:completion_handler];
// Create Action Menu
NSArray<UIMenuElement*>* menu_elements = @[ stop_tracking ];
return [UIMenu menuWithChildren:menu_elements];
}
// Creates an empty rectangular UIView that represents the
// placeholder for an element while the data loads.
UIView* CreatePlaceholderElement() {
UIView* placeholder = [[UIView alloc] initWithFrame:CGRectZero];
placeholder.backgroundColor = [UIColor colorNamed:kGrey100Color];
placeholder.translatesAutoresizingMaskIntoConstraints = NO;
return placeholder;
}
// A function that creates the horizontal UIStackView that contains and formats
// the bulk of the item's UI elements.
UIStackView* CreateHorizontalStack(TableViewItemStackContent content) {
// Use stack views to layout the subviews except for the Price Notification
// Image.
UIStackView* verticalStack = [[UIStackView alloc] initWithArrangedSubviews:@[
content.title, content.url, content.price_chip
]];
verticalStack.axis = UILayoutConstraintAxisVertical;
verticalStack.distribution = UIStackViewDistributionEqualSpacing;
verticalStack.alignment = UIStackViewAlignmentLeading;
verticalStack.spacing = kVerticalStackViewElementSpacing;
UIStackView* horizontalStack =
[[UIStackView alloc] initWithArrangedSubviews:@[
verticalStack, content.menu_button, content.track_button
]];
horizontalStack.axis = UILayoutConstraintAxisHorizontal;
horizontalStack.spacing = kTableViewColumnSpacing;
horizontalStack.distribution = UIStackViewDistributionFill;
horizontalStack.alignment = UIStackViewAlignmentCenter;
horizontalStack.translatesAutoresizingMaskIntoConstraints = NO;
return horizontalStack;
}
// Creates the entire loading screen composed of multiple placeholder
// UIViews.
UIStackView* CreateLoadingScreen(UIView* track_button, UIView* menu_button) {
TableViewItemStackContent content = {
CreatePlaceholderElement(), CreatePlaceholderElement(),
CreatePlaceholderElement(), track_button, menu_button};
content.title.layer.cornerRadius = 2;
content.url.layer.cornerRadius = 2;
content.price_chip.layer.cornerRadius = kPriceChipPlaceholderHeight / 2;
content.menu_button.layer.cornerRadius = kPriceChipPlaceholderHeight / 2;
content.track_button.layer.cornerRadius = kTrackButtonPlaceholderHeight / 2;
UIStackView* horizontalStack = CreateHorizontalStack(content);
// Set the heights and widths of the placeholders
[NSLayoutConstraint activateConstraints:@[
[content.title.heightAnchor
constraintEqualToConstant:kTitlePlaceholderHeight],
[content.title.widthAnchor
constraintEqualToConstant:kTitlePlaceholderWidth],
[content.url.heightAnchor
constraintEqualToConstant:kTitlePlaceholderHeight],
[content.url.widthAnchor constraintEqualToConstant:kURLPlaceholderWidth],
[content.price_chip.heightAnchor
constraintEqualToConstant:kPriceChipPlaceholderHeight],
[content.price_chip.widthAnchor
constraintEqualToConstant:kPriceChipPlaceholderWidth],
[content.track_button.heightAnchor
constraintEqualToConstant:kTrackButtonPlaceholderHeight],
[content.track_button.widthAnchor
constraintEqualToConstant:kTrackButtonPlaceholderWidth],
[content.menu_button.heightAnchor
constraintEqualToConstant:kPriceChipPlaceholderHeight],
[content.menu_button.widthAnchor
constraintEqualToAnchor:content.menu_button.heightAnchor]
]];
return horizontalStack;
}
} // namespace
@implementation PriceNotificationsTableViewItem
- (instancetype)initWithType:(NSInteger)type {
self = [super initWithType:type];
if (self) {
self.cellClass = [PriceNotificationsTableViewCell class];
}
return self;
}
- (void)configureCell:(PriceNotificationsTableViewCell*)tableCell
withStyler:(ChromeTableViewStyler*)styler {
[super configureCell:tableCell withStyler:styler];
tableCell.titleLabel.text = self.title;
tableCell.entryURL = self.entryURL;
[tableCell setImage:self.productImage];
[tableCell.priceNotificationsChip setPriceDrop:self.currentPrice
previousPrice:self.previousPrice];
tableCell.tracking = self.tracking;
tableCell.accessibilityTraits |= UIAccessibilityTraitButton;
tableCell.delegate = self.delegate;
tableCell.loading = self.loading;
}
@end
#pragma mark - PriceNotificationsTableViewCell
@interface PriceNotificationsTableViewCell ()
// The imageview that is displayed on the leading edge of the cell.
@property(nonatomic, strong)
PriceNotificationsImageContainerView* priceNotificationsImageContainerView;
@end
@implementation PriceNotificationsTableViewCell {
// A blank rectangle that is displays as a placeholder for the title when the
// data is loading.
// UIView* _titlePlaceholder;
UIStackView* _placeholderStackView;
// This the UIStackView that contains the data that is returned from the
// ShoppingService.
UIStackView* _horizontalStack;
// These two properties are placeholder elements. They need to be defined as
// ivars because their visibility needs to be toggled depending on whether the
// PriceNotificationTableViewCell's `tracking` property is true or false.
UIView* _menuButtonPlaceholder;
UIView* _trackButtonPlaceholder;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString*)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font =
CreateDynamicFont(UIFontTextStyleSubheadline, UIFontWeightSemibold);
_titleLabel.adjustsFontForContentSizeCategory = YES;
_URLLabel = [[UILabel alloc] init];
_URLLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
_URLLabel.adjustsFontForContentSizeCategory = YES;
_URLLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
_trackButton = [[PriceNotificationsTrackButton alloc] init];
_menuButton = [[PriceNotificationsMenuButton alloc] init];
__weak PriceNotificationsTableViewCell* weakSelf = self;
_menuButton.menu = CreateOptionMenu(^(UIAction* action) {
[weakSelf willStopTrackingItem];
});
_menuButton.showsMenuAsPrimaryAction = YES;
_priceNotificationsChip = [[PriceNotificationsPriceChipView alloc] init];
_priceNotificationsChip.translatesAutoresizingMaskIntoConstraints = NO;
_priceNotificationsChip.isAccessibilityElement = YES;
_priceNotificationsImageContainerView =
[[PriceNotificationsImageContainerView alloc] init];
_priceNotificationsImageContainerView
.translatesAutoresizingMaskIntoConstraints = NO;
[_trackButton addTarget:self
action:@selector(trackItem)
forControlEvents:UIControlEventTouchUpInside];
TableViewItemStackContent content = {_titleLabel, _URLLabel,
_priceNotificationsChip, _trackButton,
_menuButton};
_horizontalStack = CreateHorizontalStack(content);
_menuButtonPlaceholder = CreatePlaceholderElement();
_trackButtonPlaceholder = CreatePlaceholderElement();
_placeholderStackView =
CreateLoadingScreen(_trackButtonPlaceholder, _menuButtonPlaceholder);
[self.contentView addSubview:_priceNotificationsImageContainerView];
[self.contentView addSubview:_horizontalStack];
[self.contentView addSubview:_placeholderStackView];
NSLayoutConstraint* heightConstraint = [self.contentView.heightAnchor
constraintGreaterThanOrEqualToConstant:kCellContentHeight];
// Don't set the priority to required to avoid clashing with the estimated
// height.
heightConstraint.priority = UILayoutPriorityRequired - 1;
[NSLayoutConstraint activateConstraints:@[
[self.priceNotificationsImageContainerView.leadingAnchor
constraintEqualToAnchor:self.contentView.leadingAnchor
constant:kTableViewHorizontalSpacing],
[self.priceNotificationsImageContainerView.centerYAnchor
constraintEqualToAnchor:self.contentView.centerYAnchor],
// The stack view fills the remaining space, has an intrinsic height, and
// is centered vertically.
[_horizontalStack.leadingAnchor
constraintEqualToAnchor:self.priceNotificationsImageContainerView
.trailingAnchor
constant:kTableViewHorizontalSpacing],
[_horizontalStack.trailingAnchor
constraintEqualToAnchor:self.contentView.trailingAnchor
constant:-kTableViewHorizontalSpacing],
[_horizontalStack.topAnchor
constraintGreaterThanOrEqualToAnchor:self.contentView.topAnchor
constant:kCellContentSpacing],
[_horizontalStack.centerYAnchor
constraintEqualToAnchor:self.contentView.centerYAnchor],
[_horizontalStack.bottomAnchor
constraintGreaterThanOrEqualToAnchor:self.contentView.bottomAnchor
constant:-kCellContentSpacing],
heightConstraint,
]];
AddSameConstraints(_horizontalStack, _placeholderStackView);
}
return self;
}
- (void)setImage:(UIImage*)productImage {
[self.priceNotificationsImageContainerView setImage:productImage];
}
- (void)setTracking:(BOOL)tracking {
if (tracking) {
self.trackButton.hidden = YES;
_trackButtonPlaceholder.hidden = YES;
self.menuButton.hidden = NO;
_menuButtonPlaceholder.hidden = NO;
return;
}
self.trackButton.hidden = NO;
_trackButtonPlaceholder.hidden = NO;
self.menuButton.hidden = YES;
_menuButtonPlaceholder.hidden = YES;
_tracking = tracking;
}
- (void)setEntryURL:(GURL)URL {
if (URL != _entryURL) {
_entryURL = URL;
_URLLabel.text = base::SysUTF16ToNSString(
url_formatter::
FormatUrlForDisplayOmitSchemePathTrivialSubdomainsAndMobilePrefix(
_entryURL));
}
}
- (void)setLoading:(BOOL)isLoading {
_loading = isLoading;
if (_loading) {
_horizontalStack.hidden = YES;
_placeholderStackView.hidden = NO;
return;
}
_horizontalStack.hidden = NO;
_placeholderStackView.hidden = YES;
}
#pragma mark - UITableViewCell
- (void)prepareForReuse {
[super prepareForReuse];
self.delegate = nil;
self.loading = NO;
[self.trackButton setUserInteractionEnabled:YES];
}
#pragma mark - Private
// Initiates the user's subscription to the product's price tracking events.
- (void)trackItem {
[self.trackButton setUserInteractionEnabled:NO];
[self.delegate trackItemForCell:self];
}
// Stops the user's subscription to the product's price tracking events.
- (void)willStopTrackingItem {
[self.delegate stopTrackingItemForCell:self];
}
@end