// Copyright 2023 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/content_suggestions/magic_stack/magic_stack_module_container.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client_id.h"
#import "ios/chrome/browser/push_notification/model/push_notification_settings_util.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_tile_layout_util.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/most_visited_tiles_config.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_utils.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_constants.h"
#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_constants.h"
#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_module.h"
#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_module_container_delegate.h"
#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_module_content_view_delegate.h"
#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_module_contents_factory.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_state.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/utils.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"
namespace {
// The horizontal inset for the content within this container.
const CGFloat kContentHorizontalInset = 20.0f;
// The top inset for the content within this container.
const CGFloat kContentTopInset = 16.0f;
// The bottom inset for the content within this container.
const CGFloat kContentBottomInset = 24.0f;
const CGFloat kReducedContentBottomInset = 10.0f;
// Vertical spacing between the content views.
const CGFloat kContentVerticalSpacing = 16.0f;
// The corner radius of this container.
const float kCornerRadius = 24;
const CGFloat kSeparatorHeight = 0.5;
} // namespace
@interface MagicStackModuleContainer () <UIContextMenuInteractionDelegate,
MagicStackModuleContentViewDelegate>
// Redefined as ReadWrite.
@property(nonatomic, assign, readwrite) ContentSuggestionsModuleType type;
@end
@implementation MagicStackModuleContainer {
UILabel* _title;
UILabel* _subtitle;
BOOL _isPlaceholder;
UIButton* _seeMoreButton;
UIButton* _notificationsOptInButton;
UIView* _contentView;
UIView* _separator;
UIStackView* _stackView;
UIImageView* _placeholderImage;
UIStackView* _titleStackView;
MagicStackModuleContentsFactory* _magicStackModuleContentsFactory;
NSLayoutConstraint* _containerHeightAnchor;
NSLayoutConstraint* _contentStackViewBottomMarginAnchor;
UIContextMenuInteraction* _contextMenuInteraction;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.maximumContentSizeCategory = UIContentSizeCategoryAccessibilityMedium;
_magicStackModuleContentsFactory = [[MagicStackModuleContentsFactory alloc] init];
self.contentView.backgroundColor = [UIColor colorNamed:kBackgroundColor];
self.contentView.layer.cornerRadius = kCornerRadius;
self.contentView.clipsToBounds = YES;
self.layer.cornerRadius = kCornerRadius;
_titleStackView = [[UIStackView alloc] init];
_titleStackView.alignment = UIStackViewAlignmentTop;
_titleStackView.axis = UILayoutConstraintAxisHorizontal;
_titleStackView.distribution = UIStackViewDistributionFill;
// Resist Vertical expansion so all titles are the same height, allowing
// content view to fill the rest of the module space.
[_titleStackView setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
_title = [[UILabel alloc] init];
_title.font = [self fontForTitle];
_title.textColor = [UIColor colorNamed:kTextPrimaryColor];
_title.numberOfLines = 1;
_title.lineBreakMode = NSLineBreakByTruncatingTail;
_title.accessibilityTraits |= UIAccessibilityTraitHeader;
[_title setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
[_title
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:
UILayoutConstraintAxisHorizontal];
[_title
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[_titleStackView addArrangedSubview:_title];
// `setContentHuggingPriority:` does not guarantee that _titleStackView
// completely resists vertical expansion since UIStackViews do not have
// intrinsic contentSize. Constraining the title label to the StackView will
// ensure contentView expands.
[NSLayoutConstraint activateConstraints:@[
[_title.bottomAnchor constraintEqualToAnchor:_titleStackView.bottomAnchor]
]];
_seeMoreButton = [self
actionButton:l10n_util::GetNSString(IDS_IOS_MAGIC_STACK_SEE_MORE)];
_seeMoreButton.hidden = YES;
[_seeMoreButton addTarget:self
action:@selector(seeMoreButtonWasTapped:)
forControlEvents:UIControlEventTouchUpInside];
[_titleStackView addArrangedSubview:_seeMoreButton];
_notificationsOptInButton =
[self actionButton:l10n_util::GetNSString(
IDS_IOS_MAGIC_STACK_TURN_ON_NOTIFICATIONS)];
_notificationsOptInButton.hidden = YES;
[_notificationsOptInButton
addTarget:self
action:@selector(notificationsOptInButtonWasTapped:)
forControlEvents:UIControlEventTouchUpInside];
[_titleStackView addArrangedSubview:_notificationsOptInButton];
_subtitle = [[UILabel alloc] init];
_subtitle.hidden = YES;
_subtitle.font = [MagicStackModuleContainer fontForSubtitle];
_subtitle.textColor = [UIColor colorNamed:kTextSecondaryColor];
_subtitle.numberOfLines = 0;
_subtitle.lineBreakMode = NSLineBreakByWordWrapping;
_subtitle.accessibilityTraits |= UIAccessibilityTraitHeader;
[_subtitle setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[_subtitle
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:
UILayoutConstraintAxisHorizontal];
_subtitle.textAlignment =
UseRTLLayout() ? NSTextAlignmentLeft : NSTextAlignmentRight;
[_titleStackView addArrangedSubview:_subtitle];
_stackView = [[UIStackView alloc] init];
_stackView.translatesAutoresizingMaskIntoConstraints = NO;
_stackView.alignment = UIStackViewAlignmentFill;
_stackView.axis = UILayoutConstraintAxisVertical;
_stackView.spacing = kContentVerticalSpacing;
_stackView.distribution = UIStackViewDistributionFill;
[_stackView addArrangedSubview:_titleStackView];
_separator = [[UIView alloc] init];
[_separator setContentHuggingPriority:UILayoutPriorityDefaultHigh
forAxis:UILayoutConstraintAxisVertical];
_separator.backgroundColor = [UIColor colorNamed:kSeparatorColor];
[_stackView addArrangedSubview:_separator];
[NSLayoutConstraint activateConstraints:@[
[_separator.heightAnchor
constraintEqualToConstant:AlignValueToPixel(kSeparatorHeight)],
[_separator.leadingAnchor
constraintEqualToAnchor:_stackView.leadingAnchor],
[_separator.trailingAnchor
constraintEqualToAnchor:_stackView.trailingAnchor],
]];
_containerHeightAnchor =
[self.heightAnchor constraintEqualToConstant:kModuleMaxHeight];
[NSLayoutConstraint activateConstraints:@[ _containerHeightAnchor ]];
[self.contentView addSubview:_stackView];
AddSameConstraintsToSidesWithInsets(
_stackView, self,
(LayoutSides::kTop | LayoutSides::kLeading | LayoutSides::kTrailing),
NSDirectionalEdgeInsetsMake(kContentTopInset, kContentHorizontalInset,
0, kContentHorizontalInset));
_contentStackViewBottomMarginAnchor =
[_stackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor
constant:-kContentBottomInset];
[NSLayoutConstraint
activateConstraints:@[ _contentStackViewBottomMarginAnchor ]];
}
return self;
}
- (void)dealloc {
[self resetCell];
}
// Creates a button with the specified `title` positioned in the module's
// top-right corner.
//
// NOTE: This helper method does not associate an action with the generated
// button. Use `-addTarget:action:forControlEvents:` to attach an action.
- (UIButton*)actionButton:(NSString*)title {
UIButton* button = [[UIButton alloc] init];
UIButtonConfiguration* buttonConfiguration =
[UIButtonConfiguration plainButtonConfiguration];
buttonConfiguration.contentInsets = NSDirectionalEdgeInsetsZero;
buttonConfiguration.titleLineBreakMode = NSLineBreakByWordWrapping;
buttonConfiguration.attributedTitle = [[NSAttributedString alloc]
initWithString:title
attributes:@{
NSFontAttributeName :
[UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]
}];
button.configuration = buttonConfiguration;
[button setTitleColor:[UIColor colorNamed:kBlueColor]
forState:UIControlStateNormal];
button.titleLabel.numberOfLines = 2;
button.titleLabel.adjustsFontForContentSizeCategory = YES;
button.contentHorizontalAlignment =
UIControlContentHorizontalAlignmentTrailing;
[button
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[button setContentHuggingPriority:UILayoutPriorityDefaultHigh
forAxis:UILayoutConstraintAxisHorizontal];
button.pointerInteractionEnabled = YES;
button.accessibilityIdentifier = button.titleLabel.text;
return button;
}
- (void)configureWithConfig:(MagicStackModule*)config {
[self resetCell];
// Ensures that the modules conforms to a height of kModuleMaxHeight. For
// the MVT when it lives outside of the Magic Stack to stay as close to its
// intrinsic size as possible, the constraint is configured to be less than
// or equal to.
if (config.type == ContentSuggestionsModuleType::kMostVisited &&
!ShouldPutMostVisitedSitesInMagicStack()) {
_containerHeightAnchor.active = NO;
_containerHeightAnchor = [self.heightAnchor
constraintLessThanOrEqualToConstant:kModuleMaxHeight];
[NSLayoutConstraint activateConstraints:@[ _containerHeightAnchor ]];
}
if (config.type == ContentSuggestionsModuleType::kPlaceholder) {
_isPlaceholder = YES;
_placeholderImage = [[UIImageView alloc]
initWithImage:[UIImage imageNamed:@"magic_stack_placeholder_module"]];
_placeholderImage.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_placeholderImage];
AddSameConstraints(_placeholderImage, self);
[self bringSubviewToFront:_placeholderImage];
_separator.hidden = YES;
return;
}
_type = config.type;
if ([self allowsLongPress]) {
if (!_contextMenuInteraction) {
_contextMenuInteraction =
[[UIContextMenuInteraction alloc] initWithDelegate:self];
[self addInteraction:_contextMenuInteraction];
}
}
_title.text = [MagicStackModuleContainer titleStringForModule:_type];
_title.accessibilityIdentifier =
[MagicStackModuleContainer accessibilityIdentifierForModule:_type];
_seeMoreButton.hidden = !config.shouldShowSeeMore;
// The "See More" button takes precedence over the notifications opt-in
// button.
_notificationsOptInButton.hidden =
config.shouldShowSeeMore || !config.showNotificationsOptIn;
if ([self shouldShowSubtitle]) {
// TODO(crbug.com/40279482): Update MagicStackModuleContainer to take an id
// config in its initializer so the container can build itself from a
// passed config/state object.
NSString* subtitle = [self subtitleStringForConfig:config];
_subtitle.text = subtitle;
_subtitle.accessibilityIdentifier = subtitle;
_subtitle.hidden = NO;
}
if ([_title.text length] == 0) {
[_titleStackView removeFromSuperview];
}
_separator.hidden = ![self shouldShowSeparator];
_contentView = [_magicStackModuleContentsFactory
contentViewForConfig:config
traitCollection:self.traitCollection
contentViewDelegate:self];
[_stackView addArrangedSubview:_contentView];
// Configures `contentView` to be the view willing to expand if needed to
// fill extra vertical space in the container.
[_contentView
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisVertical];
[self updateBottomContentMarginsForConfig:config];
NSMutableArray* accessibilityElements =
[[NSMutableArray alloc] initWithObjects:_title, nil];
if (config.shouldShowSeeMore) {
[accessibilityElements addObject:_seeMoreButton];
} else if (config.showNotificationsOptIn) {
[accessibilityElements addObject:_notificationsOptInButton];
} else if ([self shouldShowSubtitle]) {
[accessibilityElements addObject:_subtitle];
}
[accessibilityElements addObject:_contentView];
self.accessibilityElements = accessibilityElements;
}
// Returns the module's title, if any, given the Magic Stack module `type`.
+ (NSString*)titleStringForModule:(ContentSuggestionsModuleType)type {
switch (type) {
case ContentSuggestionsModuleType::kShortcuts:
return l10n_util::GetNSString(
IDS_IOS_CONTENT_SUGGESTIONS_SHORTCUTS_MODULE_TITLE);
case ContentSuggestionsModuleType::kMostVisited:
if (ShouldPutMostVisitedSitesInMagicStack()) {
return l10n_util::GetNSString(
IDS_IOS_CONTENT_SUGGESTIONS_MOST_VISITED_MODULE_TITLE);
}
return @"";
case ContentSuggestionsModuleType::kTabResumption:
return l10n_util::GetNSString(IDS_IOS_TAB_RESUMPTION_TITLE);
case ContentSuggestionsModuleType::kSetUpListSync:
case ContentSuggestionsModuleType::kSetUpListDefaultBrowser:
case ContentSuggestionsModuleType::kSetUpListAutofill:
case ContentSuggestionsModuleType::kCompactedSetUpList:
case ContentSuggestionsModuleType::kSetUpListAllSet:
case ContentSuggestionsModuleType::kSetUpListNotifications:
return content_suggestions::SetUpListTitleString();
case ContentSuggestionsModuleType::kSafetyCheck:
return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_TITLE);
case ContentSuggestionsModuleType::kParcelTracking:
return l10n_util::GetNSString(
IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_TITLE);
case ContentSuggestionsModuleType::kPriceTrackingPromo:
// Price Tracking Promo design does not use title.
return @"";
default:
NOTREACHED_IN_MIGRATION();
return @"";
}
}
// Returns the accessibility identifier given the Magic Stack module `type`.
+ (NSString*)accessibilityIdentifierForModule:
(ContentSuggestionsModuleType)type {
switch (type) {
case ContentSuggestionsModuleType::kTabResumption:
return kMagicStackContentSuggestionsModuleTabResumptionAccessibilityIdentifier;
default:
// TODO(crbug.com/40946679): the code should use constants for
// accessibility identifiers, and not localized strings.
return [self titleStringForModule:type];
}
}
// Returns the font for the module title string.
- (UIFont*)fontForTitle {
return CreateDynamicFont(UIFontTextStyleFootnote, UIFontWeightSemibold, self);
}
// Returns the font for the module subtitle string.
+ (UIFont*)fontForSubtitle {
return CreateDynamicFont(UIFontTextStyleFootnote, UIFontWeightRegular);
}
// Updates the bottom content margins if the module contents need it.
- (void)updateBottomContentMarginsForConfig:(MagicStackModule*)config {
switch (config.type) {
case ContentSuggestionsModuleType::kMostVisited:
case ContentSuggestionsModuleType::kShortcuts:
case ContentSuggestionsModuleType::kCompactedSetUpList:
_contentStackViewBottomMarginAnchor.constant =
-kReducedContentBottomInset;
break;
case ContentSuggestionsModuleType::kSafetyCheck: {
SafetyCheckState* safetyCheckConfig =
static_cast<SafetyCheckState*>(config);
if ([safetyCheckConfig numberOfIssues] > 1) {
_contentStackViewBottomMarginAnchor.constant =
-kReducedContentBottomInset;
}
break;
}
default:
break;
}
}
#pragma mark UICollectionViewCell Overrides
- (void)prepareForReuse {
[super prepareForReuse];
[self resetCell];
}
#pragma mark - UITraitEnvironment
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (previousTraitCollection.preferredContentSizeCategory !=
self.traitCollection.preferredContentSizeCategory) {
_title.font = [self fontForTitle];
}
}
#pragma mark - MagicStackModuleContentViewDelegate
- (void)setSubtitle:(NSString*)subtitle {
_subtitle.text = subtitle;
_subtitle.accessibilityIdentifier = subtitle;
}
#pragma mark - UIContextMenuInteractionDelegate
- (UIContextMenuConfiguration*)contextMenuInteraction:
(UIContextMenuInteraction*)interaction
configurationForMenuAtLocation:(CGPoint)location {
CHECK([self allowsLongPress]);
__weak MagicStackModuleContainer* weakSelf = self;
UIContextMenuActionProvider actionProvider =
^(NSArray<UIMenuElement*>* suggestedActions) {
return [UIMenu menuWithTitle:[weakSelf contextMenuTitle]
children:[weakSelf contextMenuActions]];
};
return
[UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
actionProvider:actionProvider];
}
#pragma mark - Helpers
// Returns the list of actions for the long-press / context menu.
- (NSArray<UIAction*>*)contextMenuActions {
NSMutableArray<UIAction*>* actions = [[NSMutableArray alloc] init];
if ((IsSetUpListModuleType(self.type) && IsIOSTipsNotificationsEnabled()) ||
(self.type == ContentSuggestionsModuleType::kSafetyCheck &&
IsSafetyCheckNotificationsEnabled())) {
[actions addObject:[self toggleNotificationsActionForModuleType:self.type]];
}
[actions addObject:[self hideAction]];
[actions addObject:[self customizeCardAction]];
return actions;
}
// Returns the menu action to hide this module type.
- (UIAction*)hideAction {
__weak __typeof(self) weakSelf = self;
UIAction* hideAction = [UIAction
actionWithTitle:[self contextMenuHideDescription]
image:DefaultSymbolWithPointSize(kHideActionSymbol, 18)
identifier:nil
handler:^(UIAction* action) {
[weakSelf.delegate neverShowModuleType:weakSelf.type];
}];
hideAction.attributes = UIMenuElementAttributesDestructive;
return hideAction;
}
// Returns the menu action to hide this module type.
- (UIAction*)customizeCardAction {
__weak __typeof(self) weakSelf = self;
UIAction* hideAction = [UIAction
actionWithTitle:
l10n_util::GetNSString(
IDS_IOS_MAGIC_STACK_CONTEXT_MENU_CUSTOMIZE_CARDS_TITLE)
image:DefaultSymbolWithPointSize(kSliderHorizontalSymbol, 18)
identifier:nil
handler:^(UIAction* action) {
[weakSelf.delegate customizeCardsWasTapped];
}];
return hideAction;
}
// Returns the `PushNotificationClientId` associated with the specified `type`.
// Currently, push notifications are exclusively supported by the Set Up List
// and Safety Check modules.
- (PushNotificationClientId)pushNotificationClientId:
(ContentSuggestionsModuleType)type {
// This is only supported for Set Up List and Safety Check modules.
CHECK(IsSetUpListModuleType(type) ||
type == ContentSuggestionsModuleType::kSafetyCheck);
if (type == ContentSuggestionsModuleType::kSafetyCheck) {
return PushNotificationClientId::kSafetyCheck;
}
if (IsSetUpListModuleType(type)) {
return PushNotificationClientId::kTips;
}
NOTREACHED();
}
// Retrieves the message ID for the push notification feature title associated
// with the specified `ContentSuggestionsModuleType`. Currently, push
// notifications are exclusively supported by the Set Up List and Safety Check
// modules.
- (int)pushNotificationTitleMessageId:(ContentSuggestionsModuleType)type {
// This is only supported for Set Up List and Safety Check modules.
CHECK(IsSetUpListModuleType(type) ||
type == ContentSuggestionsModuleType::kSafetyCheck);
if (type == ContentSuggestionsModuleType::kSafetyCheck) {
return IDS_IOS_SAFETY_CHECK_TITLE;
}
if (IsSetUpListModuleType(type)) {
return content_suggestions::SetUpListTitleStringID();
}
NOTREACHED();
}
// Returns the menu action to opt-in to Tips Notifications.
- (UIAction*)toggleNotificationsActionForModuleType:
(ContentSuggestionsModuleType)moduleType {
const PushNotificationClientId clientId =
[self pushNotificationClientId:moduleType];
BOOL optedIn = [self optedInToNotificationsForClient:clientId];
__weak __typeof(self) weakSelf = self;
NSString* title;
NSString* symbol;
int featureTitle = [self pushNotificationTitleMessageId:moduleType];
if (optedIn) {
title = l10n_util::GetNSStringF(
IDS_IOS_TIPS_NOTIFICATIONS_CONTEXT_MENU_ITEM_OFF,
l10n_util::GetStringUTF16(featureTitle));
symbol = kBellSlashSymbol;
} else {
title =
l10n_util::GetNSStringF(IDS_IOS_TIPS_NOTIFICATIONS_CONTEXT_MENU_ITEM,
l10n_util::GetStringUTF16(featureTitle));
symbol = kBellSymbol;
}
return [UIAction
actionWithTitle:title
image:DefaultSymbolWithPointSize(symbol, 18)
identifier:nil
handler:^(UIAction* action) {
if (optedIn) {
[weakSelf.delegate disableNotifications:weakSelf.type];
} else {
[weakSelf.delegate enableNotifications:weakSelf.type];
}
}];
}
// Handles taps on the "See More" button.
- (void)seeMoreButtonWasTapped:(UIButton*)button {
[_delegate seeMoreWasTappedForModuleType:_type];
}
// Handles taps on the notifications opt-in button.
- (void)notificationsOptInButtonWasTapped:(UIButton*)button {
[_delegate enableNotifications:_type];
}
// `YES` if this container should show a context menu when the user performs a
// long-press gesture.
- (BOOL)allowsLongPress {
switch (_type) {
case ContentSuggestionsModuleType::kTabResumption:
case ContentSuggestionsModuleType::kSafetyCheck:
case ContentSuggestionsModuleType::kSetUpListSync:
case ContentSuggestionsModuleType::kSetUpListDefaultBrowser:
case ContentSuggestionsModuleType::kSetUpListAutofill:
case ContentSuggestionsModuleType::kSetUpListNotifications:
case ContentSuggestionsModuleType::kCompactedSetUpList:
case ContentSuggestionsModuleType::kParcelTracking:
return YES;
default:
return NO;
}
}
// Determines if a subtitle should be displayed based on the
// `ContentSuggestionsModuleType`. Returns `NO` if a Magic Stack module action
// button is currently displayed.
- (BOOL)shouldShowSubtitle {
if (!_seeMoreButton.isHidden || !_notificationsOptInButton.isHidden) {
return NO;
}
if (_type == ContentSuggestionsModuleType::kSafetyCheck) {
return YES;
}
return NO;
}
// Returns the module's subtitle, if any, given the Magic Stack module `type`.
- (NSString*)subtitleStringForConfig:(MagicStackModule*)config {
if (config.type == ContentSuggestionsModuleType::kSafetyCheck) {
SafetyCheckState* safetyCheckConfig =
static_cast<SafetyCheckState*>(config);
return FormatElapsedTimeSinceLastSafetyCheck(safetyCheckConfig.lastRunTime);
}
return @"";
}
// Based on ContentSuggestionsModuleType, returns YES if a separator should be
// shown between the module title/subtitle row, and the remaining bottom-half of
// the module.
- (BOOL)shouldShowSeparator {
switch (_type) {
case ContentSuggestionsModuleType::kSetUpListSync:
case ContentSuggestionsModuleType::kSetUpListDefaultBrowser:
case ContentSuggestionsModuleType::kSetUpListAutofill:
case ContentSuggestionsModuleType::kSetUpListAllSet:
case ContentSuggestionsModuleType::kSetUpListNotifications:
case ContentSuggestionsModuleType::kSafetyCheck:
return YES;
case ContentSuggestionsModuleType::kTabResumption:
return !IsTabResumption1_5Enabled();
default:
return NO;
}
}
// Title string for the context menu of this container.
- (NSString*)contextMenuTitle {
switch (_type) {
case ContentSuggestionsModuleType::kTabResumption:
return l10n_util::GetNSString(IDS_IOS_TAB_RESUMPTION_CONTEXT_MENU_TITLE);
case ContentSuggestionsModuleType::kSafetyCheck:
return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_CONTEXT_MENU_TITLE);
case ContentSuggestionsModuleType::kSetUpListSync:
case ContentSuggestionsModuleType::kSetUpListDefaultBrowser:
case ContentSuggestionsModuleType::kSetUpListAutofill:
case ContentSuggestionsModuleType::kCompactedSetUpList:
case ContentSuggestionsModuleType::kSetUpListNotifications:
return l10n_util::GetNSString(
IDS_IOS_SET_UP_LIST_HIDE_MODULE_CONTEXT_MENU_TITLE);
case ContentSuggestionsModuleType::kParcelTracking:
return l10n_util::GetNSString(IDS_IOS_PARCEL_TRACKING_CONTEXT_MENU_TITLE);
default:
NOTREACHED();
}
}
// Descriptor string for hide action of the context menu of this container.
- (NSString*)contextMenuHideDescription {
switch (_type) {
case ContentSuggestionsModuleType::kTabResumption:
return l10n_util::GetNSString(
IDS_IOS_TAB_RESUMPTION_CONTEXT_MENU_DESCRIPTION);
case ContentSuggestionsModuleType::kSafetyCheck:
return l10n_util::GetNSString(
IDS_IOS_SAFETY_CHECK_CONTEXT_MENU_DESCRIPTION);
case ContentSuggestionsModuleType::kSetUpListSync:
case ContentSuggestionsModuleType::kSetUpListDefaultBrowser:
case ContentSuggestionsModuleType::kSetUpListAutofill:
case ContentSuggestionsModuleType::kSetUpListNotifications:
case ContentSuggestionsModuleType::kCompactedSetUpList:
return l10n_util::GetNSStringF(
IDS_IOS_SET_UP_LIST_HIDE_MODULE_CONTEXT_MENU_DESCRIPTION,
l10n_util::GetStringUTF16(
content_suggestions::SetUpListTitleStringID()));
case ContentSuggestionsModuleType::kParcelTracking:
return l10n_util::GetNSStringF(
IDS_IOS_PARCEL_TRACKING_CONTEXT_MENU_DESCRIPTION,
base::SysNSStringToUTF16(l10n_util::GetNSString(
IDS_IOS_CONTENT_SUGGESTIONS_PARCEL_TRACKING_MODULE_TITLE)));
default:
NOTREACHED();
}
}
// Reset the main configurations of the cell.
- (void)resetCell {
_title.text = nil;
_subtitle.text = nil;
_isPlaceholder = NO;
if (_placeholderImage) {
[_placeholderImage removeFromSuperview];
_placeholderImage = nil;
}
if (_contentView) {
[_contentView removeFromSuperview];
_contentView = nil;
}
if (_contextMenuInteraction) {
[self removeInteraction:_contextMenuInteraction];
_contextMenuInteraction = nil;
}
}
// Returns YES if the user has already opted-in to notifications for the
// specified `clientId`.
- (BOOL)optedInToNotificationsForClient:(PushNotificationClientId)clientId {
// Currently, push notifications are exclusively supported for the Set Up List
// and Safety Check modules.
CHECK(clientId == PushNotificationClientId::kTips ||
clientId == PushNotificationClientId::kSafetyCheck);
// IMPORTANT: Notifications for Set Up List and Safety Check are managed
// through the app-wide notification settings. If a feature that utilizes
// per-profile notification settings is being introduced, ensure a `gaia_id`
// is passed to `GetMobileNotificationPermissionStatusForClient()` below.
return push_notification_settings::
GetMobileNotificationPermissionStatusForClient(clientId, "");
}
@end