Skip to content

Commit fc8347b

Browse files
committed
Merge branch 'trunk' into core-data-test-case
Conflicts: WordPress/WordPressTest/AccountServiceTests.swift WordPress/WordPressTest/CommentServiceTests.swift WordPress/WordPressTest/Dashboard/DashboardPostsSyncManagerTests.swift WordPress/WordPressTest/MediaRequestAuthenticatorTests.swift WordPress/WordPressTest/PostCoordinatorTests.swift WordPress/WordPressTest/QuickStartFactoryTests.swift WordPress/WordPressTest/Services/MediaCoordinatorTests.swift WordPress/WordPressTest/Services/PostServiceWPComTests.swift WordPress/WordPressTest/ViewRelated/Post/Utils/PostNoticeViewModelTests.swift
2 parents ecfc2b2 + ef661ae commit fc8347b

27 files changed

+271
-106
lines changed

WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@ public class BloggingPrompt: NSManagedObject {
3838
self.answerCount = Int32(remotePrompt.answeredUsersCount)
3939
self.displayAvatarURLs = remotePrompt.answeredUserAvatarURLs
4040
}
41+
42+
func textForDisplay() -> String {
43+
return text.stringByDecodingXMLCharacters().trim()
44+
}
4145
}

WordPress/Classes/Services/BloggingPromptsService.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ class BloggingPromptsService {
6565
}, failure: failure)
6666
}
6767

68+
/// Convenience method to obtain the blogging prompt for the current day,
69+
/// either from local cache or remote.
70+
///
71+
/// - Parameters:
72+
/// - success: Closure to be called when the fetch process succeeded.
73+
/// - failure: Closure to be called when the fetch process failed.
74+
func todaysPrompt(success: @escaping (BloggingPrompt?) -> Void,
75+
failure: @escaping (Error?) -> Void) {
76+
guard localTodaysPrompt == nil else {
77+
success(localTodaysPrompt)
78+
return
79+
}
80+
81+
fetchTodaysPrompt(success: success, failure: failure)
82+
}
83+
6884
/// Convenience method to fetch the blogging prompts for the Prompts List.
6985
/// Fetches 11 prompts - the current day and 10 previous.
7086
///
@@ -113,7 +129,7 @@ private extension BloggingPromptsService {
113129
///
114130
/// - Parameters:
115131
/// - date: When specified, only prompts from the specified date will be returned.
116-
/// - number: The amount of prompts to return. Defaults to 24 when unspecified.
132+
/// - number: The amount of prompts to return.
117133
/// - Returns: An array of `BloggingPrompt` objects sorted descending by date.
118134
func loadPrompts(from date: Date, number: Int) -> [BloggingPrompt] {
119135
guard let utcDate = utcDateIgnoringTime(from: date) else {

WordPress/Classes/Utility/ContextManager.m

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
// MARK: - Static Variables
1111
//
1212
static ContextManager *_instance;
13-
static ContextManager *_override;
1413

1514

1615
// MARK: - Private Properties
@@ -72,7 +71,7 @@ + (instancetype)internalSharedInstance
7271
_instance = [[ContextManager alloc] init];
7372
});
7473

75-
return _override ?: _instance;
74+
return _instance;
7675
}
7776

7877
#pragma mark - Contexts

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,22 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
320320
forExampleDisplay = true
321321
}
322322

323+
// Class method to determine if the Dashboard should show this card.
324+
// Specifically, it checks if today's prompt has been skipped,
325+
// and therefore should not be shown.
326+
static func shouldShowCard(for blog: Blog) -> Bool {
327+
guard FeatureFlag.bloggingPrompts.enabled else {
328+
return false
329+
}
330+
331+
guard let todaysPrompt = BloggingPromptsService(blog: blog)?.localTodaysPrompt else {
332+
// If there is no cached prompt, it can't have been skipped. So show the card.
333+
return true
334+
}
335+
336+
return !userSkippedPrompt(todaysPrompt, for: blog)
337+
}
338+
323339
}
324340

325341
// MARK: - BlogDashboardCardConfigurable
@@ -354,7 +370,7 @@ private extension DashboardPromptsCardCell {
354370
return
355371
}
356372

357-
promptLabel.text = forExampleDisplay ? Strings.examplePrompt : prompt?.text.stringByDecodingXMLCharacters().trim()
373+
promptLabel.text = forExampleDisplay ? Strings.examplePrompt : prompt?.textForDisplay()
358374
containerStackView.addArrangedSubview(promptTitleView)
359375

360376
if let attribution = prompt?.promptAttribution {
@@ -373,15 +389,13 @@ private extension DashboardPromptsCardCell {
373389
// MARK: Prompt Fetching
374390

375391
func fetchPrompt() {
376-
// TODO: check for cached prompt first.
377-
378392
guard let bloggingPromptsService = bloggingPromptsService else {
379393
didFailLoadingPrompt = true
380394
DDLogError("Failed creating BloggingPromptsService instance.")
381395
return
382396
}
383397

384-
bloggingPromptsService.fetchTodaysPrompt(success: { [weak self] (prompt) in
398+
bloggingPromptsService.todaysPrompt(success: { [weak self] (prompt) in
385399
self?.prompt = prompt
386400
self?.didFailLoadingPrompt = false
387401
}, failure: { [weak self] (error) in
@@ -418,7 +432,8 @@ private extension DashboardPromptsCardCell {
418432
}
419433

420434
func skipMenuTapped() {
421-
// TODO.
435+
saveSkippedPromptForSite()
436+
presenterViewController?.reloadCardsLocally()
422437
}
423438

424439
func removeMenuTapped() {
@@ -476,6 +491,7 @@ private extension DashboardPromptsCardCell {
476491
static let exampleAnswerCount = 19
477492
static let cardIconSize = CGSize(width: 18, height: 18)
478493
static let cardFrameConstraintPriority = UILayoutPriority(999)
494+
static let skippedPromptsUDKey = "wp_skipped_blogging_prompts"
479495
}
480496

481497
// MARK: Contextual Menu
@@ -537,3 +553,48 @@ private extension DashboardPromptsCardCell {
537553
}
538554
}
539555
}
556+
557+
// MARK: - User Defaults
558+
559+
private extension DashboardPromptsCardCell {
560+
561+
static var allSkippedPrompts: [[String: Int32]] {
562+
return UserDefaults.standard.array(forKey: Constants.skippedPromptsUDKey) as? [[String: Int32]] ?? []
563+
}
564+
565+
func saveSkippedPromptForSite() {
566+
guard let prompt = prompt,
567+
let siteID = blog?.dotComID?.stringValue else {
568+
return
569+
}
570+
571+
clearSkippedPromptForSite()
572+
573+
let skippedPrompt = [siteID: prompt.promptID]
574+
var updatedSkippedPrompts = DashboardPromptsCardCell.allSkippedPrompts
575+
updatedSkippedPrompts.append(skippedPrompt)
576+
577+
UserDefaults.standard.set(updatedSkippedPrompts, forKey: Constants.skippedPromptsUDKey)
578+
}
579+
580+
func clearSkippedPromptForSite() {
581+
guard let siteID = blog?.dotComID?.stringValue else {
582+
return
583+
}
584+
585+
let updatedSkippedPrompts = DashboardPromptsCardCell.allSkippedPrompts.filter { $0.keys.first != siteID }
586+
UserDefaults.standard.set(updatedSkippedPrompts, forKey: Constants.skippedPromptsUDKey)
587+
}
588+
589+
static func userSkippedPrompt(_ prompt: BloggingPrompt, for blog: Blog) -> Bool {
590+
guard let siteID = blog.dotComID?.stringValue else {
591+
return false
592+
}
593+
594+
let siteSkippedPrompts = allSkippedPrompts.filter { $0.keys.first == siteID }
595+
let matchingPrompts = siteSkippedPrompts.filter { $0.values.first == prompt.promptID }
596+
597+
return !matchingPrompts.isEmpty
598+
}
599+
600+
}

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/DashboardCard.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ enum DashboardCard: String, CaseIterable {
5757
case .todaysStats:
5858
return self.shouldShowRemoteCard(apiResponse: apiResponse)
5959
case .prompts:
60-
return FeatureFlag.bloggingPrompts.enabled
60+
return DashboardPromptsCardCell.shouldShowCard(for: blog)
6161
case .ghost:
6262
return blog.dashboardState.isFirstLoad
6363
case .failure:

WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptTableViewCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class BloggingPromptTableViewCell: UITableViewCell, NibReusable {
2929

3030
func configure(_ prompt: BloggingPrompt) {
3131
self.prompt = prompt
32-
titleLabel.text = prompt.text.stringByDecodingXMLCharacters().trim()
32+
titleLabel.text = prompt.textForDisplay()
3333
dateLabel.text = dateFormatter.string(from: prompt.date)
3434
answerCountLabel.text = answerInfoText
3535
answeredStateView.isHidden = !prompt.answered

WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController+Helper.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ extension ReaderStreamViewController {
5151

5252
static let defaultResponse = NoResultsResponse(
5353
title: NSLocalizedString("No recent posts", comment: "A message title"),
54-
message: NSLocalizedString("No posts have been made recently", comment: "A default message shown whe the reader can find no post to display"))
54+
message: NSLocalizedString("No posts have been made recently", comment: "A default message shown when the reader can find no post to display"))
5555

5656
/// Returns a NoResultsResponse instance appropriate for the specified ReaderTopic
5757
///
@@ -120,7 +120,7 @@ extension ReaderStreamViewController {
120120

121121
func configureNoResultsViewForSavedPosts() {
122122

123-
let noResultsResponse = NoResultsResponse(title: NSLocalizedString("No Saved Posts",
123+
let noResultsResponse = NoResultsResponse(title: NSLocalizedString("No saved posts",
124124
comment: "Message displayed in Reader Saved Posts view if a user hasn't yet saved any posts."),
125125
message: NSLocalizedString("Tap [bookmark-outline] to save a post to your list.",
126126
comment: "A hint displayed in the Saved Posts section of the Reader. The '[bookmark-outline]' placeholder will be replaced by an icon at runtime – please leave that string intact."))

WordPress/Classes/ViewRelated/System/Action Sheet/ActionSheetViewController.swift

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ActionSheetViewController: UIViewController {
2424
static let buttonSpacing: CGFloat = 8
2525
static let additionalSafeAreaInsetsRegular: UIEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0)
2626
static let minimumWidth: CGFloat = 300
27+
static let maximumWidth: CGFloat = 600
2728

2829
enum Header {
2930
static let spacing: CGFloat = 16
@@ -48,6 +49,9 @@ class ActionSheetViewController: UIViewController {
4849
let headerView: UIView?
4950
let buttons: [ActionSheetButton]
5051
let headerTitle: String
52+
private weak var scrollView: UIScrollView?
53+
private var scrollViewHeightConstraint: NSLayoutConstraint?
54+
private var scrollViewTopConstraint: NSLayoutConstraint?
5155

5256
init(headerView: UIView? = nil, headerTitle: String, buttons: [ActionSheetButton]) {
5357
self.headerView = headerView
@@ -87,52 +91,60 @@ class ActionSheetViewController: UIViewController {
8791
headerLabel.font = WPStyleGuide.fontForTextStyle(.headline)
8892
headerLabel.text = headerTitle
8993
headerLabel.translatesAutoresizingMaskIntoConstraints = false
94+
headerLabel.adjustsFontForContentSizeCategory = true
9095

9196
let buttonViews = buttons.map({ (buttonInfo) -> UIButton in
9297
return button(buttonInfo)
9398
})
9499

95-
NSLayoutConstraint.activate([
96-
gripButton.heightAnchor.constraint(equalToConstant: Constants.gripHeight)
97-
])
98100

99-
let buttonConstraints = buttonViews.map { button in
100-
return button.heightAnchor.constraint(equalToConstant: Constants.Button.height)
101+
let buttonConstraints = buttonViews.flatMap { button in
102+
[
103+
button.heightAnchor.constraint(equalToConstant: Constants.Button.height),
104+
button.widthAnchor.constraint(equalTo: view.widthAnchor),
105+
]
101106
}
102107

103-
NSLayoutConstraint.activate(buttonConstraints)
108+
let scrollView = UIScrollView()
109+
scrollView.translatesAutoresizingMaskIntoConstraints = false
110+
view.addSubviews([gripButton, scrollView])
104111

105-
let stackView = UIStackView(arrangedSubviews: [gripButton])
112+
let stackView = UIStackView()
113+
stackView.translatesAutoresizingMaskIntoConstraints = false
114+
stackView.axis = .vertical
115+
scrollView.addSubview(stackView)
116+
scrollView.pinSubviewToAllEdges(stackView)
106117

107118
if let headerView = headerView {
108119
stackView.addArrangedSubview(headerView)
109120
}
110121

111122
stackView.addArrangedSubviews([headerLabelView] + buttonViews)
112-
113-
stackView.setCustomSpacing(Constants.Header.spacing, after: gripButton)
114123
stackView.setCustomSpacing(Constants.Header.spacing, after: headerLabelView)
115124

116125
buttonViews.forEach { button in
117126
stackView.setCustomSpacing(Constants.buttonSpacing, after: button)
118127
}
119128

120-
stackView.translatesAutoresizingMaskIntoConstraints = false
121-
stackView.axis = .vertical
122-
129+
let topConstraint = scrollView.topAnchor.constraint(equalTo: gripButton.bottomAnchor, constant: Constants.Header.spacing)
130+
scrollViewTopConstraint = topConstraint
131+
let secondaryTopConstraint = scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
132+
secondaryTopConstraint.priority = .defaultHigh
133+
NSLayoutConstraint.activate([
134+
gripButton.heightAnchor.constraint(equalToConstant: Constants.gripHeight),
135+
gripButton.widthAnchor.constraint(equalTo: view.widthAnchor),
136+
gripButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
137+
gripButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: Constants.Stack.insets.top),
138+
topConstraint,
139+
secondaryTopConstraint,
140+
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
141+
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
142+
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
143+
] + buttonConstraints)
144+
145+
self.scrollView = scrollView
123146
refreshForTraits()
124-
125-
view.addSubview(stackView)
126-
let stackViewConstraints = [
127-
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -Constants.Stack.insets.left),
128-
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: Constants.Stack.insets.right),
129-
view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: stackView.topAnchor, constant: -Constants.Stack.insets.top),
130-
]
131-
132-
let bottomAnchor = view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: Constants.Stack.insets.bottom)
133-
bottomAnchor.priority = .defaultHigh
134-
135-
NSLayoutConstraint.activate(stackViewConstraints + [bottomAnchor])
147+
updateScrollViewHeight()
136148
}
137149

138150
private func createButton(_ handler: @escaping () -> Void) -> UIButton {
@@ -154,6 +166,7 @@ class ActionSheetViewController: UIViewController {
154166
button.contentEdgeInsets = Constants.Button.contentInsets
155167
button.translatesAutoresizingMaskIntoConstraints = false
156168
button.flipInsetsForRightToLeftLayoutDirection()
169+
button.titleLabel?.adjustsFontForContentSizeCategory = true
157170
return button
158171
}
159172

@@ -198,14 +211,32 @@ class ActionSheetViewController: UIViewController {
198211
if presentingViewController?.traitCollection.horizontalSizeClass == .regular && presentingViewController?.traitCollection.verticalSizeClass != .compact {
199212
gripButton.isHidden = true
200213
additionalSafeAreaInsets = Constants.additionalSafeAreaInsetsRegular
214+
scrollViewTopConstraint?.isActive = false
201215
} else {
202216
gripButton.isHidden = false
203217
additionalSafeAreaInsets = .zero
218+
scrollViewTopConstraint?.isActive = true
204219
}
205220
}
206221

207222
override func viewDidLayoutSubviews() {
208223
super.viewDidLayoutSubviews()
209-
return preferredContentSize = CGSize(width: Constants.minimumWidth, height: view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height)
224+
updateScrollViewHeight()
225+
let compressedSize = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
226+
let width = min(max(Constants.minimumWidth, compressedSize.width), Constants.maximumWidth)
227+
preferredContentSize = CGSize(width: width, height: compressedSize.height)
228+
}
229+
230+
private func updateScrollViewHeight() {
231+
guard let scrollView = scrollView else {
232+
return
233+
}
234+
scrollView.layoutIfNeeded()
235+
let scrollViewHeight = scrollView.contentSize.height
236+
let heightConstraint = scrollViewHeightConstraint ?? scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: scrollViewHeight)
237+
heightConstraint.constant = scrollViewHeight
238+
heightConstraint.priority = .defaultHigh
239+
heightConstraint.isActive = true
240+
scrollViewHeightConstraint = heightConstraint
210241
}
211242
}

WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ private extension BloggingPromptsHeaderView {
7171
shareButton.titleLabel?.adjustsFontForContentSizeCategory = true
7272
shareButton.titleLabel?.adjustsFontSizeToFitWidth = true
7373
shareButton.setTitleColor(WPStyleGuide.BloggingPrompts.buttonTitleColor, for: .normal)
74+
attributionLabel.adjustsFontForContentSizeCategory = true
7475
}
7576

7677
func configureConstraints() {
@@ -92,7 +93,7 @@ private extension BloggingPromptsHeaderView {
9293
}
9394

9495
func configure(_ prompt: BloggingPrompt?) {
95-
promptLabel.text = prompt?.text
96+
promptLabel.text = prompt?.textForDisplay()
9697

9798
let answered = prompt?.answered ?? false
9899
answerPromptButton.isHidden = answered

0 commit comments

Comments
 (0)