-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add FXIOS-15840 [Ad Blocker] Add ad blocker badge to menu #33889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
0294dac
chore: add strings
issammani 873e762
chore: add feature flag
issammani 4b677ac
feat: add toggle to settings
issammani 891edee
feat: add tests
issammani 3ef2606
chore: add icons
issammani c1d843a
chore: add badge telemetry action
issammani 77a3f98
fix: refactor badge into reusable component
issammani f3820bc
feat: ad block navigation
issammani e7fe9dc
feat: add tests
issammani 82140ce
fix: add image assets
issammani 0d2ca16
fix: add present method to coordinator
issammani c7f33c4
feat: setup custom detents
issammani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, You can obtain one at http://mozilla.org/MPL/2.0/ | ||
|
|
||
| import UIKit | ||
| import Common | ||
|
|
||
| final class MenuSiteBadge: UIView, ThemeApplicable { | ||
| private struct UX { | ||
| static let cornerRadius: CGFloat = 12 | ||
| static let borderWidth: CGFloat = 1 | ||
| static let horizontalPadding: CGFloat = 10 | ||
| static let verticalPadding: CGFloat = 6 | ||
| static let contentSpacing: CGFloat = 4 | ||
| static let iconSize: CGFloat = 16 | ||
| static let chevronSize: CGFloat = 20 | ||
| } | ||
|
|
||
| var tapHandler: (() -> Void)? | ||
| private let mainMenuHelper: MainMenuInterface | ||
|
|
||
| private lazy var stack: UIStackView = .build { [weak self] stack in | ||
| guard let self else { return } | ||
| stack.isLayoutMarginsRelativeArrangement = true | ||
| stack.layoutMargins = UIEdgeInsets(top: UX.verticalPadding, | ||
| left: UX.horizontalPadding, | ||
| bottom: UX.verticalPadding, | ||
| right: UX.horizontalPadding) | ||
| stack.distribution = .fill | ||
| stack.axis = .horizontal | ||
| stack.clipsToBounds = true | ||
| stack.spacing = UX.contentSpacing | ||
| let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapped)) | ||
| stack.isUserInteractionEnabled = true | ||
| stack.addGestureRecognizer(tapGesture) | ||
| } | ||
|
|
||
| private var label: UILabel = .build { label in | ||
| label.font = FXFontStyles.Regular.footnote.scaledFont() | ||
| label.numberOfLines = 0 | ||
| label.lineBreakMode = .byWordWrapping | ||
| label.adjustsFontForContentSizeCategory = true | ||
| label.accessibilityTraits = .button | ||
| } | ||
|
|
||
| private var icon: UIImageView = .build { imageView in | ||
| imageView.contentMode = .scaleAspectFit | ||
| } | ||
|
|
||
| private var chevron: UIImageView = .build { imageView in | ||
| let imageName = StandardImageIdentifiers.Large.chevronRight | ||
| let image = UIImage(named: imageName)? | ||
| .withRenderingMode(.alwaysTemplate) | ||
| .imageFlippedForRightToLeftLayoutDirection() ?? UIImage() | ||
| imageView.image = image | ||
| imageView.contentMode = .scaleAspectFit | ||
| } | ||
|
|
||
| init(mainMenuHelper: MainMenuInterface) { | ||
| self.mainMenuHelper = mainMenuHelper | ||
| super.init(frame: .zero) | ||
| setupViews() | ||
| } | ||
|
|
||
| required init?(coder: NSCoder) { | ||
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| override func layoutSubviews() { | ||
| super.layoutSubviews() | ||
| if #available(iOS 26.0, *) { | ||
| stack.layer.cornerRadius = stack.frame.height / 2 | ||
| } else { | ||
| stack.layer.cornerRadius = UX.cornerRadius | ||
| stack.layer.borderWidth = UX.borderWidth | ||
| } | ||
| } | ||
|
|
||
| private func setupViews() { | ||
| addSubview(stack) | ||
| stack.addArrangedSubview(icon) | ||
| stack.addArrangedSubview(label) | ||
| stack.addArrangedSubview(chevron) | ||
|
|
||
| NSLayoutConstraint.activate([ | ||
| stack.topAnchor.constraint(equalTo: topAnchor), | ||
| stack.bottomAnchor.constraint(equalTo: bottomAnchor), | ||
| stack.leadingAnchor.constraint(equalTo: leadingAnchor), | ||
| stack.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
|
|
||
| icon.widthAnchor.constraint(equalToConstant: UX.iconSize), | ||
| chevron.widthAnchor.constraint(equalToConstant: UX.chevronSize) | ||
| ]) | ||
| } | ||
|
|
||
| func configure(text: String, iconName: String, useTemplate: Bool) { | ||
| label.text = text | ||
| let image: UIImage = useTemplate | ||
| ? UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate) ?? UIImage() | ||
| : UIImage(named: iconName) ?? UIImage() | ||
| icon.image = image | ||
| } | ||
|
|
||
| func applyTheme(theme: Theme) { | ||
| label.textColor = theme.colors.textSecondary | ||
| stack.layer.borderColor = theme.colors.actionSecondaryHover.cgColor | ||
| if #available(iOS 26.0, *) { | ||
| stack.backgroundColor = theme.colors.layerSurfaceMedium | ||
| .withAlphaComponent(mainMenuHelper.backgroundAlpha()) | ||
| } else { | ||
| stack.backgroundColor = .clear | ||
| } | ||
| icon.tintColor = theme.colors.iconSecondary | ||
| chevron.tintColor = theme.colors.iconSecondary | ||
| } | ||
|
|
||
| @objc | ||
| private func tapped() { | ||
| tapHandler?() | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,24 +7,31 @@ import Common | |
| import SiteImageView | ||
| import ComponentLibrary | ||
|
|
||
| public struct MenuSiteAdBlockerBadgeData { | ||
| public let title: String | ||
| public let image: String | ||
| public let shouldUseRenderMode: Bool | ||
|
|
||
| public init(title: String, image: String, shouldUseRenderMode: Bool) { | ||
| self.title = title | ||
| self.image = image | ||
| self.shouldUseRenderMode = shouldUseRenderMode | ||
| } | ||
| } | ||
|
|
||
| public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable { | ||
| private struct UX { | ||
| static let closeButtonSize: CGFloat = 30 | ||
| static let contentLabelsSpacing: CGFloat = 1 | ||
| static let horizontalContentMargin: CGFloat = 16 | ||
| static let favIconSize: CGFloat = 40 | ||
| static let siteProtectionsContentTopMargin: CGFloat = 4 | ||
| static let siteProtectionsContentCornerRadius: CGFloat = 12 | ||
| static let siteProtectionsContentBorderWidth: CGFloat = 1 | ||
| static let siteProtectionsContentHorizontalPadding: CGFloat = 10 | ||
| static let siteProtectionsContentVerticalPadding: CGFloat = 6 | ||
| static let siteProtectionsIcon: CGFloat = 16 | ||
| static let siteProtectionsMoreSettingsIcon: CGFloat = 20 | ||
| static let siteProtectionsContentSpacing: CGFloat = 4 | ||
| static let badgesTopMargin: CGFloat = 4 | ||
| static let badgesSpacing: CGFloat = 8 | ||
| } | ||
|
|
||
| public var closeButtonCallback: (() -> Void)? | ||
| public var siteProtectionsButtonCallback: (() -> Void)? | ||
| public var adBlockerButtonCallback: (() -> Void)? | ||
| public var mainMenuHelper: MainMenuInterface = MainMenuHelper() | ||
|
|
||
| private var contentLabels: UIStackView = .build { stack in | ||
|
|
@@ -57,42 +64,26 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable { | |
| button.setImage(UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate) ?? UIImage(), for: .normal) | ||
| } | ||
|
|
||
| private lazy var siteProtectionsContent: UIStackView = .build { [weak self] stack in | ||
| guard let self else { return } | ||
| stack.isLayoutMarginsRelativeArrangement = true | ||
| stack.layoutMargins = UIEdgeInsets(top: UX.siteProtectionsContentVerticalPadding, | ||
| left: UX.siteProtectionsContentHorizontalPadding, | ||
| bottom: UX.siteProtectionsContentVerticalPadding, | ||
| right: UX.siteProtectionsContentHorizontalPadding) | ||
| stack.distribution = .fill | ||
| private lazy var badgesStack: UIStackView = .build { stack in | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| stack.axis = .horizontal | ||
| stack.clipsToBounds = true | ||
| stack.spacing = UX.siteProtectionsContentSpacing | ||
| let tapGesture = UITapGestureRecognizer(target: self, action: #selector(siteProtectionsTapped)) | ||
| stack.isUserInteractionEnabled = true | ||
| stack.addGestureRecognizer(tapGesture) | ||
| } | ||
|
|
||
| private var siteProtectionsLabel: UILabel = .build { label in | ||
| label.font = FXFontStyles.Regular.footnote.scaledFont() | ||
| label.numberOfLines = 0 | ||
| label.lineBreakMode = .byWordWrapping | ||
| label.adjustsFontForContentSizeCategory = true | ||
| label.accessibilityTraits = .button | ||
| stack.alignment = .center | ||
| stack.distribution = .fill | ||
| stack.spacing = UX.badgesSpacing | ||
| } | ||
|
|
||
| private var siteProtectionsIcon: UIImageView = .build { imageView in | ||
| imageView.contentMode = .scaleAspectFit | ||
| } | ||
| private lazy var siteProtectionsBadge: MenuSiteBadge = { | ||
| let badge = MenuSiteBadge(mainMenuHelper: mainMenuHelper) | ||
| badge.translatesAutoresizingMaskIntoConstraints = false | ||
| badge.tapHandler = { [weak self] in self?.siteProtectionsButtonCallback?() } | ||
| return badge | ||
| }() | ||
|
|
||
| private var siteProtectionsMoreSettingsIcon: UIImageView = .build { imageView in | ||
| let imageName = StandardImageIdentifiers.Large.chevronRight | ||
| let image = UIImage(named: imageName)? | ||
| .withRenderingMode(.alwaysTemplate) | ||
| .imageFlippedForRightToLeftLayoutDirection() ?? UIImage() | ||
| imageView.image = image | ||
| imageView.contentMode = .scaleAspectFit | ||
| } | ||
| private lazy var adBlockerBadge: MenuSiteBadge = { | ||
| let badge = MenuSiteBadge(mainMenuHelper: mainMenuHelper) | ||
| badge.translatesAutoresizingMaskIntoConstraints = false | ||
| badge.tapHandler = { [weak self] in self?.adBlockerButtonCallback?() } | ||
| return badge | ||
| }() | ||
|
|
||
| init() { | ||
| super.init(frame: .zero) | ||
|
|
@@ -103,34 +94,22 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable { | |
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| override public func layoutSubviews() { | ||
| super.layoutSubviews() | ||
| if #available(iOS 26.0, *) { | ||
| siteProtectionsContent.layer.cornerRadius = siteProtectionsContent.frame.height / 2 | ||
| } else { | ||
| siteProtectionsContent.layer.cornerRadius = UX.siteProtectionsContentCornerRadius | ||
| siteProtectionsContent.layer.borderWidth = UX.siteProtectionsContentBorderWidth | ||
| } | ||
| } | ||
|
|
||
| private func setupViews() { | ||
| contentLabels.addArrangedSubview(titleLabel) | ||
| contentLabels.addArrangedSubview(subtitleLabel) | ||
| addSubviews(contentLabels, favicon, closeButton, siteProtectionsContent) | ||
| siteProtectionsContent.addArrangedSubview(siteProtectionsIcon) | ||
| siteProtectionsContent.addArrangedSubview(siteProtectionsLabel) | ||
| siteProtectionsContent.addArrangedSubview(siteProtectionsMoreSettingsIcon) | ||
| addSubviews(contentLabels, favicon, closeButton, badgesStack) | ||
| badgesStack.addArrangedSubview(siteProtectionsBadge) | ||
|
|
||
| let siteProtectionsTopFromFavicon = siteProtectionsContent.topAnchor.constraint( | ||
| let badgesTopFromFavicon = badgesStack.topAnchor.constraint( | ||
| greaterThanOrEqualTo: favicon.bottomAnchor, | ||
| constant: UX.siteProtectionsContentTopMargin | ||
| constant: UX.badgesTopMargin | ||
| ) | ||
|
|
||
| let siteProtectionsTopFromLabels = siteProtectionsContent.topAnchor.constraint( | ||
| let badgesTopFromLabels = badgesStack.topAnchor.constraint( | ||
| equalTo: contentLabels.bottomAnchor, | ||
| constant: UX.siteProtectionsContentTopMargin | ||
| constant: UX.badgesTopMargin | ||
| ) | ||
| siteProtectionsTopFromLabels.priority = .defaultHigh | ||
| badgesTopFromLabels.priority = .defaultHigh | ||
| NSLayoutConstraint.activate([ | ||
| contentLabels.topAnchor.constraint(equalTo: self.topAnchor), | ||
| contentLabels.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, | ||
|
|
@@ -145,17 +124,14 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable { | |
| closeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -UX.horizontalContentMargin), | ||
| closeButton.topAnchor.constraint(equalTo: self.topAnchor), | ||
|
|
||
| siteProtectionsIcon.widthAnchor.constraint(equalToConstant: UX.siteProtectionsIcon), | ||
| siteProtectionsMoreSettingsIcon.widthAnchor.constraint(equalToConstant: UX.siteProtectionsMoreSettingsIcon), | ||
|
|
||
| siteProtectionsTopFromLabels, | ||
| siteProtectionsTopFromFavicon, | ||
| siteProtectionsContent.trailingAnchor.constraint(lessThanOrEqualTo: closeButton.leadingAnchor), | ||
| siteProtectionsContent.bottomAnchor.constraint(equalTo: self.bottomAnchor), | ||
| badgesTopFromLabels, | ||
| badgesTopFromFavicon, | ||
| badgesStack.leadingAnchor.constraint(equalTo: favicon.leadingAnchor), | ||
| badgesStack.trailingAnchor.constraint(lessThanOrEqualTo: closeButton.leadingAnchor), | ||
| badgesStack.bottomAnchor.constraint(equalTo: self.bottomAnchor), | ||
|
|
||
| closeButton.widthAnchor.constraint(equalToConstant: UX.closeButtonSize), | ||
| closeButton.heightAnchor.constraint(equalToConstant: UX.closeButtonSize), | ||
| siteProtectionsContent.leadingAnchor.constraint(equalTo: favicon.leadingAnchor) | ||
| closeButton.heightAnchor.constraint(equalToConstant: UX.closeButtonSize) | ||
| ]) | ||
|
|
||
| closeButton.layer.cornerRadius = 0.5 * UX.closeButtonSize | ||
|
|
@@ -167,17 +143,24 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable { | |
| image: String?, | ||
| state: String, | ||
| stateImage: String, | ||
| shouldUseRenderMode: Bool | ||
| shouldUseRenderMode: Bool, | ||
| adBlocker: MenuSiteAdBlockerBadgeData? = nil | ||
| ) { | ||
| titleLabel.text = title | ||
| subtitleLabel.text = subtitle | ||
| siteProtectionsLabel.text = state | ||
| let siteProtectionsImage: UIImage = if shouldUseRenderMode { | ||
| UIImage(named: stateImage)?.withRenderingMode(.alwaysTemplate) ?? UIImage() | ||
| siteProtectionsBadge.configure(text: state, | ||
| iconName: stateImage, | ||
| useTemplate: shouldUseRenderMode) | ||
| if let adBlocker { | ||
| if adBlockerBadge.superview == nil { | ||
| badgesStack.addArrangedSubview(adBlockerBadge) | ||
| } | ||
| adBlockerBadge.configure(text: adBlocker.title, | ||
| iconName: adBlocker.image, | ||
| useTemplate: adBlocker.shouldUseRenderMode) | ||
| } else { | ||
| UIImage(named: stateImage) ?? UIImage() | ||
| adBlockerBadge.removeFromSuperview() | ||
| } | ||
| siteProtectionsIcon.image = siteProtectionsImage | ||
|
|
||
| let image = FaviconImageViewModel(siteURLString: image, | ||
| faviconCornerRadius: UX.favIconSize / 2) | ||
|
|
@@ -197,25 +180,12 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable { | |
| closeButtonCallback?() | ||
| } | ||
|
|
||
| @objc | ||
| func siteProtectionsTapped() { | ||
| siteProtectionsButtonCallback?() | ||
| } | ||
|
|
||
| public func applyTheme(theme: Theme) { | ||
| titleLabel.textColor = theme.colors.textPrimary | ||
| subtitleLabel.textColor = theme.colors.textSecondary | ||
| closeButton.tintColor = theme.colors.iconSecondary | ||
| closeButton.backgroundColor = theme.colors.actionCloseButton.withAlphaComponent(mainMenuHelper.backgroundAlpha()) | ||
| siteProtectionsLabel.textColor = theme.colors.textSecondary | ||
| siteProtectionsContent.layer.borderColor = theme.colors.actionSecondaryHover.cgColor | ||
| if #available(iOS 26.0, *) { | ||
| let backgroundColor = theme.colors.layerSurfaceMedium.withAlphaComponent(mainMenuHelper.backgroundAlpha()) | ||
| siteProtectionsContent.backgroundColor = backgroundColor | ||
| } else { | ||
| siteProtectionsContent.backgroundColor = .clear | ||
| } | ||
| siteProtectionsIcon.tintColor = theme.colors.iconSecondary | ||
| siteProtectionsMoreSettingsIcon.tintColor = theme.colors.iconSecondary | ||
| siteProtectionsBadge.applyTheme(theme: theme) | ||
| adBlockerBadge.applyTheme(theme: theme) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should make the entire view the a11y element here instead of the label, that way during voiceover the whole view is focused.
Would remove
label.accessibilityTraits = .buttonand addlabel.isAccessibilityElement = falsethen add this to the init:
and
accessibilityLabel = textinconfigure()