Skip to content

Commit 450c1e7

Browse files
Merge pull request #19782 from wordpress-mobile/task/19450-phase-3-card
Jetpack Focus: Adds Jetpack menu card
2 parents a37dd79 + 43ba84b commit 450c1e7

File tree

10 files changed

+383
-11
lines changed

10 files changed

+383
-11
lines changed

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/BlogDashboardCardFrameView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class BlogDashboardCardFrameView: UIView {
6969
button.accessibilityTraits = .button
7070
button.isHidden = true
7171
button.setContentHuggingPriority(.defaultHigh, for: .horizontal)
72-
button.on(.touchUpInside) { [weak self] _ in
72+
button.on([.touchUpInside, .menuActionTriggered]) { [weak self] _ in
7373
self?.onEllipsisButtonTap?()
7474
}
7575
return button

WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ typedef NS_ENUM(NSUInteger, BlogDetailsSectionCategory) {
2020
BlogDetailsSectionCategoryExternal,
2121
BlogDetailsSectionCategoryRemoveSite,
2222
BlogDetailsSectionCategoryMigrationSuccess,
23+
BlogDetailsSectionCategoryJetpackBrandingCard,
2324
};
2425

2526
typedef NS_ENUM(NSUInteger, BlogDetailsSubsection) {
@@ -40,6 +41,7 @@ typedef NS_ENUM(NSUInteger, BlogDetailsSubsection) {
4041
BlogDetailsSubsectionPlugins,
4142
BlogDetailsSubsectionHome,
4243
BlogDetailsSubsectionMigrationSuccess,
44+
BlogDetailsSubsectionJetpackBrandingCard,
4345
};
4446

4547

WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
static NSString *const BlogDetailsQuickStartCellIdentifier = @"BlogDetailsQuickStartCell";
3030
static NSString *const BlogDetailsSectionFooterIdentifier = @"BlogDetailsSectionFooterView";
3131
static NSString *const BlogDetailsMigrationSuccessCellIdentifier = @"BlogDetailsMigrationSuccessCell";
32+
static NSString *const BlogDetailsJetpackBrandingCardCellIdentifier = @"BlogDetailsJetpackBrandingCardCellIdentifier";
3233

3334
NSString * const WPBlogDetailsRestorationID = @"WPBlogDetailsID";
3435
NSString * const WPBlogDetailsBlogKey = @"WPBlogDetailsBlogKey";
@@ -355,6 +356,7 @@ - (void)viewDidLoad
355356
[self.tableView registerClass:[QuickStartCell class] forCellReuseIdentifier:BlogDetailsQuickStartCellIdentifier];
356357
[self.tableView registerClass:[BlogDetailsSectionFooterView class] forHeaderFooterViewReuseIdentifier:BlogDetailsSectionFooterIdentifier];
357358
[self.tableView registerClass:[MigrationSuccessCell class] forCellReuseIdentifier:BlogDetailsMigrationSuccessCellIdentifier];
359+
[self.tableView registerClass:[JetpackBrandingMenuCardCell class] forCellReuseIdentifier:BlogDetailsJetpackBrandingCardCellIdentifier];
358360

359361
self.hasLoggedDomainCreditPromptShownEvent = NO;
360362

@@ -451,6 +453,7 @@ - (void)showDetailViewForSubsection:(BlogDetailsSubsection)section
451453
[self showDashboard];
452454
break;
453455
case BlogDetailsSubsectionQuickStart:
456+
case BlogDetailsSubsectionJetpackBrandingCard:
454457
self.restorableSelectedIndexPath = indexPath;
455458
[self.tableView selectRowAtIndexPath:indexPath
456459
animated:NO
@@ -558,6 +561,7 @@ - (NSIndexPath *)indexPathForSubsection:(BlogDetailsSubsection)subsection
558561
case BlogDetailsSubsectionReminders:
559562
case BlogDetailsSubsectionHome:
560563
case BlogDetailsSubsectionMigrationSuccess:
564+
case BlogDetailsSubsectionJetpackBrandingCard:
561565
return [NSIndexPath indexPathForRow:0 inSection:section];
562566
case BlogDetailsSubsectionDomainCredit:
563567
return [NSIndexPath indexPathForRow:0 inSection:section];
@@ -738,6 +742,9 @@ - (void)configureTableViewData
738742
if (MigrationSuccessCardView.shouldShowMigrationSuccessCard == YES) {
739743
[marr addObject:[self migrationSuccessSectionViewModel]];
740744
}
745+
if (JetpackBrandingMenuCardCoordinator.shouldShowCard == YES) {
746+
[marr addObject:[self jetpackCardSectionViewModel]];
747+
}
741748

742749
if ([DomainCreditEligibilityChecker canRedeemDomainCreditWithBlog:self.blog]) {
743750
if (!self.hasLoggedDomainCreditPromptShownEvent) {
@@ -1165,6 +1172,12 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
11651172
[cell configureWithViewController:self];
11661173
return cell;
11671174
}
1175+
1176+
if (section.category == BlogDetailsSectionCategoryJetpackBrandingCard) {
1177+
JetpackBrandingMenuCardCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsJetpackBrandingCardCellIdentifier];
1178+
[cell configureWithViewController:self];
1179+
return cell;
1180+
}
11681181

11691182
BlogDetailsRow *row = [section.rows objectAtIndex:indexPath.row];
11701183
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.identifier];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
extension BlogDetailsViewController {
4+
5+
@objc func jetpackCardSectionViewModel() -> BlogDetailsSection {
6+
let row = BlogDetailsRow()
7+
row.callback = {}
8+
9+
let section = BlogDetailsSection(title: nil,
10+
rows: [row],
11+
footerTitle: nil,
12+
category: .jetpackBrandingCard)
13+
return section
14+
}
15+
}
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import UIKit
2+
import Lottie
3+
4+
class JetpackBrandingMenuCardCell: UITableViewCell {
5+
6+
// MARK: Private Variables
7+
8+
private weak var viewController: UIViewController?
9+
10+
/// Sets the animation based on the language orientation
11+
private var animation: Animation? {
12+
traitCollection.layoutDirection == .leftToRight ?
13+
Animation.named(Constants.animationLtr) :
14+
Animation.named(Constants.animationRtl)
15+
}
16+
17+
// MARK: Lazy Loading Views
18+
19+
private lazy var cardFrameView: BlogDashboardCardFrameView = {
20+
let frameView = BlogDashboardCardFrameView()
21+
frameView.translatesAutoresizingMaskIntoConstraints = false
22+
frameView.configureButtonContainerStackView()
23+
frameView.hideHeader()
24+
25+
frameView.onEllipsisButtonTap = {
26+
// TODO: Track menu shown
27+
}
28+
frameView.ellipsisButton.showsMenuAsPrimaryAction = true
29+
frameView.ellipsisButton.menu = contextMenu
30+
31+
return frameView
32+
}()
33+
34+
private lazy var containerStackView: UIStackView = {
35+
let stackView = UIStackView()
36+
stackView.axis = .vertical
37+
stackView.alignment = .fill
38+
stackView.translatesAutoresizingMaskIntoConstraints = false
39+
stackView.spacing = Metrics.spacing
40+
stackView.layoutMargins = Metrics.containerMargins
41+
stackView.isLayoutMarginsRelativeArrangement = true
42+
stackView.addArrangedSubviews([logosSuperview, descriptionLabel, learnMoreSuperview])
43+
return stackView
44+
}()
45+
46+
private lazy var logosSuperview: UIView = {
47+
let view = UIView()
48+
view.translatesAutoresizingMaskIntoConstraints = false
49+
view.backgroundColor = .clear
50+
view.addSubview(logosAnimationView)
51+
52+
view.topAnchor.constraint(equalTo: logosAnimationView.topAnchor).isActive = true
53+
view.bottomAnchor.constraint(equalTo: logosAnimationView.bottomAnchor).isActive = true
54+
view.leadingAnchor.constraint(equalTo: logosAnimationView.leadingAnchor).isActive = true
55+
56+
return view
57+
}()
58+
59+
private lazy var logosAnimationView: AnimationView = {
60+
let view = AnimationView()
61+
view.translatesAutoresizingMaskIntoConstraints = false
62+
view.animation = animation
63+
64+
// Height Constraint
65+
view.heightAnchor.constraint(equalToConstant: Metrics.animationsViewHeight).isActive = true
66+
67+
// Width constraint to achieve aspect ratio
68+
let animationSize = animation?.size ?? .init(width: 1, height: 1)
69+
let ratio = animationSize.width / animationSize.height
70+
view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: ratio).isActive = true
71+
72+
return view
73+
}()
74+
75+
private lazy var descriptionLabel: UILabel = {
76+
let label = UILabel()
77+
label.translatesAutoresizingMaskIntoConstraints = false
78+
label.font = Metrics.descriptionFont
79+
label.numberOfLines = 0
80+
label.adjustsFontForContentSizeCategory = true
81+
82+
return label
83+
}()
84+
85+
private lazy var learnMoreSuperview: UIView = {
86+
let view = UIView()
87+
view.translatesAutoresizingMaskIntoConstraints = false
88+
view.backgroundColor = .clear
89+
view.addSubview(learnMoreButton)
90+
91+
view.topAnchor.constraint(equalTo: learnMoreButton.topAnchor).isActive = true
92+
view.bottomAnchor.constraint(equalTo: learnMoreButton.bottomAnchor).isActive = true
93+
view.leadingAnchor.constraint(equalTo: learnMoreButton.leadingAnchor).isActive = true
94+
95+
return view
96+
}()
97+
98+
private lazy var learnMoreButton: UIButton = {
99+
let button = UIButton()
100+
button.translatesAutoresizingMaskIntoConstraints = false
101+
button.tintColor = Metrics.learnMoreButtonTextColor
102+
button.titleLabel?.font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .regular)
103+
button.titleLabel?.adjustsFontForContentSizeCategory = true
104+
button.setTitle(Strings.learnMoreButtonText, for: .normal)
105+
button.addTarget(self, action: #selector(learnMoreButtonTapped), for: .touchUpInside)
106+
107+
if #available(iOS 15.0, *) {
108+
var learnMoreButtonConfig: UIButton.Configuration = .plain()
109+
learnMoreButtonConfig.contentInsets = Metrics.learnMoreButtonContentInsets
110+
button.configuration = learnMoreButtonConfig
111+
} else {
112+
button.contentEdgeInsets = Metrics.learnMoreButtonContentEdgeInsets
113+
button.flipInsetsForRightToLeftLayoutDirection()
114+
}
115+
116+
return button
117+
}()
118+
119+
// MARK: Initializers
120+
121+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
122+
super.init(style: style, reuseIdentifier: reuseIdentifier)
123+
commonInit()
124+
}
125+
126+
required init?(coder: NSCoder) {
127+
super.init(coder: coder)
128+
commonInit()
129+
}
130+
131+
private func commonInit() {
132+
setupViews()
133+
setupContent()
134+
// TODO: Track card shown
135+
}
136+
137+
// MARK: Helpers
138+
139+
private func setupViews() {
140+
contentView.addSubview(cardFrameView)
141+
contentView.pinSubviewToAllEdges(cardFrameView, priority: Metrics.cardFrameConstraintPriority)
142+
cardFrameView.add(subview: containerStackView)
143+
}
144+
145+
private func setupContent() {
146+
logosAnimationView.play()
147+
let config = JetpackBrandingMenuCardCoordinator.cardConfig
148+
descriptionLabel.text = config?.description
149+
learnMoreSuperview.isHidden = config?.learnMoreButtonURL == nil
150+
}
151+
152+
// MARK: Actions
153+
154+
@objc private func learnMoreButtonTapped() {
155+
guard let config = JetpackBrandingMenuCardCoordinator.cardConfig,
156+
let urlString = config.learnMoreButtonURL,
157+
let url = URL(string: urlString) else {
158+
return
159+
}
160+
161+
let webViewController = WebViewControllerFactory.controller(url: url, source: Constants.analyticsSource)
162+
let navController = UINavigationController(rootViewController: webViewController)
163+
viewController?.present(navController, animated: true)
164+
// TODO: Track button tapped
165+
}
166+
}
167+
168+
// MARK: Contexual Menu
169+
170+
private extension JetpackBrandingMenuCardCell {
171+
172+
// MARK: Items
173+
174+
// Defines the structure of the contextual menu items.
175+
private var contextMenuItems: [MenuItem] {
176+
return [.remindLater(remindMeLaterTapped), .hide(hideThisTapped)]
177+
}
178+
179+
// MARK: Menu Creation
180+
181+
private var contextMenu: UIMenu {
182+
let actions = contextMenuItems.map { $0.toAction }
183+
return .init(title: String(), options: .displayInline, children: actions)
184+
}
185+
186+
// MARK: Actions
187+
188+
private func remindMeLaterTapped() {
189+
// TODO: Implement this
190+
}
191+
192+
private func hideThisTapped() {
193+
// TODO: Implement this
194+
}
195+
}
196+
197+
private extension JetpackBrandingMenuCardCell {
198+
199+
enum Metrics {
200+
// General
201+
static let spacing: CGFloat = 10
202+
static let containerMargins = UIEdgeInsets(top: 20, left: 20, bottom: 12, right: 20)
203+
static let cardFrameConstraintPriority = UILayoutPriority(999)
204+
205+
// Animation view
206+
static let animationsViewHeight: CGFloat = 32
207+
208+
// Description Label
209+
static var descriptionFont: UIFont {
210+
let maximumFontPointSize: CGFloat = 16
211+
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
212+
let font = UIFont(descriptor: fontDescriptor, size: min(fontDescriptor.pointSize, maximumFontPointSize))
213+
return UIFontMetrics.default.scaledFont(for: font)
214+
}
215+
216+
// Learn more button
217+
static let learnMoreButtonContentInsets = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 24)
218+
static let learnMoreButtonContentEdgeInsets = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 24)
219+
static let learnMoreButtonTextColor: UIColor = UIColor.muriel(color: .jetpackGreen, .shade40)
220+
}
221+
222+
enum Constants {
223+
static let animationLtr = "JetpackAllFeaturesLogosAnimation_ltr"
224+
static let animationRtl = "JetpackAllFeaturesLogosAnimation_rtl"
225+
static let analyticsSource = "jetpack_menu_card"
226+
static let remindMeLaterSystemImageName = "alarm"
227+
static let hideThisLaterSystemImageName = "eye.slash"
228+
}
229+
230+
enum Strings {
231+
static let learnMoreButtonText = NSLocalizedString("jetpack.menuCard.learnMore",
232+
value: "Learn more",
233+
comment: "Title of a button that displays a blog post in a web view.")
234+
static let remindMeLaterMenuItemTitle = NSLocalizedString("jetpack.menuCard.remindLater",
235+
value: "Remind me later",
236+
comment: "Menu item title to hide the card for now and show it later.")
237+
static let hideCardMenuItemTitle = NSLocalizedString("jetpack.menuCard.hide",
238+
value: "Hide this",
239+
comment: "Menu item title to hide the card.")
240+
}
241+
242+
enum MenuItem {
243+
case remindLater(_ handler: () -> Void)
244+
case hide(_ handler: () -> Void)
245+
246+
var title: String {
247+
switch self {
248+
case .remindLater:
249+
return Strings.remindMeLaterMenuItemTitle
250+
case .hide:
251+
return Strings.hideCardMenuItemTitle
252+
}
253+
}
254+
255+
var image: UIImage? {
256+
switch self {
257+
case .remindLater:
258+
return .init(systemName: Constants.remindMeLaterSystemImageName)
259+
case .hide:
260+
return .init(systemName: Constants.hideThisLaterSystemImageName)
261+
}
262+
}
263+
264+
var toAction: UIAction {
265+
switch self {
266+
case .remindLater(let handler),
267+
.hide(let handler):
268+
return UIAction(title: title, image: image, attributes: []) { _ in
269+
handler()
270+
}
271+
}
272+
}
273+
}
274+
}
275+
276+
extension JetpackBrandingMenuCardCell {
277+
278+
@objc(configureWithViewController:)
279+
func configure(with viewController: UIViewController) {
280+
self.viewController = viewController
281+
}
282+
}

0 commit comments

Comments
 (0)