-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Hiding dashboard cards: add context menus #20384
Changes from 7 commits
c11b579
1d2c7fb
60b88d8
c72dc43
6cd31fd
9d2cd35
07bbcfd
0389d8c
f2715d9
8c96c77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,16 +25,6 @@ class BlogDashboardCardFrameView: UIView { | |
return topStackView | ||
}() | ||
|
||
/// Card's icon image view | ||
private lazy var iconImageView: UIImageView = { | ||
let iconImageView = UIImageView(image: UIImage.gridicon(.posts, size: Constants.iconSize).withRenderingMode(.alwaysTemplate)) | ||
iconImageView.tintColor = .label | ||
iconImageView.frame = CGRect(x: 0, y: 0, width: Constants.iconSize.width, height: Constants.iconSize.height) | ||
iconImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) | ||
iconImageView.isAccessibilityElement = false | ||
return iconImageView | ||
}() | ||
|
||
/// Card's title | ||
private lazy var titleLabel: UILabel = { | ||
let titleLabel = UILabel() | ||
|
@@ -45,19 +35,6 @@ class BlogDashboardCardFrameView: UIView { | |
return titleLabel | ||
}() | ||
|
||
/// Chevron displayed in case there's any action associated | ||
private lazy var chevronImageView: UIImageView = { | ||
let chevronImage = UIImage.gridicon(.chevronRight, size: Constants.iconSize).withRenderingMode(.alwaysTemplate) | ||
let chevronImageView = UIImageView(image: chevronImage.imageFlippedForRightToLeftLayoutDirection()) | ||
chevronImageView.frame = CGRect(x: 0, y: 0, width: Constants.iconSize.width, height: Constants.iconSize.height) | ||
chevronImageView.tintColor = .listIcon | ||
chevronImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) | ||
chevronImageView.isAccessibilityElement = false | ||
chevronImageView.isHidden = true | ||
chevronImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) | ||
return chevronImageView | ||
}() | ||
|
||
/// Ellipsis Button displayed on the top right corner of the view. | ||
/// Displayed only when an associated action is set | ||
private(set) lazy var ellipsisButton: UIButton = { | ||
|
@@ -102,19 +79,10 @@ class BlogDashboardCardFrameView: UIView { | |
} | ||
} | ||
|
||
/// The icon to be displayed at the header | ||
var icon: UIImage? { | ||
didSet { | ||
iconImageView.image = icon?.withRenderingMode(.alwaysTemplate) | ||
iconImageView.isHidden = icon == nil | ||
} | ||
} | ||
|
||
/// Closure to be called when anywhere in the view is tapped. | ||
/// If set, the chevron image is displayed. | ||
var onViewTap: (() -> Void)? { | ||
didSet { | ||
updateChevronImageState() | ||
addViewTapGestureIfNeeded() | ||
} | ||
} | ||
|
@@ -124,10 +92,8 @@ class BlogDashboardCardFrameView: UIView { | |
/// If set, the chevron image is displayed. | ||
var onHeaderTap: (() -> Void)? { | ||
didSet { | ||
updateChevronImageState() | ||
addHeaderTapGestureIfNeeded() | ||
} | ||
|
||
} | ||
|
||
/// Closure to be called when the ellipsis button is tapped.. | ||
|
@@ -177,7 +143,7 @@ class BlogDashboardCardFrameView: UIView { | |
headerStackView.isHidden = true | ||
buttonContainerStackView.isHidden = false | ||
|
||
if !ellipsisButton.isHidden || !chevronImageView.isHidden { | ||
if !ellipsisButton.isHidden { | ||
mainStackViewTrailingConstraint?.constant = -Constants.mainStackViewTrailingPadding | ||
} | ||
} | ||
|
@@ -228,30 +194,17 @@ class BlogDashboardCardFrameView: UIView { | |
ellipsisButton.setImage(UIImage.gridicon(.ellipsis).imageWithTintColor(.listIcon), for: .normal) | ||
} | ||
|
||
private func updateChevronImageState() { | ||
chevronImageView.isHidden = onViewTap == nil && onHeaderTap == nil | ||
assertOnTapRecognitionCorrectUsage() | ||
} | ||
|
||
private func updateEllipsisButtonState() { | ||
ellipsisButton.isHidden = onEllipsisButtonTap == nil | ||
let headerPadding = ellipsisButton.isHidden ? | ||
Constants.headerPaddingWithEllipsisButtonHidden : | ||
Constants.headerPaddingWithEllipsisButtonShown | ||
headerStackView.layoutMargins = headerPadding | ||
assertOnTapRecognitionCorrectUsage() | ||
} | ||
|
||
/// Only one of two types of action should be associated with the card. | ||
/// Either ellipsis button tap, or view/header tap | ||
private func assertOnTapRecognitionCorrectUsage() { | ||
let bothTypesUsed = (onViewTap != nil || onHeaderTap != nil) && onEllipsisButtonTap != nil | ||
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. These code removals from |
||
assert(!bothTypesUsed, "Using onViewTap or onHeaderTap alongside onEllipsisButtonTap is not supported and will result in unexpected behavior.") | ||
} | ||
|
||
private func addHeaderTapGestureIfNeeded() { | ||
// Reset any previously added gesture recognizers | ||
headerStackView.gestureRecognizers?.forEach {headerStackView.removeGestureRecognizer($0)} | ||
headerStackView.gestureRecognizers?.forEach { headerStackView.removeGestureRecognizer($0) } | ||
|
||
// Add gesture recognizer if needed | ||
if onHeaderTap != nil { | ||
|
@@ -262,7 +215,7 @@ class BlogDashboardCardFrameView: UIView { | |
|
||
private func addViewTapGestureIfNeeded() { | ||
// Reset any previously added gesture recognizers | ||
self.gestureRecognizers?.forEach {self.removeGestureRecognizer($0)} | ||
self.gestureRecognizers?.forEach { self.removeGestureRecognizer($0) } | ||
|
||
// Add gesture recognizer if needed | ||
if onViewTap != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,7 @@ class DashboardPostsListCardCell: UICollectionViewCell, Reusable { | |
|
||
// MARK: Views | ||
|
||
private var frameView: BlogDashboardCardFrameView? | ||
private let frameView = BlogDashboardCardFrameView() | ||
|
||
lazy var tableView: UITableView = { | ||
let tableView = PostCardTableView() | ||
|
@@ -68,14 +68,9 @@ class DashboardPostsListCardCell: UICollectionViewCell, Reusable { | |
} | ||
|
||
private func addSubviews() { | ||
let frameView = BlogDashboardCardFrameView() | ||
frameView.icon = UIImage.gridicon(.posts, size: Constants.iconSize) | ||
frameView.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
frameView.add(subview: tableView) | ||
|
||
self.frameView = frameView | ||
|
||
contentView.addSubview(frameView) | ||
contentView.pinSubviewToAllEdges(frameView, priority: Constants.constraintPriority) | ||
} | ||
|
@@ -104,23 +99,37 @@ extension DashboardPostsListCardCell { | |
assertionFailure("Cell used with wrong card type") | ||
return | ||
} | ||
addContextMenu(card: cardType, blog: blog) | ||
|
||
viewModel = PostsCardViewModel(blog: blog, status: status, view: self) | ||
viewModel?.viewDidLoad() | ||
tableView.dataSource = viewModel?.diffableDataSource | ||
viewModel?.refresh() | ||
} | ||
|
||
private func addContextMenu(card: DashboardCard, blog: Blog) { | ||
guard FeatureFlag.personalizeHomeTab.enabled else { return } | ||
|
||
frameView.onEllipsisButtonTap = { | ||
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. I'm curious about your thoughts.
I'm looking at each of the cells which repeat the same pattern of adding ellipsis button handling, hiding action, and tracking code. Although duplication is not necessarily evil, I'm wondering if in this situation it makes and is possible to have something reused. To describe the card as "personalizable" and then it will share common these functionality and layout traits. What do you think about that?
Is this a counter-argument to my question and it's difficult to reuse the same layout and functionality code between personalizable cards? 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. Thanks for bringing this up. I did add some reuse but didn't want to go as far as refactoring the existing cards:
But there is clearly some duplication in how the context menus get added: if FeatureFlag.personalizeHomeTab.enabled {
frameView.onEllipsisButtonTap = {
BlogDashboardAnalytics.trackContextualMenuAccessed(for: .todaysStats)
}
frameView.ellipsisButton.showsMenuAsPrimaryAction = true
frameView.ellipsisButton.menu = UIMenu(title: "", options: .displayInline, children: [
BlogDashboardHelpers.makeHideCardAction(for: .todaysStats, siteID: blog.dotComID?.intValue ?? 0)
])
} I could make it reusable fairly easily: extension DashboardStatsCardCell {
private func configureCard(for blog: Blog, in viewController: UIViewController) {
if FeatureFlag.personalizeHomeTab.enabled {
let menu = UIMenu(title: "", options: .displayInline, children: [
BlogDashboardHelpers.makeHideCardAction(for: .todaysStats, siteID: blog.dotComID?.intValue ?? 0)
])
frameView.setContextMenu(menu, card: .todaysStats)
}
// ...
}
}
extension BlogDashboardCardFrameView {
func setContextMenu(_ menu: UIMenu, card: DashboardCard) {
onEllipsisButtonTap = {
BlogDashboardAnalytics.trackContextualMenuAccessed(for: card)
}
ellipsisButton.showsMenuAsPrimaryAction = true
ellipsisButton.menu = menu
}
} This way, we'll remove the most obvious instances of duplication, and cards will retain complete control over how their menus are configured. Should I make this change in this PR? I have some other ideas for refactoring this and also Ultimately, I'll want this "Hide this" action to appear automatically based on which cards are personalizable, but that sounds a bit overcomplicated. I do want to look into it a bit more. 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. I also already used the same approach for adding the ellipsis in #20393, which is consistent with other cards. I think it'll make sense to update them all at once to make it easy to review/test. 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. Thanks for the insightful answer and your examples! Those
I think we could do that in other PRs as well. There are already a fair amount of changes, it will be easier to digest additional ones in a separate PR. |
||
BlogDashboardAnalytics.trackContextualMenuAccessed(for: card) | ||
} | ||
frameView.ellipsisButton.showsMenuAsPrimaryAction = true | ||
frameView.ellipsisButton.menu = UIMenu(title: "", options: .displayInline, children: [ | ||
BlogDashboardHelpers.makeHideCardAction(for: card, siteID: blog.dotComID?.intValue ?? 0) | ||
]) | ||
} | ||
|
||
private func configureDraftsList(blog: Blog) { | ||
frameView?.title = Strings.draftsTitle | ||
frameView?.titleHint = Strings.draftsTitleHint | ||
frameView?.onHeaderTap = { [weak self] in | ||
frameView.title = Strings.draftsTitle | ||
frameView.titleHint = Strings.draftsTitleHint | ||
frameView.onHeaderTap = { [weak self] in | ||
self?.presentPostList(with: .draft) | ||
} | ||
} | ||
|
||
private func configureScheduledList(blog: Blog) { | ||
frameView?.title = Strings.scheduledTitle | ||
frameView?.onHeaderTap = { [weak self] in | ||
frameView.title = Strings.scheduledTitle | ||
frameView.onHeaderTap = { [weak self] in | ||
self?.presentPostList(with: .scheduled) | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import Foundation | ||
|
||
struct BlogDashboardHelpers { | ||
static func makeHideCardAction(for card: DashboardCard, siteID: Int) -> UIAction { | ||
UIAction( | ||
title: Strings.hideThis, | ||
image: UIImage(systemName: "minus.circle"), | ||
attributes: [.destructive], | ||
handler: { _ in | ||
BlogDashboardAnalytics.trackHideTapped(for: card) | ||
BlogDashboardPersonalizationService(siteID: siteID) | ||
.setEnabled(false, for: card) | ||
}) | ||
} | ||
|
||
private enum Strings { | ||
static let hideThis = NSLocalizedString("blogDashboard.contextMenu.hideThis", value: "Hide this", comment: "Title for the context menu action that hides the dashboard card.") | ||
} | ||
} |
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.
The cut-off for 22.1 is on Monday and the feature is still under the feature flag and development.
I think it's safer to move these notes to 22.2 and aim to enable those changes then.
Does that sound good?
CC: @guarani