chromium/ios/chrome/browser/ntp/ui_bundled/incognito/incognito_view.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/ntp/ui_bundled/incognito/incognito_view.h"

#import "base/ios/ns_range.h"
#import "components/content_settings/core/common/features.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/drag_and_drop/model/url_drag_drop_handler.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/ntp/ui_bundled/incognito/incognito_view_util.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_url_loader_delegate.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/common/string_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/referrer.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"

namespace {

const CGFloat kStackViewHorizontalMargin = 20.0;
const CGFloat kStackViewMaxWidth = 416.0;
const CGFloat kStackViewDefaultSpacing = 20.0;
const CGFloat kStackViewImageSpacing = 22.0;
const CGFloat kLayoutGuideVerticalMargin = 8.0;
const CGFloat kLayoutGuideMinHeight = 12.0;

// The size of the incognito symbol image.
NSInteger kIncognitoSymbolImagePointSize = 72;

// Returns a font, scaled to the current dynamic type settings, that is suitable
// for the title of the incognito page.
UIFont* TitleFont() {
  return [[UIFontMetrics defaultMetrics]
      scaledFontForFont:[UIFont boldSystemFontOfSize:26.0]];
}

// Returns the color to use for body text.
UIColor* BodyTextColor() {
  return [UIColor colorNamed:kTextSecondaryColor];
}

// Returns a font, scaled to the current dynamic type settings, that is suitable
// for the body text of the incognito page.
UIFont* BodyFont() {
  return [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
}

// Returns a font, scaled to the current dynamic type settings, that is suitable
// for bolded text in the body of the incognito page.
UIFont* BoldBodyFont() {
  UIFontDescriptor* baseDescriptor = [UIFontDescriptor
      preferredFontDescriptorWithTextStyle:UIFontTextStyleSubheadline];
  UIFontDescriptor* styleDescriptor = [baseDescriptor
      fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
  // Use a `size` of 0.0 to use the default size for the descriptor.
  return [UIFont fontWithDescriptor:styleDescriptor size:0.0];
}

// Takes an HTML string containing a bulleted list and formats it to display
// properly in a UILabel.  Removes the "<ul>" tag and replaces "<li>" with a
// bullet unicode character.
NSAttributedString* FormatHTMLListForUILabel(NSString* listString) {
  listString = [listString stringByReplacingOccurrencesOfString:@"<ul>"
                                                     withString:@""];
  listString = [listString stringByReplacingOccurrencesOfString:@"</ul>"
                                                     withString:@""];

  // Use a regular expression to find and remove all leading whitespace from the
  // lines which contain the "<li>" tag.  This un-indents the bulleted lines.
  listString = [listString
      stringByReplacingOccurrencesOfString:@"\n *<li>"
                                withString:@"\n\u2022  "
                                   options:NSRegularExpressionSearch
                                     range:NSMakeRange(0, [listString length])];
  listString = [listString
      stringByTrimmingCharactersInSet:[NSCharacterSet
                                          whitespaceAndNewlineCharacterSet]];

  const StringWithTag parsedString =
      ParseStringWithTag(listString, @"<em>", @"</em>");

  NSMutableAttributedString* attributedText =
      [[NSMutableAttributedString alloc] initWithString:parsedString.string];
  [attributedText addAttribute:NSFontAttributeName
                         value:BodyFont()
                         range:NSMakeRange(0, attributedText.length)];
  if (parsedString.range != NSMakeRange(NSNotFound, 0)) {
    [attributedText addAttribute:NSFontAttributeName
                           value:BoldBodyFont()
                           range:parsedString.range];
  }
  return attributedText;
}

}  // namespace

@interface IncognitoView () <URLDropDelegate>

@end

@implementation IncognitoView {
  UIView* _containerView;
  UIStackView* _stackView;
  UILabel* _notSavedLabel;
  UILabel* _visibleDataLabel;

  // Layout Guide whose height is the height of the bottom unsafe area.
  UILayoutGuide* _bottomUnsafeAreaGuide;
  UILayoutGuide* _bottomUnsafeAreaGuideInSuperview;

  // Height constraint for adding margins for the bottom toolbar.
  NSLayoutConstraint* _bottomToolbarMarginHeight;

  // Constraint ensuring that `containerView` is at least as high as the
  // superview of the IncognitoNTPView, i.e. the Incognito panel.
  // This ensures that if the Incognito panel is higher than a compact
  // `containerView`, the `containerView`'s `topGuide` and `bottomGuide` are
  // forced to expand, centering the views in between them.
  NSArray<NSLayoutConstraint*>* _superViewConstraints;

  // Handles drop interactions for this view.
  URLDragDropHandler* _dragDropHandler;
}

- (instancetype)initWithFrame:(CGRect)frame {
  return [self initWithFrame:frame
      showTopIncognitoImageAndTitle:YES
          stackViewHorizontalMargin:kStackViewHorizontalMargin
                  stackViewMaxWidth:kStackViewMaxWidth];
}

- (instancetype)initWithFrame:(CGRect)frame
    showTopIncognitoImageAndTitle:(BOOL)showTopIncognitoImageAndTitle
        stackViewHorizontalMargin:(CGFloat)stackViewHorizontalMargin
                stackViewMaxWidth:(CGFloat)stackViewMaxWidth {
  self = [super initWithFrame:frame];
  if (self) {
    _dragDropHandler = [[URLDragDropHandler alloc] init];
    _dragDropHandler.dropDelegate = self;
    [self addInteraction:[[UIDropInteraction alloc]
                             initWithDelegate:_dragDropHandler]];

    self.alwaysBounceVertical = YES;
    // The bottom safe area is taken care of with the bottomUnsafeArea guides.
    self.contentInsetAdjustmentBehavior =
        UIScrollViewContentInsetAdjustmentNever;

    // Container to hold and vertically position the stack view.
    _containerView = [[UIView alloc] initWithFrame:frame];
    [_containerView setTranslatesAutoresizingMaskIntoConstraints:NO];

    // Stackview in which all the subviews (image, labels, button) are added.
    _stackView = [[UIStackView alloc] init];
    [_stackView setTranslatesAutoresizingMaskIntoConstraints:NO];
    _stackView.axis = UILayoutConstraintAxisVertical;
    _stackView.spacing = kStackViewDefaultSpacing;
    _stackView.distribution = UIStackViewDistributionFill;
    _stackView.alignment = UIStackViewAlignmentCenter;
    [_containerView addSubview:_stackView];

    if (showTopIncognitoImageAndTitle) {
      // Incognito image.
      UIImage* incognitoImage;
      UIImageSymbolConfiguration* configuration = [UIImageSymbolConfiguration
          configurationWithPointSize:kIncognitoSymbolImagePointSize
                              weight:UIImageSymbolWeightLight
                               scale:UIImageSymbolScaleMedium];
      incognitoImage =
          SymbolWithPalette(CustomSymbolWithConfiguration(
                                kIncognitoCircleFillSymbol, configuration),
                            LargeIncognitoPalette());

      UIImageView* incognitoImageView =
          [[UIImageView alloc] initWithImage:incognitoImage];
      incognitoImageView.tintColor = [UIColor colorNamed:kTextPrimaryColor];
      [_stackView addArrangedSubview:incognitoImageView];
      [_stackView setCustomSpacing:kStackViewImageSpacing
                         afterView:incognitoImageView];
    }

    [self addTextSectionsWithTitleShown:showTopIncognitoImageAndTitle];

    // `topGuide` and `bottomGuide` exist to vertically position the stackview
    // inside the container scrollview.
    UILayoutGuide* topGuide = [[UILayoutGuide alloc] init];
    UILayoutGuide* bottomGuide = [[UILayoutGuide alloc] init];
    _bottomUnsafeAreaGuide = [[UILayoutGuide alloc] init];
    [_containerView addLayoutGuide:topGuide];
    [_containerView addLayoutGuide:bottomGuide];
    [_containerView addLayoutGuide:_bottomUnsafeAreaGuide];

    // This layout guide is used to prevent the content from being displayed
    // below the bottom toolbar.
    UILayoutGuide* bottomToolbarMarginGuide = [[UILayoutGuide alloc] init];
    [_containerView addLayoutGuide:bottomToolbarMarginGuide];

    _bottomToolbarMarginHeight =
        [bottomToolbarMarginGuide.heightAnchor constraintEqualToConstant:0];
    // Updates the constraints to the correct value.
    [self updateToolbarMargins];

    [self addSubview:_containerView];

    [NSLayoutConstraint activateConstraints:@[
      // Position the stack view's top at some margin under from the container
      // top.
      [topGuide.topAnchor constraintEqualToAnchor:_containerView.topAnchor],
      [_stackView.topAnchor constraintEqualToAnchor:topGuide.bottomAnchor
                                           constant:kLayoutGuideVerticalMargin],

      // Position the stack view's bottom guide at some margin from the
      // container bottom.
      [bottomGuide.topAnchor
          constraintEqualToAnchor:bottomToolbarMarginGuide.bottomAnchor
                         constant:kLayoutGuideVerticalMargin],
      [_containerView.bottomAnchor
          constraintEqualToAnchor:bottomGuide.bottomAnchor],

      // Position the stack view above the bottom toolbar margin guide.
      [bottomToolbarMarginGuide.topAnchor
          constraintEqualToAnchor:_stackView.bottomAnchor],

      // Center the stackview horizontally with a minimum margin.
      [_stackView.leadingAnchor
          constraintGreaterThanOrEqualToAnchor:_containerView.leadingAnchor
                                      constant:stackViewHorizontalMargin],
      [_stackView.trailingAnchor
          constraintLessThanOrEqualToAnchor:_containerView.trailingAnchor
                                   constant:-stackViewHorizontalMargin],
      [_stackView.centerXAnchor
          constraintEqualToAnchor:_containerView.centerXAnchor],

      // Constraint the _bottomUnsafeAreaGuide to the stack view and the
      // container view. Its height is set in the -didMoveToSuperview to take
      // into account the unsafe area.
      [_bottomUnsafeAreaGuide.topAnchor
          constraintEqualToAnchor:_stackView.bottomAnchor
                         constant:2 * kLayoutGuideMinHeight +
                                  kLayoutGuideVerticalMargin],
      [_bottomUnsafeAreaGuide.bottomAnchor
          constraintEqualToAnchor:_containerView.bottomAnchor],

      // Ensure that the stackview width is constrained.
      [_stackView.widthAnchor
          constraintLessThanOrEqualToConstant:stackViewMaxWidth],

      // Activate the height constraints.
      _bottomToolbarMarginHeight,

      // Set a minimum top margin and make the bottom guide twice as tall as the
      // top guide.
      [topGuide.heightAnchor
          constraintGreaterThanOrEqualToConstant:kLayoutGuideMinHeight],
      [bottomGuide.heightAnchor constraintEqualToAnchor:topGuide.heightAnchor
                                             multiplier:2.0],
    ]];

    // Constraints comunicating the size of the contentView to the scrollview.
    // See UIScrollView autolayout information at
    // https://developer.apple.com/library/ios/releasenotes/General/RN-iOSSDK-6_0/index.html
    NSDictionary* viewsDictionary = @{@"containerView" : _containerView};
    NSArray* constraints = @[
      @"V:|-0-[containerView]-0-|",
      @"H:|-0-[containerView]-0-|",
    ];
    ApplyVisualConstraints(constraints, viewsDictionary);
  }
  return self;
}

#pragma mark - UIView overrides

- (void)didMoveToSuperview {
  [super didMoveToSuperview];
  if (!self.superview) {
    return;
  }

  id<LayoutGuideProvider> safeAreaGuide = self.superview.safeAreaLayoutGuide;
  _bottomUnsafeAreaGuideInSuperview = [[UILayoutGuide alloc] init];
  [self.superview addLayoutGuide:_bottomUnsafeAreaGuideInSuperview];

  _superViewConstraints = @[
    [safeAreaGuide.bottomAnchor
        constraintEqualToAnchor:_bottomUnsafeAreaGuideInSuperview.topAnchor],
    [self.superview.bottomAnchor
        constraintEqualToAnchor:_bottomUnsafeAreaGuideInSuperview.bottomAnchor],
    [_bottomUnsafeAreaGuide.heightAnchor
        constraintGreaterThanOrEqualToAnchor:_bottomUnsafeAreaGuideInSuperview
                                                 .heightAnchor],
    [_containerView.widthAnchor
        constraintEqualToAnchor:self.superview.widthAnchor],
    [_containerView.heightAnchor
        constraintGreaterThanOrEqualToAnchor:self.superview.heightAnchor],
  ];

  [NSLayoutConstraint activateConstraints:_superViewConstraints];
}

- (void)willMoveToSuperview:(UIView*)newSuperview {
  [NSLayoutConstraint deactivateConstraints:_superViewConstraints];
  [self.superview removeLayoutGuide:_bottomUnsafeAreaGuideInSuperview];
  [super willMoveToSuperview:newSuperview];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  [self updateToolbarMargins];
}

- (void)safeAreaInsetsDidChange {
  [super safeAreaInsetsDidChange];
  [self updateToolbarMargins];
}

#pragma mark - Notifications

- (void)contentSizeCategoryDidChange {
  UIColor* bodyTextColor = BodyTextColor();

  // Recompute the text for `_notSavedLabel` and `_visibleDataLabel`, as these
  // two include font information in their attributedText.
  _notSavedLabel.attributedText = FormatHTMLListForUILabel(
      l10n_util::GetNSString(IDS_NEW_TAB_OTR_NOT_SAVED));
  _notSavedLabel.textColor = bodyTextColor;
  _visibleDataLabel.attributedText =
      FormatHTMLListForUILabel(l10n_util::GetNSString(IDS_NEW_TAB_OTR_VISIBLE));
  _visibleDataLabel.textColor = bodyTextColor;
}

#pragma mark - URLDropDelegate

- (BOOL)canHandleURLDropInView:(UIView*)view {
  return YES;
}

- (void)view:(UIView*)view didDropURL:(const GURL&)URL atPoint:(CGPoint)point {
  [self.URLLoaderDelegate loadURLInTab:URL];
}

#pragma mark - Private

// Updates the height of the margins for the top and bottom toolbars.
- (void)updateToolbarMargins {
  if (IsSplitToolbarMode(self)) {
    _bottomToolbarMarginHeight.constant = kSecondaryToolbarWithoutOmniboxHeight;
  } else {
    _bottomToolbarMarginHeight.constant = 0;
  }
}

// Triggers a navigation to the help page.
- (void)learnMoreButtonPressed {
  [self.URLLoaderDelegate loadURLInTab:GetLearnMoreIncognitoUrl()];
}

// Adds views containing the text of the incognito page to `_stackView`.
- (void)addTextSectionsWithTitleShown:(BOOL)showTitle {
  UIColor* bodyTextColor = BodyTextColor();
  UIColor* linkTextColor = [UIColor colorNamed:kBlueColor];

  // Title.
  if (showTitle) {
    UIColor* titleTextColor = [UIColor colorNamed:kTextPrimaryColor];
    UILabel* titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    titleLabel.font = TitleFont();
    titleLabel.textColor = titleTextColor;
    titleLabel.numberOfLines = 0;
    titleLabel.textAlignment = NSTextAlignmentCenter;
    titleLabel.text = l10n_util::GetNSString(IDS_NEW_TAB_OTR_TITLE);
    titleLabel.adjustsFontForContentSizeCategory = YES;
    [_stackView addArrangedSubview:titleLabel];
  }

  // The Subtitle and Learn More link have no vertical spacing between them,
  // so they are embedded in a separate stack view.
  UILabel* subtitleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  subtitleLabel.font = BodyFont();
  subtitleLabel.textColor = bodyTextColor;
  subtitleLabel.numberOfLines = 0;
  subtitleLabel.text =
      l10n_util::GetNSString(IDS_NEW_TAB_OTR_SUBTITLE_WITH_READING_LIST);
  subtitleLabel.adjustsFontForContentSizeCategory = YES;

  UIButton* learnMoreButton = [UIButton buttonWithType:UIButtonTypeCustom];
  learnMoreButton.accessibilityTraits = UIAccessibilityTraitLink;
  learnMoreButton.accessibilityHint =
      l10n_util::GetNSString(IDS_IOS_INCOGNITO_INTERSTITIAL_LEARN_MORE_HINT);
  [learnMoreButton
      setTitle:l10n_util::GetNSString(IDS_NEW_TAB_OTR_LEARN_MORE_LINK)
      forState:UIControlStateNormal];
  [learnMoreButton setTitleColor:linkTextColor forState:UIControlStateNormal];
  learnMoreButton.titleLabel.font = BodyFont();
  learnMoreButton.titleLabel.adjustsFontForContentSizeCategory = YES;
  [learnMoreButton addTarget:self
                      action:@selector(learnMoreButtonPressed)
            forControlEvents:UIControlEventTouchUpInside];
  // TODO(crbug.com/40128314): Style as a link rather than a button.
  learnMoreButton.pointerInteractionEnabled = YES;

  UIStackView* subtitleStackView = [[UIStackView alloc]
      initWithArrangedSubviews:@[ subtitleLabel, learnMoreButton ]];
  subtitleStackView.axis = UILayoutConstraintAxisVertical;
  subtitleStackView.spacing = 0;
  subtitleStackView.distribution = UIStackViewDistributionFill;
  subtitleStackView.alignment = UIStackViewAlignmentLeading;
  [_stackView addArrangedSubview:subtitleStackView];

  // Text explaining what data that is not saved. This label uses an attributed
  // string, so it must be manually adjusted when Dynamic Type settings are
  // changed.
  NSAttributedString* notSavedText = FormatHTMLListForUILabel(
      l10n_util::GetNSString(IDS_NEW_TAB_OTR_NOT_SAVED));
  _notSavedLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  _notSavedLabel.numberOfLines = 0;
  _notSavedLabel.adjustsFontForContentSizeCategory = NO;
  _notSavedLabel.attributedText = notSavedText;
  _notSavedLabel.textColor = bodyTextColor;
  [_stackView addArrangedSubview:_notSavedLabel];

  // Text explaining what data might still be visible. This label uses an
  // attributed string, so it must be manually adjusted when Dynamic Type
  // settings are changed.
  NSAttributedString* visibleDataText =
      FormatHTMLListForUILabel(l10n_util::GetNSString(IDS_NEW_TAB_OTR_VISIBLE));
  _visibleDataLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  _visibleDataLabel.numberOfLines = 0;
  _visibleDataLabel.adjustsFontForContentSizeCategory = NO;
  _visibleDataLabel.attributedText = visibleDataText;
  _visibleDataLabel.textColor = bodyTextColor;
  [_stackView addArrangedSubview:_visibleDataLabel];

  // `_notSavedLabel` and `visibleDataLabel` should have the same width as
  // `subtitleStackView`, even if they can be constrained narrower.
  [NSLayoutConstraint activateConstraints:@[
    [_notSavedLabel.widthAnchor
        constraintEqualToAnchor:subtitleStackView.widthAnchor],
    [_visibleDataLabel.widthAnchor
        constraintEqualToAnchor:subtitleStackView.widthAnchor],
  ]];

  // Register for notifications when the Dynamic Type setting is changed.
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(contentSizeCategoryDidChange)
             name:UIContentSizeCategoryDidChangeNotification
           object:nil];
}

@end