diff --git a/WordPress/Classes/Services/ReaderCardService.swift b/WordPress/Classes/Services/ReaderCardService.swift index 47b2c016b3ed..b9a8334fc2d4 100644 --- a/WordPress/Classes/Services/ReaderCardService.swift +++ b/WordPress/Classes/Services/ReaderCardService.swift @@ -47,14 +47,15 @@ class ReaderCardService { func fetch(isFirstPage: Bool, refreshCount: Int = 0, success: @escaping (Int, Bool) -> Void, failure: @escaping (Error?) -> Void) { followedInterestsService.fetchFollowedInterestsLocally { [weak self] topics in - guard let self, - let interests = topics, - !interests.isEmpty else { - failure(Errors.noInterests) + guard let self, let interests = topics else { + failure(URLError(.unknown)) // Should never happen return } - let slugs = interests.map { $0.slug } + var slugs = interests.map { $0.slug } + if slugs.isEmpty { + slugs = ["dailyprompt", "wordpress"] // Matches wp.com + } let success: ([RemoteReaderCard], String?) -> Void = { [weak self] cards, pageHandle in guard let self else { return @@ -157,10 +158,6 @@ class ReaderCardService { isFirstPage ? nil : self.pageHandle } - enum Errors: Error { - case noInterests - } - private enum Constants { static let paginationMultiplier = 100 static let firstPage = 1 diff --git a/WordPress/Classes/Services/ReaderSearchSuggestionService.swift b/WordPress/Classes/Services/ReaderSearchSuggestionService.swift deleted file mode 100644 index 6efeadf46ac3..000000000000 --- a/WordPress/Classes/Services/ReaderSearchSuggestionService.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Foundation - -/// Provides functionality for fetching, saving, and deleting search phrases -/// used to search for content in the reader. -/// -@objc class ReaderSearchSuggestionService: NSObject { - - private let coreDataStack: CoreDataStack - - @objc init(coreDataStack: CoreDataStack) { - self.coreDataStack = coreDataStack - super.init() - } - - /// Creates or updates an existing record for the specified search phrase. - /// - /// - Parameters: - /// - phrase: The search phrase in question. - /// - @objc(createOrUpdateSuggestionForPhrase:) - func createOrUpdateSuggestion(forPhrase phrase: String) { - self.coreDataStack.performAndSave { context in - var suggestion = self.findSuggestion(forPhrase: phrase, in: context) - if suggestion == nil { - suggestion = NSEntityDescription.insertNewObject( - forEntityName: ReaderSearchSuggestion.classNameWithoutNamespaces(), - into: context - ) as? ReaderSearchSuggestion - suggestion?.searchPhrase = phrase - } - suggestion?.date = Date() - } - } - - /// Find and return the ReaderSearchSuggestion matching the specified search phrase. - /// - /// - Parameters: - /// - phrase: The search phrase in question. - /// - /// - Returns: A matching search phrase or nil. - /// - private func findSuggestion(forPhrase phrase: String, in context: NSManagedObjectContext) -> ReaderSearchSuggestion? { - let phrase = NSRegularExpression.escapedPattern(for: phrase) - let fetchRequest = NSFetchRequest(entityName: "ReaderSearchSuggestion") - fetchRequest.predicate = NSPredicate(format: "searchPhrase MATCHES[cd] %@", phrase) - - var suggestions = [ReaderSearchSuggestion]() - do { - suggestions = try context.fetch(fetchRequest) as! [ReaderSearchSuggestion] - } catch let error as NSError { - DDLogError("Error fetching search suggestion for phrase \(phrase) : \(error.localizedDescription)") - } - - return suggestions.first - } - - /// Deletes all saved search suggestions. - /// - @objc func deleteAllSuggestions() { - self.coreDataStack.performAndSave { context in - let fetchRequest = NSFetchRequest(entityName: "ReaderSearchSuggestion") - do { - let suggestions = try context.fetch(fetchRequest) as! [ReaderSearchSuggestion] - suggestions.forEach(context.delete(_:)) - } catch let error as NSError { - DDLogError("Error fetching search suggestion : \(error.localizedDescription)") - } - } - } - -} diff --git a/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift b/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift index 5950ff480da1..a5eb335f9f4a 100644 --- a/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift +++ b/WordPress/Classes/Stores/UserPersistentRepositoryUtility.swift @@ -18,6 +18,7 @@ private enum UPRUConstants { static let readerSidebarSelectionKey = "readerSidebarSelectionKey" static let isReaderSelectedKey = "isReaderSelectedKey" static let readerSearchHistoryKey = "readerSearchHistoryKey" + static let readerDidSelectInterestsKey = "readerDidSelectInterestsKey" } protocol UserPersistentRepositoryUtility: AnyObject { @@ -190,4 +191,13 @@ extension UserPersistentRepositoryUtility { .set(newValue, forKey: UPRUConstants.readerSearchHistoryKey) } } + + var readerDidSelectInterestsKey: Bool { + get { + UserPersistentStoreFactory.instance().bool(forKey: UPRUConstants.readerDidSelectInterestsKey) + } + set { + UserPersistentStoreFactory.instance().set(newValue, forKey: UPRUConstants.readerDidSelectInterestsKey) + } + } } diff --git a/WordPress/Classes/System/Root View/ReaderPresenter.swift b/WordPress/Classes/System/Root View/ReaderPresenter.swift index 5c7738e339b6..fcda27c1748c 100644 --- a/WordPress/Classes/System/Root View/ReaderPresenter.swift +++ b/WordPress/Classes/System/Root View/ReaderPresenter.swift @@ -80,6 +80,8 @@ final class ReaderPresenter: NSObject, SplitViewDisplayable { case .organization(let objectID): show(makeViewController(withTopicID: objectID)) } + + hideSupplementaryColumnIfNeeded() } private func popMainNavigationController() { diff --git a/WordPress/Classes/System/WordPressAppDelegate.swift b/WordPress/Classes/System/WordPressAppDelegate.swift index 125d465f46b9..f102c8728a65 100644 --- a/WordPress/Classes/System/WordPressAppDelegate.swift +++ b/WordPress/Classes/System/WordPressAppDelegate.swift @@ -705,8 +705,10 @@ extension WordPressAppDelegate { ReaderPostService(coreDataStack: ContextManager.shared).clearInUseFlags() ReaderTopicService(coreDataStack: ContextManager.shared).clearInUseFlags() ReaderPostService(coreDataStack: ContextManager.shared).clearSavedPostFlags() - ReaderSearchSuggestionService(coreDataStack: ContextManager.sharedInstance()).deleteAllSuggestions() + UserDefaults.standard.isReaderSelected = false UserDefaults.standard.readerSidebarSelection = nil + UserDefaults.standard.readerSearchHistory = [] + UserDefaults.standard.readerDidSelectInterestsKey = false } } diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift index 9dbc731c2a23..d56795982d20 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift @@ -9,6 +9,8 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe private var selectedChannel: ReaderDiscoverChannel = .recommended private let topic: ReaderAbstractTopic private var streamVC: ReaderStreamViewController? + private weak var selectInterestsVC: ReaderSelectInterestsViewController? + private let selectInterestsCoordinator = ReaderSelectInterestsCoordinator() private let tags: ManagedObjectsObserver private let viewContext: NSManagedObjectContext private var cancellables: [AnyCancellable] = [] @@ -36,6 +38,8 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe setupHeaderView() configureStream(for: selectedChannel) + + showSelectInterestsIfNeeded() } private func setupNavigation() { @@ -112,13 +116,51 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe ReaderCardService.removeAllCards() } - // MARK: - ReaderDiscoverHeaderViewDelegate + // MARK: ReaderDiscoverHeaderViewDelegate func readerDiscoverHeaderView(_ view: ReaderDiscoverHeaderView, didChangeSelection selection: ReaderDiscoverChannel) { self.selectedChannel = selection configureStream(for: selection) WPAnalytics.track(.readerDiscoverChannelSelected, properties: selection.analyticsProperties) } + + // MARK: Select Interests + + private func showSelectInterestsIfNeeded() { + guard !UserDefaults.standard.readerDidSelectInterestsKey else { + return + } + selectInterestsCoordinator.isFollowingInterests { [weak self] isFollowing in + if !isFollowing { + self?.showSelectInterestsScreen() + } + } + } + + private func showSelectInterestsScreen() { + guard selectInterestsVC == nil else { return } + + let selectInterestsVC = ReaderSelectInterestsViewController(configuration: .discover) + selectInterestsVC.isModalInPresentation = true + selectInterestsVC.didSaveInterests = { [weak self] _ in + self?.didSaveInterests() + } + present(selectInterestsVC, animated: true) + self.selectInterestsVC = selectInterestsVC + } + + private func didSaveInterests() { + UserDefaults.standard.readerDidSelectInterestsKey = true + + guard selectInterestsVC != nil else { return } + dismiss(animated: true) { + if let streamVC = self.streamVC { + streamVC.scrollViewToTop() + streamVC.displayLoadingStream() + streamVC.syncIfAppropriate(forceSync: true) + } + } + } } private class ReaderDiscoverStreamViewController: ReaderStreamViewController { @@ -167,11 +209,6 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController { addObservers() } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - displaySelectInterestsIfNeeded() - } - // MARK: - UITableView override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -233,10 +270,6 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController { return cell } - private func isTableViewAtTheTop() -> Bool { - return tableView.contentOffset.y == 0 - } - @objc private func reload(_ notification: Foundation.Notification) { tableView.reloadData() } @@ -279,7 +312,7 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController { override func syncIfAppropriate(forceSync: Bool = false) { // Only sync if the tableview is at the top, otherwise this will change tableview's offset - if isTableViewAtTheTop() { + if tableView.contentOffset.y <= 0 { super.syncIfAppropriate(forceSync: forceSync) } } @@ -333,20 +366,6 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController { } } -// MARK: - Select Interests Display -private extension ReaderDiscoverStreamViewController { - func displaySelectInterestsIfNeeded() { - selectInterestsVC.userIsFollowingTopics { [weak self] isFollowing in - guard let self else { return } - if isFollowing { - self.hideSelectInterestsView() - } else { - self.showSelectInterestsView() - } - } - } -} - // MARK: - ReaderTopicsTableCardCellDelegate extension ReaderDiscoverStreamViewController: ReaderTopicsTableCardCellDelegate { diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 64fd0bc0adbb..8f954f29d216 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -177,12 +177,6 @@ import AutomatticTracks private var showConfirmation = true - lazy var selectInterestsVC = ReaderSelectInterestsViewController(configuration: .discover) - - /// Tracks whether or not we should force sync - /// This is set to true after the Reader Manage view is dismissed - var shouldForceRefresh = false - var isEmbeddedInDiscover = false private var isCompact = true { @@ -197,7 +191,11 @@ import AutomatticTracks oldValue?.removeFromSuperview() if let emptyStateView { view.addSubview(emptyStateView) - emptyStateView.pinEdges(to: view.safeAreaLayoutGuide) + emptyStateView.pinEdges(.horizontal, to: view.safeAreaLayoutGuide) + NSLayoutConstraint.activate([ + emptyStateView.topAnchor.constraint(equalTo: tableView.tableHeaderView?.bottomAnchor ?? view.safeAreaLayoutGuide.topAnchor), + emptyStateView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + ]) footerView.isHidden = true hideGhost() @@ -523,7 +521,7 @@ import AutomatticTracks recentlyBlockedSitePostObjectIDs.removeAllObjects() updateAndPerformFetchRequest() configureStreamHeader() - tableView.setContentOffset(CGPoint.zero, animated: false) + tableView.setContentOffset(CGPoint(x: 0, y: -(tableView.adjustedContentInset.top)), animated: false) content.refresh() if synchronize { @@ -577,16 +575,9 @@ import AutomatticTracks // MARK: - Instance Methods - /// Scrolls to the top of the list of posts. @objc func scrollViewToTop() { - guard tableView.numberOfRows(inSection: .zero) > 0 else { - tableView.setContentOffset(.zero, animated: true) - return - } - - /// `scrollToRow` somehow works better when the first cell has dynamic height. With `setContentOffset`, - /// sometimes it doesn't perfectly scroll to the top, thus making the top cell appear clipped. - tableView.scrollToRow(at: IndexPath(row: .zero, section: .zero), at: .top, animated: true) + // Uses `contentInset.top` to accomodate for the safe area insets + tableView.setContentOffset(CGPoint(x: 0, y: -(tableView.adjustedContentInset.top)), animated: true) } /// Returns the analytics property dictionary for the current topic. @@ -1550,48 +1541,7 @@ extension ReaderStreamViewController { emptyStateView = makeEmptyStateView(.noFollowedSites) } - func showSelectInterestsView() { - guard selectInterestsVC.parent == nil else { - return - } - - selectInterestsVC.view.frame = self.view.bounds - self.add(selectInterestsVC) - - selectInterestsVC.didSaveInterests = { [weak self] _ in - guard let self else { - return - } - self.hideSelectInterestsView() - } - } - - func hideSelectInterestsView(showLoadingStream: Bool = true) { - guard selectInterestsVC.parent != nil else { - if shouldForceRefresh { - scrollViewToTop() - displayLoadingStream() - syncIfAppropriate(forceSync: true) - shouldForceRefresh = false - } - - return - } - - scrollViewToTop() - displayLoadingStream() - syncIfAppropriate(forceSync: true) - - UIView.animate(withDuration: 0.2, animations: { - self.selectInterestsVC.view.alpha = 0 - }) { _ in - self.selectInterestsVC.remove() - self.selectInterestsVC.view.alpha = 1 - } - } - func hideResultsStatus() { - hideSelectInterestsView() emptyStateView = nil footerView.isHidden = false tableView.tableHeaderView?.isHidden = false diff --git a/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderSelectInterestsViewController.swift b/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderSelectInterestsViewController.swift index ce1094822be0..de283bd212d9 100644 --- a/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderSelectInterestsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Select Interests/ReaderSelectInterestsViewController.swift @@ -136,11 +136,6 @@ class ReaderSelectInterestsViewController: UIViewController { saveSelectedInterests() } - // MARK: - Display logic - func userIsFollowingTopics(completion: @escaping (Bool) -> Void) { - coordinator.isFollowingInterests(completion: completion) - } - // MARK: - Private: Configuration private func configureCollectionView() { let nib = UINib(nibName: String(describing: ReaderInterestsCollectionViewCell.self), bundle: nil) diff --git a/WordPress/WordPressTest/ReaderCardServiceTests.swift b/WordPress/WordPressTest/ReaderCardServiceTests.swift index 4ea5f9dadc34..ca0e3e8afbb3 100644 --- a/WordPress/WordPressTest/ReaderCardServiceTests.swift +++ b/WordPress/WordPressTest/ReaderCardServiceTests.swift @@ -15,21 +15,6 @@ class ReaderCardServiceTests: CoreDataTestCase { remoteService = ReaderPostServiceRemoteMock() } - /// Returns an error if the user don't follow any Interest - /// - func testReturnErrorWhenNotFollowingAnyInterest() { - let expectation = self.expectation(description: "Error when now following interests") - let service = ReaderCardService(service: remoteService, coreDataStack: contextManager, followedInterestsService: followedInterestsService) - followedInterestsService.returnInterests = false - - service.fetch(isFirstPage: true, success: { _, _ in }, failure: { error in - expect(error).toNot(beNil()) - expectation.fulfill() - }) - - waitForExpectations(timeout: 5, handler: nil) - } - /// Save 10 cards in the database /// The API returns 11, but one of them is unknown and shouldn't be saved ///