Skip to content

Commit

Permalink
Reader Improvements: Adds ability to save interests from the Select I…
Browse files Browse the repository at this point in the history
…nterests view (#14487)

* Update select interests loading view

* Add remote saving of selected interests to the Followed Interests Service

* Save when tapping the next button on the Select Interests view

* Add a property to help determine if a tagtopic was added while logged out

* Prevent the ReaderTagTopic's created locally from being deleted while syncing

* Only show following tags while the user is logged out for reader phase 2

* Add more specific logged out interest helpers

* Style the activityt indicator for dark mode

* Update the followInterests method to support adding interests while logged out

* Inject the logged in state to the service

* Add tests for the saveInterest method

* Add a callback when the interests are saved

* Show the select interests view over the discover tab content

* Fix an issue where the select interests view may not appear when logging in

* Change iOS 12 and below next button colors

* Updates WordPressKit to PR branch

* Apply suggestions from code review

Co-authored-by: Leandro Alonso <[email protected]>

* Change comment to be more descriptive

* Fix build error

* Create a constructor to create a tagtopic from a remote interests

* Fix lint issue

* Small fixes

* Hound woof

* update pods

* Revert duration to zero and add a comment

* Show errors when: interestes fails to load and fails to save

* Update Podfile.lock

* removing the lalala

Co-authored-by: Leandro Alonso <[email protected]>
Co-authored-by: Yael Rubinstein <[email protected]>
  • Loading branch information
3 people authored Jul 22, 2020
1 parent e5ed5f7 commit 8542f34
Show file tree
Hide file tree
Showing 14 changed files with 501 additions and 106 deletions.
25 changes: 25 additions & 0 deletions WordPress/Classes/Models/ReaderTagTopic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,29 @@ import Foundation
override open class var TopicType: String {
return "tag"
}

// MARK: - Logged Out Helpers

/// The tagID used if an interest was added locally and not sync'd with the server
class var loggedOutTagID: NSNumber {
return NSNotFound as NSNumber
}

/// If an interest was added while the user is not logged into a WP.com account
/// The tagID will be 0
@objc var wasAddedWhileLoggedOut: Bool {
return tagID == Self.loggedOutTagID
}

/// Creates a new ReaderTagTopic object from a RemoteReaderInterest
convenience init?(remoteInterest: RemoteReaderInterest, context: NSManagedObjectContext) {
self.init(context: context)

title = remoteInterest.title
slug = remoteInterest.slug
tagID = Self.loggedOutTagID
type = Self.TopicType
following = true
showInMenu = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ protocol ReaderFollowedInterestsService: AnyObject {
/// Fetches the users followed interests from the network, then returns the sync'd interests
/// - Parameter completion: Called after a fetch, will return nil if the user has no interests or an error occurred
func fetchFollowedInterestsRemotely(completion: @escaping ([ReaderTagTopic]?) -> Void)


/// Follow the provided interests
/// If the user is not logged into a WP.com account, the interests will only be saved locally.
func followInterests(_ interests: [RemoteReaderInterest],
success: @escaping ([ReaderTagTopic]?) -> Void,
failure: @escaping (Error) -> Void,
isLoggedIn: Bool)
}

// MARK: - CoreData Fetching
Expand All @@ -22,16 +30,65 @@ extension ReaderTopicService: ReaderFollowedInterestsService {
public func fetchFollowedInterestsRemotely(completion: @escaping ([ReaderTagTopic]?) -> Void) {
fetchReaderMenu(success: { [weak self] in
self?.fetchFollowedInterestsLocally(completion: completion)
}) { error in
}) { [weak self] error in
DDLogError("Could not fetch remotely followed interests: \(String(describing: error))")
completion(nil)
self?.fetchFollowedInterestsLocally(completion: completion)
}
}

func followInterests(_ interests: [RemoteReaderInterest],
success: @escaping ([ReaderTagTopic]?) -> Void,
failure: @escaping (Error) -> Void,
isLoggedIn: Bool) {
// If the user is logged in, attempt to save the interests on the server
// If the user is not logged in, save the interests locally
if isLoggedIn {
let slugs = interests.map { $0.slug }

let topicService = ReaderTopicServiceRemote(wordPressComRestApi: apiRequest())
topicService.followInterests(withSlugs: slugs, success: { [weak self] in
self?.fetchFollowedInterestsRemotely(completion: success)
}) { error in
failure(error)
}
} else {
followInterestsLocally(interests, success: success, failure: failure)
}
}

private func followInterestsLocally(_ interests: [RemoteReaderInterest],
success: @escaping ([ReaderTagTopic]?) -> Void,
failure: @escaping (Error) -> Void) {
// We create a "remote" service to get an accurate path for the tag
// https://public-api.../tags/_tag_/posts
let service = ReaderTopicServiceRemote(wordPressComRestApi: apiRequest())

interests.forEach { interest in
guard let topic = ReaderTagTopic(remoteInterest: interest, context: managedObjectContext) else {
return
}

topic.path = service.pathForTopic(slug: interest.slug)
}

ContextManager.sharedInstance().save(managedObjectContext, withCompletionBlock: { [weak self] in
self?.fetchFollowedInterestsLocally(completion: success)
})
}

private func apiRequest() -> WordPressComRestApi {
let accountService = AccountService(managedObjectContext: managedObjectContext)
let defaultAccount = accountService.defaultWordPressComAccount()
let token: String? = defaultAccount?.authToken

return WordPressComRestApi.defaultApi(oAuthToken: token,
userAgent: WPUserAgent.wordPress())
}

// MARK: - Private: Helpers
// MARK: - Private: Fetching Helpers
private func followedInterestsFetchRequest() -> NSFetchRequest<ReaderTagTopic> {
let entityName = "ReaderTagTopic"
let predicate = NSPredicate(format: "following = YES")
let predicate = NSPredicate(format: "following = YES AND showInMenu = YES")
let fetchRequest = NSFetchRequest<ReaderTagTopic>(entityName: entityName)
fetchRequest.predicate = predicate

Expand Down
13 changes: 13 additions & 0 deletions WordPress/Classes/Services/ReaderTopicService.m
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,19 @@ - (void)mergeMenuTopics:(NSArray *)topics withSuccess:(void (^)(void))success
// removing the topic, if it was once followed its not now.
topic.following = NO;
} else {
// If the user adds a locally saved tag/interest prevent it from being deleted
// while the user is logged out.
if ([Feature enabled:FeatureFlagReaderImprovementsPhase2]) {
if (!ReaderHelpers.isLoggedIn && [topic isKindOfClass:ReaderTagTopic.class]) {
ReaderTagTopic *tagTopic = (ReaderTagTopic *)topic;

if (tagTopic.wasAddedWhileLoggedOut) {
DDLogInfo(@"Not deleting a locally saved topic: %@", topic.title);
continue;
}
}
}

DDLogInfo(@"Deleting topic: %@", topic.title);
[self preserveSavedPostsFromTopic:topic];
[self.managedObjectContext deleteObject:topic];
Expand Down
10 changes: 8 additions & 2 deletions WordPress/Classes/ViewRelated/Reader/Filter/FilterProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,14 @@ extension ReaderTagTopic {

static var tagsFetchRequest: NSFetchRequest<NSFetchRequestResult> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ReaderTagTopic")
fetchRequest.predicate = NSPredicate(format: "following == %@ AND showInMenu == YES AND type == 'tag'",
NSNumber(value: ReaderHelpers.isLoggedIn()))
// Only show following tags, even if the user is logged out
if FeatureFlag.readerImprovementsPhase2.enabled {
fetchRequest.predicate = NSPredicate(format: "following == YES AND showInMenu == YES AND type == 'tag'")
} else {
fetchRequest.predicate = NSPredicate(format: "following == %@ AND showInMenu == YES AND type == 'tag'",
NSNumber(value: ReaderHelpers.isLoggedIn()))
}

fetchRequest.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare))]
return fetchRequest
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ class FilterSheetView: UIView {
}()

private lazy var emptyView: EmptyActionView = {
return EmptyActionView(tappedButton: tappedEmptyAddButton)
let view = EmptyActionView(tappedButton: tappedEmptyAddButton)

// Hide the button if the user is not logged in
if FeatureFlag.readerImprovementsPhase2.enabled {
view.button.isHidden = !ReaderHelpers.isLoggedIn()
}

return view
}()

private lazy var ghostableTableView: UITableView = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ class ReaderSelectInterestsCoordinator {
}()
}

// MARK: - Saving
public func saveInterests(interests: [RemoteReaderInterest], completion: @escaping (Bool) -> Void) {
let isLoggedIn = userId != nil

interestsService.followInterests(interests, success: { _ in
completion(true)

}, failure: { _ in
completion(false)

}, isLoggedIn: isLoggedIn)
}

// MARK: - Display Logic

/// Determines whether or not the select interests view should be displayed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ import WordPressFlux
@objc static let restorationClassIdentifier = "ReaderStreamViewControllerRestorationIdentifier"
@objc static let restorableTopicPathKey: String = "RestorableTopicPathKey"

private var interestsCoordinator = ReaderSelectInterestsCoordinator()

// MARK: - Properties

// Select Interests
private lazy var interestsCoordinator: ReaderSelectInterestsCoordinator = {
return ReaderSelectInterestsCoordinator()
}()

private var selectInterestsViewController: ReaderSelectInterestsViewController?

/// Called if the stream or tag fails to load
var streamLoadFailureBlock: (() -> Void)? = nil

Expand Down Expand Up @@ -327,21 +332,6 @@ import WordPressFlux
super.viewWillAppear(animated)

syncIfAppropriate()

testCheckIfNeedToDisplaySelectInterests()
}

// TODO: Remove this for a real implementation, this is just for testing right now
private func testCheckIfNeedToDisplaySelectInterests() {
if FeatureFlag.readerImprovementsPhase2.enabled {
interestsCoordinator.shouldDisplay { shouldDisplay in
if shouldDisplay {
let controller = ReaderSelectInterestsViewController()
self.navigationController?.present(controller, animated: true)
}
self.interestsCoordinator.markAsSeen()
}
}
}

override func viewDidAppear(_ animated: Bool) {
Expand Down Expand Up @@ -1815,6 +1805,8 @@ extension ReaderStreamViewController: ReaderContentViewController {
return
}
siteID = content.topicType == .discover ? ReaderHelpers.discoverSiteID : nil

displaySelectInterestsIfNeeded(content)
}
}

Expand All @@ -1841,6 +1833,65 @@ extension ReaderStreamViewController: ReaderPostUndoCellDelegate {
}


// MARK: - Select Interests Display
private extension ReaderStreamViewController {
func displaySelectInterestsIfNeeded(_ content: ReaderContent) {
guard FeatureFlag.readerImprovementsPhase2.enabled,
content.topicType == .discover else {
// Removes the view if we're not on the discover tab, and it exists
selectInterestsViewController?.remove()
return
}

if self.selectInterestsViewController != nil {
showSelectInterestsViewIfNeeded()
return
}

// If we're not showing the select interests view, check to see if we should
interestsCoordinator.shouldDisplay { [unowned self] shouldDisplay in
if shouldDisplay {
self.makeSelectInterestsViewControllerIfNeeded()
self.showSelectInterestsViewIfNeeded()
}
}
}

func showSelectInterestsViewIfNeeded() {
guard let controller = selectInterestsViewController else {
return
}

// Using duration zero to prevent the screen from blinking
UIView.animate(withDuration: 0) {
controller.view.frame = self.view.bounds
self.add(controller, asChildOf: self)
}
}

func makeSelectInterestsViewControllerIfNeeded() {
if selectInterestsViewController != nil {
return
}

let controller = ReaderSelectInterestsViewController()
controller.didSaveInterests = { [unowned self] in
guard let controller = self.selectInterestsViewController else {
return
}

UIView.animate(withDuration: 0.2, animations: {
controller.view.alpha = 0.0
}) { [unowned self] _ in
controller.remove()
self.selectInterestsViewController = nil
}
}

selectInterestsViewController = controller
}
}

// MARK: - View content types without a topic
private extension ReaderStreamViewController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ReaderInterestViewModel {
return interest.slug
}

private var interest: RemoteReaderInterest
let interest: RemoteReaderInterest

init(interest: RemoteReaderInterest) {
self.interest = interest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,29 @@ class ReaderInterestsStyleGuide {

public class func applyNextButtonStyle(button: FancyButton) {
let disabledBackgroundColor: UIColor
let titleColor: UIColor

if #available(iOS 13.0, *) {
// System Gray 4 on Dark mode is the same color as tertiarySystemBackground
disabledBackgroundColor = UIColor(light: .systemGray4, dark: .systemGray3)
titleColor = .textTertiary
} else {
disabledBackgroundColor = .lightGray
disabledBackgroundColor = .tertiaryBackground
titleColor = .text
}

button.disabledTitleColor = .textTertiary
button.disabledTitleColor = titleColor
button.disabledBorderColor = disabledBackgroundColor
button.disabledBackgroundColor = disabledBackgroundColor
}

// MARK: - Loading
public class func applyLoadingLabelStyles(label: UILabel) {
label.font = WPStyleGuide.fontForTextStyle(.body)
label.textColor = .textSubtle
}

public class func applyActivityIndicatorStyles(indicator: UIActivityIndicatorView) {
indicator.color = UIColor(light: .black, dark: .white)
}
}
Loading

0 comments on commit 8542f34

Please sign in to comment.