Skip to content

Commit

Permalink
Hiding dashboard cards: add context menus (#20384)
Browse files Browse the repository at this point in the history
* Remove unused code from BlogDashboardCardFrameView

The chevronImage and the iconImageView were removed during the recent dashboard card redesign

* Fix localizable strings comments in Blaze card

* Fix size of the context menu buttons on Dashboard (was too small)

* Add context menu to Drafts and Scheduled posts

* Add Hide This action to Todays Stats card on Dashboard

* Update release notes

* Revert "Fix size of the context menu buttons on Dashboard (was too small)"

This reverts commit 60b88d8.

* Remove personalization from release notes for now

* Update context menu icon on dashboard cards
  • Loading branch information
kean authored Apr 3, 2023
1 parent f316bf4 commit 4e2bd45
Show file tree
Hide file tree
Showing 15 changed files with 99 additions and 83 deletions.
1 change: 0 additions & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* [**] [internal] Refactor updating account related Core Data operations, which ususally happens during log in and out of the app. [#20394]
* [***] [internal] Refactor uploading photos (from the device photo, the Free Photo library, and other sources) to the WordPress Media Library. Affected areas are where you can choose a photo and upload, including the "Media" screen, adding images to a post, updating site icon, etc. [#20322]
* [**] [WordPress-only] Warns user about sites with only individual plugins not supporting core app features and offers the option to switch to the Jetpack app. [#20408]
* [**] Add a "Personalize Home Tab" button to the bottom of the Home tab that allows changing cards visibility. [#20369]

22.0
-----
Expand Down
6 changes: 6 additions & 0 deletions WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ import Foundation
// My Site Dashboard
case dashboardCardShown
case dashboardCardItemTapped
case dashboardCardContextualMenuAccessed
case dashboardCardHideTapped
case mySiteTabTapped
case mySiteSiteMenuShown
case mySiteDashboardShown
Expand Down Expand Up @@ -1033,6 +1035,10 @@ import Foundation
return "my_site_dashboard_card_shown"
case .dashboardCardItemTapped:
return "my_site_dashboard_card_item_tapped"
case .dashboardCardContextualMenuAccessed:
return "my_site_dashboard_contextual_menu_accessed"
case .dashboardCardHideTapped:
return "my_site_dashboard_card_hide_tapped"
case .mySiteTabTapped:
return "my_site_tab_tapped"
case .mySiteSiteMenuShown:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ extension BlazeCardView {
comment: "Description for the Blaze dashboard card.")
static let hideThis = NSLocalizedString("blaze.dashboard.card.menu.hide",
value: "Hide this",
comment: "Title for a menu action in the context menu on the Jetpack install card.")
comment: "Title for a menu action in the context menu on the Blaze card.")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ class DashboardBlazeCardCell: DashboardCollectionViewCell {
}

let onEllipsisTap: () -> Void = { [weak self] in
BlogDashboardAnalytics.trackContextualMenuAccessed(for: .blaze)
BlazeEventsTracker.trackContextualMenuAccessed(for: .dashboardCard)
}

let onHideThisTap: UIActionHandler = { [weak self] _ in
BlogDashboardAnalytics.trackHideTapped(for: .blaze)
BlazeEventsTracker.trackHideThisTapped(for: .dashboardCard)
BlazeHelper.hideBlazeCard(for: self?.blog)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -46,24 +36,12 @@ 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 = {
let button = UIButton(type: .custom)
button.setImage(UIImage.gridicon(.ellipsis).imageWithTintColor(.listIcon), for: .normal)
button.setImage(UIImage(named: "more-horizontal-mobile"), for: .normal)
button.tintColor = UIColor.listIcon
button.contentEdgeInsets = Constants.ellipsisButtonPadding
button.isAccessibilityElement = true
button.accessibilityLabel = Strings.ellipsisButtonAccessibilityLabel
Expand Down Expand Up @@ -104,19 +82,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()
}
}
Expand All @@ -126,10 +95,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..
Expand Down Expand Up @@ -163,11 +130,6 @@ class BlogDashboardCardFrameView: UIView {
}
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateColors()
}

/// Add a subview inside the card frame
func add(subview: UIView) {
mainStackView.addArrangedSubview(subview)
Expand All @@ -179,7 +141,7 @@ class BlogDashboardCardFrameView: UIView {
headerStackView.isHidden = true
buttonContainerStackView.isHidden = false

if !ellipsisButton.isHidden || !chevronImageView.isHidden {
if !ellipsisButton.isHidden {
mainStackViewTrailingConstraint?.constant = -Constants.mainStackViewTrailingPadding
}
}
Expand Down Expand Up @@ -228,34 +190,17 @@ class BlogDashboardCardFrameView: UIView {
buttonContainerStackView.removeFromSuperview()
}

private func updateColors() {
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
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 {
Expand All @@ -266,7 +211,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 = {
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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
let frameView = BlogDashboardCardFrameView()
frameView.translatesAutoresizingMaskIntoConstraints = false
frameView.title = Strings.cardFrameTitle
frameView.icon = Style.frameIconImage

// NOTE: Remove the logic when support for iOS 14 is dropped
if #available (iOS 15.0, *) {
// assign an empty closure so the button appears.
frameView.onEllipsisButtonTap = {}
frameView.onEllipsisButtonTap = {
BlogDashboardAnalytics.trackContextualMenuAccessed(for: .prompts)
}
frameView.ellipsisButton.showsMenuAsPrimaryAction = true
frameView.ellipsisButton.menu = contextMenu
} else {
// Show a fallback implementation using `MenuSheetViewController`.
// iOS 13 doesn't support showing UIMenu programmatically.
// iOS 14 doesn't support `UIDeferredMenuElement.uncached`.
frameView.onEllipsisButtonTap = { [weak self] in
BlogDashboardAnalytics.trackContextualMenuAccessed(for: .prompts)
self?.showMenuSheet()
}
}
Expand Down Expand Up @@ -505,6 +507,7 @@ private extension DashboardPromptsCardCell {
return
}
WPAnalytics.track(.promptsDashboardCardMenuRemove)
BlogDashboardAnalytics.trackHideTapped(for: .prompts)
let service = BlogDashboardPersonalizationService(siteID: siteID)
service.setEnabled(false, for: .prompts)
let notice = Notice(title: Strings.promptRemovedTitle, message: Strings.promptRemovedSubtitle, feedbackType: .success, actionTitle: Strings.undoSkipTitle) { _ in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ final class DashboardQuickStartCardCell: UICollectionViewCell, Reusable, BlogDas
fallthrough

case .newSite:
cardFrameView.icon = UIImage.gridicon(.listOrdered, size: Metrics.iconSize)
configureOnEllipsisButtonTap(sourceRect: cardFrameView.ellipsisButton.frame)
cardFrameView.showHeader()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class DashboardStatsCardCell: UICollectionViewCell, Reusable {
// MARK: Private Variables

private var viewModel: DashboardStatsViewModel?
private var frameView: BlogDashboardCardFrameView?
private let frameView = BlogDashboardCardFrameView()
private var nudgeView: DashboardStatsNudgeView?
private var statsStackView: DashboardStatsStackView?

Expand Down Expand Up @@ -39,11 +39,8 @@ class DashboardStatsCardCell: UICollectionViewCell, Reusable {
}

private func addSubviews() {
let frameView = BlogDashboardCardFrameView()
frameView.title = Strings.statsTitle
frameView.titleHint = Strings.statsTitleHint
frameView.icon = UIImage.gridicon(.statsAlt, size: Constants.iconSize)
self.frameView = frameView

let statsStackview = DashboardStatsStackView()
frameView.add(subview: statsStackview)
Expand Down Expand Up @@ -74,11 +71,20 @@ extension DashboardStatsCardCell: BlogDashboardCardConfigurable {
}

private func configureCard(for blog: Blog, in viewController: UIViewController) {

frameView?.onViewTap = { [weak self] in
frameView.onViewTap = { [weak self] in
self?.showStats(for: blog, from: viewController)
}

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)
])
}

statsStackView?.views = viewModel?.todaysViews
statsStackView?.visitors = viewModel?.todaysVisitors
statsStackView?.likes = viewModel?.todaysLikes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ class BlogDashboardAnalytics {
}
}
}

static func trackContextualMenuAccessed(for card: DashboardCard) {
WPAnalytics.track(.dashboardCardContextualMenuAccessed, properties: ["card": card.rawValue])
}

static func trackHideTapped(for card: DashboardCard) {
WPAnalytics.track(.dashboardCardHideTapped, properties: ["card": card.rawValue])
}
}
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.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ class JetpackRemoteInstallCardView: UIView {
private lazy var cardFrameView: BlogDashboardCardFrameView = {
let frameView = BlogDashboardCardFrameView()
frameView.translatesAutoresizingMaskIntoConstraints = false
frameView.icon = .none
frameView.onEllipsisButtonTap = {}
frameView.ellipsisButton.showsMenuAsPrimaryAction = true
frameView.ellipsisButton.menu = contextMenu
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "more-horizontal-mobile.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading

0 comments on commit 4e2bd45

Please sign in to comment.