Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reader: Local Default Topics #23788

Draft
wants to merge 8 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions WordPress/Classes/Models/ReaderDefaultTopic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,26 @@ import Foundation
override open class var TopicType: String {
return "default"
}

/// - note: Starting with 25.5, the app no longer uses the default topics
/// fetched from `/read/menu`. Instead, it uses the topics created locally.
/// The only reason these topics are needed is to filter entities in Core Data.
static func make(path: Path, in context: NSManagedObjectContext) throws -> ReaderDefaultTopic {
if let topic = context.firstObject(ofType: ReaderDefaultTopic.self, matching: NSPredicate(format: "path == %@", path.rawValue)) {
return topic
}
let topic = ReaderDefaultTopic(entity: ReaderDefaultTopic.entity(), insertInto: context)
topic.type = TopicType
topic.path = path.rawValue
topic.title = ""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a required field but it's never used only by other topic types.

try context.save()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use performAndSave instead of saving the context directly. Or, at bare minimal, use ContextManager.save.

Also, we don't update and save the main context.

return topic
}

enum Path: String {
// These are arbitrary values just to identify the entites.
case recommended = "/jpios/discover/recommended"
case firstPosts = "/jpios/discover/first-posts"
case latest = "/jpios/discover/latest"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean there should be three unique "default topics" (a.k.a "Discover" topics?) in Core Data? If that's the case, should we delete -[ReaderTopicService defaultTopicForRemoteTopic:inContext:] to avoid arbitrary ReaderDefaultTopic being created?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two that are still remaining: one for "Recent" and one for "Likes", so I can't remove them just yet. The "Discover" topic will also be in the database for now, but the app never displays them anywhere, so it's going to be fine. My plan is to ultimately remove ReaderDefaultTopic entirely. There are no good reasons to store them in Core Data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, this PR also fixes the following issue:

Steps

  • Open Discover
  • Select "Latest Posts" and wait until they load
  • Terminate the app
  • Reopen Discover (it shows "Recommended" again)

Expected result:

  • It loads new posts

Actual result:

  • It shows posts cached for "Latest Posts", then reloads

}
6 changes: 0 additions & 6 deletions WordPress/Classes/Services/ReaderTopicService.h
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
#import <Foundation/Foundation.h>
#import "CoreDataService.h"

extern NSString * const ReaderTopicFreshlyPressedPathCommponent;

@class ReaderAbstractTopic;
@class ReaderTagTopic;
@class ReaderSiteTopic;
@class ReaderSearchTopic;

@interface ReaderTopicService : CoreDataService

- (ReaderAbstractTopic *)currentTopicInContext:(NSManagedObjectContext *)context;

- (void)setCurrentTopic:(ReaderAbstractTopic *)topic;

/**
Fetches the topics for the reader's menu.

Expand Down
88 changes: 1 addition & 87 deletions WordPress/Classes/Services/ReaderTopicService.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
@import WordPressKit;
@import WordPressShared;

NSString * const ReaderTopicFreshlyPressedPathCommponent = @"freshly-pressed";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer used.

static NSString * const ReaderTopicCurrentTopicPathKey = @"ReaderTopicCurrentTopicPathKey";

@implementation ReaderTopicService

- (void)fetchReaderMenuWithSuccess:(void (^)(void))success failure:(void (^)(NSError *error))failure
Expand Down Expand Up @@ -43,73 +40,6 @@ - (void)fetchReaderMenuWithSuccess:(void (^)(void))success failure:(void (^)(NSE
} completion:nil onQueue:dispatch_get_main_queue()];
}

- (ReaderAbstractTopic *)currentTopicInContext:(NSManagedObjectContext *)context
{
ReaderAbstractTopic *topic;
topic = [self currentTopicFromSavedPathInContext:context];

if (!topic) {
topic = [self currentTopicFromDefaultTopicInContext:context];
[self setCurrentTopic:topic];
}

return topic;
}

- (ReaderAbstractTopic *)currentTopicFromSavedPathInContext:(NSManagedObjectContext *)context
{
ReaderAbstractTopic *topic;
NSString *topicPathString = [[UserPersistentStoreFactory userDefaultsInstance] stringForKey:ReaderTopicCurrentTopicPathKey];
if (topicPathString) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:[ReaderAbstractTopic classNameWithoutNamespaces]];
request.predicate = [NSPredicate predicateWithFormat:@"path = %@", topicPathString];

NSError *error;
topic = [[context executeFetchRequest:request error:&error] firstObject];
if (error) {
DDLogError(@"%@ error fetching topic: %@", NSStringFromSelector(_cmd), error);
}
}
return topic;
}

- (ReaderAbstractTopic *)currentTopicFromDefaultTopicInContext:(NSManagedObjectContext *)context
{
// Return the default topic
ReaderAbstractTopic *topic;
NSError *error;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:[ReaderDefaultTopic classNameWithoutNamespaces]];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES];
request.sortDescriptors = @[sortDescriptor];
NSArray *topics = [context executeFetchRequest:request error:&error];
if (error) {
DDLogError(@"%@ error fetching topic: %@", NSStringFromSelector(_cmd), error);
return nil;
}

if ([topics count] == 0) {
return nil;
}

NSArray *matches = [topics filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"path CONTAINS[cd] %@", ReaderTopicFreshlyPressedPathCommponent]];
if ([matches count]) {
topic = matches[0];
} else {
topic = topics[0];
}

return topic;
}

- (void)setCurrentTopic:(ReaderAbstractTopic *)topic
{
if (!topic) {
[[UserPersistentStoreFactory userDefaultsInstance] removeObjectForKey:ReaderTopicCurrentTopicPathKey];
} else {
[[UserPersistentStoreFactory userDefaultsInstance] setObject:topic.path forKey:ReaderTopicCurrentTopicPathKey];
}
}

- (void)deleteAllSearchTopics
{
[self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) {
Expand Down Expand Up @@ -176,7 +106,6 @@ - (void)clearInUseFlags
- (void)deleteAllTopics
{
[self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) {
[self setCurrentTopic:nil];
NSArray *currentTopics = [ReaderAbstractTopic lookupAllInContext:context error:nil];
for (ReaderAbstractTopic *topic in currentTopics) {
DDLogInfo(@"Deleting topic: %@", topic.title);
Expand Down Expand Up @@ -305,8 +234,6 @@ - (void)followTagNamed:(NSString *)topicName
NSDictionary *properties = @{@"tag":topicName, @"source":source};
[WPAnalytics trackReaderStat:WPAnalyticsStatReaderTagFollowed properties:properties];
[self.coreDataStack performAndSaveUsingBlock:^(NSManagedObjectContext *context) {
[self selectTopicWithID:topicID inContext:context];

// mark tag topic as followed
ReaderTagTopic *tag = [ReaderTagTopic lookupWithTagID:topicID inContext:context];
if (tag) {
Expand Down Expand Up @@ -597,16 +524,6 @@ - (WordPressComRestApi *)apiForRequest
return api;
}

/**
Finds an existing topic matching the specified topicID and, if found, makes it the
selected topic.
*/
- (void)selectTopicWithID:(NSNumber *)topicID inContext:(NSManagedObjectContext *)context
{
ReaderAbstractTopic *topic = [ReaderTagTopic lookupWithTagID:topicID inContext:context];
[self setCurrentTopic:topic];
}

/**
Create a new `ReaderAbstractTopic` or update an existing `ReaderAbstractTopic`.

Expand Down Expand Up @@ -862,10 +779,7 @@ - (void)mergeMenuTopics:(NSArray *)topics isLoggedIn:(BOOL)isLoggedIn withSucces
if ([currentTopics count] > 0) {
for (ReaderAbstractTopic *topic in currentTopics) {
if (![topic isKindOfClass:[ReaderSiteTopic class]] && ![topicsToKeep containsObject:topic]) {

if ([topic isEqual:[self currentTopicInContext:context]]) {
self.currentTopic = nil;
}

if (topic.inUse) {
if (!ReaderHelpers.isLoggedIn && [topic isKindOfClass:ReaderTagTopic.class]) {
DDLogInfo(@"Not unfollowing a locally saved topic: %@", topic.title);
Expand Down
21 changes: 13 additions & 8 deletions WordPress/Classes/System/Root View/ReaderPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,20 @@ final class ReaderPresenter: NSObject, SplitViewDisplayable {

private func makeViewController(for screen: ReaderStaticScreen) -> UIViewController {
switch screen {
case .recent, .discover, .likes:
if let topic = screen.topicType.flatMap(sidebarViewModel.getTopic) {
if screen == .discover {
return ReaderDiscoverViewController(topic: topic)
} else {
return ReaderStreamViewController.controllerWithTopic(topic)
}
case .discover:
return ReaderDiscoverViewController()
case .recent:
// TODO: (tech debt) Fix an issue where this fails if opened before the menus are fetched for the first time
if let topic = sidebarViewModel.getRecentTopic() {
return ReaderStreamViewController.controllerWithTopic(topic)
} else {
return makeErrorViewController()
}
case .likes:
if let topic = sidebarViewModel.getLikesTopic() {
return ReaderStreamViewController.controllerWithTopic(topic)
} else {
return makeErrorViewController() // This should never happen
return makeErrorViewController()
}
case .saved:
return ReaderStreamViewController.controllerForContentType(.saved)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@ import WordPressShared
class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDelegate {
private let headerView = ReaderDiscoverHeaderView()
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<ReaderTagTopic>
private let viewContext: NSManagedObjectContext
private var cancellables: [AnyCancellable] = []

init(topic: ReaderAbstractTopic) {
wpAssert(ReaderHelpers.topicIsDiscover(topic))
init() {
self.viewContext = ContextManager.shared.mainContext
self.topic = topic
self.tags = ManagedObjectsObserver(
predicate: ReaderSidebarTagsSection.predicate,
sortDescriptors: [SortDescriptor(\.title, order: .forward)],
Expand All @@ -40,6 +37,8 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe
configureStream(for: selectedChannel)

showSelectInterestsIfNeeded()

WPAnalytics.trackReaderEvent(.readerDiscoverShown, properties: [:])
}

private func setupNavigation() {
Expand All @@ -66,24 +65,32 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe
// MARK: - Selected Stream

private func configureStream(for channel: ReaderDiscoverChannel) {
showStreamViewController(makeViewController(for: channel))
do {
showStreamViewController(try makeViewController(for: channel))
} catch {
wpAssertionFailure("failed to show stream", userInfo: ["error": "\(error)"])
}
}

private func makeViewController(for channel: ReaderDiscoverChannel) -> ReaderStreamViewController {
private func makeViewController(for channel: ReaderDiscoverChannel) throws -> ReaderStreamViewController {
switch channel {
case .recommended:
ReaderDiscoverStreamViewController(topic: topic)
ReaderDiscoverStreamViewController(topic: try makeTopic(path: .recommended))
case .firstPosts:
ReaderDiscoverStreamViewController(topic: topic, stream: .firstPosts, sorting: .date)
ReaderDiscoverStreamViewController(topic: try makeTopic(path: .firstPosts), stream: .firstPosts, sorting: .date)
case .latest:
ReaderDiscoverStreamViewController(topic: topic, sorting: .date)
ReaderDiscoverStreamViewController(topic: try makeTopic(path: .latest), sorting: .date)
case .dailyPrompts:
ReaderStreamViewController.controllerWithTagSlug(ReaderTagTopic.dailyPromptTag)
case .tag(let tag):
ReaderStreamViewController.controllerWithTopic(tag)
}
}

private func makeTopic(path: ReaderDefaultTopic.Path) throws -> ReaderDefaultTopic {
try ReaderDefaultTopic.make(path: path, in: viewContext)
}

private func showStreamViewController(_ streamVC: ReaderStreamViewController) {
if let currentVC = self.streamVC {
deleteCachedReaderCards()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,28 +106,6 @@ struct ReaderNotificationKeys {
return topic.isKind(of: ReaderSearchTopic.self)
}

/// Check if the specified topic is for Freshly Pressed
///
/// - Parameters:
/// - topic: A ReaderAbstractTopic
///
/// - Returns: True if the topic is for Freshly Pressed
///
@objc open class func topicIsFreshlyPressed(_ topic: ReaderAbstractTopic) -> Bool {
return topic.path.hasSuffix("/freshly-pressed")
}

/// Check if the specified topic is for Discover
///
/// - Parameters:
/// - topic: A ReaderAbstractTopic
///
/// - Returns: True if the topic is for Discover
///
@objc open class func topicIsDiscover(_ topic: ReaderAbstractTopic) -> Bool {
return topic.path.contains("/read/sites/53424024/posts")
}

/// Check if the specified topic is for Following
///
/// - Parameters:
Expand Down Expand Up @@ -155,10 +133,7 @@ struct ReaderNotificationKeys {
class func trackLoadedTopic(_ topic: ReaderAbstractTopic, withProperties properties: [AnyHashable: Any]) {
var stat: WPAnalyticsStat?

if topicIsFreshlyPressed(topic) {
stat = .readerFreshlyPressedLoaded

} else if topicIsFollowing(topic) {
if topicIsFollowing(topic) {
WPAnalytics.trackReader(.readerFollowingShown, properties: properties)

} else if topicIsLiked(topic) {
Expand All @@ -167,16 +142,11 @@ struct ReaderNotificationKeys {
} else if isTopicSite(topic) {
WPAnalytics.trackReader(.readerBlogPreviewed, properties: properties)

} else if isTopicDefault(topic) && topicIsDiscover(topic) {
// Tracks Discover only if it was one of the default menu items.
WPAnalytics.trackReaderEvent(.readerDiscoverShown, properties: properties)

} else if isTopicList(topic) {
stat = .readerListLoaded

} else if isTopicTag(topic) {
stat = .readerTagLoaded

} else if let teamTopic = topic as? ReaderTeamTopic {
WPAnalytics.trackReader(teamTopic.shownTrackEvent, properties: properties)
}
Expand Down Expand Up @@ -260,38 +230,6 @@ struct ReaderNotificationKeys {
Blog.lookup(withID: siteID, in: ContextManager.sharedInstance().mainContext)?.isAdmin ?? false
}

// convenience method that returns the topic type
class func topicType(_ topic: ReaderAbstractTopic?) -> ReaderTopicType {
guard let topic = topic else {
return .noTopic
}
if topicIsDiscover(topic) {
return .discover
}
if topicIsFollowing(topic) {
return .following
}
if topicIsLiked(topic) {
return .likes
}
if isTopicList(topic) {
return .list
}
if isTopicSearchTopic(topic) {
return .search
}
if isTopicSite(topic) {
return .site
}
if isTopicTag(topic) {
return .tag
}
if topic is ReaderTeamTopic {
return .organization
}
return .noTopic
}

// MARK: Logged in helper

@objc open class func isLoggedIn() -> Bool {
Expand Down Expand Up @@ -527,19 +465,6 @@ struct ReaderNotificationKeys {
}
}

/// Typed topic type
enum ReaderTopicType {
case discover
case following
case likes
case list
case search
case site
case tag
case organization
case noTopic
}

@objc enum SiteOrganizationType: Int {
// site does not belong to an organization
case none
Expand Down
Loading