Skip to content
Open
16 changes: 12 additions & 4 deletions WordPressKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@
4A68E3D329406AA0004AC3DC /* RemoteMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3D029406AA0004AC3DC /* RemoteMenu.swift */; };
4A68E3D429406AA0004AC3DC /* RemoteMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3D129406AA0004AC3DC /* RemoteMenuItem.swift */; };
4A68E3D529406AA0004AC3DC /* RemoteMenuLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3D229406AA0004AC3DC /* RemoteMenuLocation.swift */; };
4A68E40B294922A8004AC3DC /* RemoteReaderPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E40A294922A8004AC3DC /* RemoteReaderPost.swift */; };
4A68E40D294930CC004AC3DC /* RemoteReaderPostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E40C294930CC004AC3DC /* RemoteReaderPostTests.swift */; };
4A68E3D729406DA2004AC3DC /* RemoteUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3D629406DA2004AC3DC /* RemoteUser.swift */; };
4A68E3D929406E0D004AC3DC /* RemoteTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3D829406E0D004AC3DC /* RemoteTheme.swift */; };
4A68E3DB29406EA0004AC3DC /* RemoteSourcePostAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3DA29406EA0004AC3DC /* RemoteSourcePostAttribution.swift */; };
Expand Down Expand Up @@ -346,7 +348,7 @@
82FFBF521F45F04100F4573F /* RemoteBlogJetpackMonitorSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82FFBF511F45F04100F4573F /* RemoteBlogJetpackMonitorSettings.swift */; };
82FFBF561F460DD400F4573F /* BlogJetpackSettingsServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82FFBF551F460DD400F4573F /* BlogJetpackSettingsServiceRemote.swift */; };
8B074A4E27AC2FFD003A2EB8 /* dashboard-400-invalid-card.json in Resources */ = {isa = PBXBuildFile; fileRef = 8B074A4D27AC2FFD003A2EB8 /* dashboard-400-invalid-card.json */; };
8B16CE8E25250039007BE5A9 /* RemoteReaderPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B16CE8D25250039007BE5A9 /* RemoteReaderPost.swift */; };
8B16CE8E25250039007BE5A9 /* ReaderPostsEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B16CE8D25250039007BE5A9 /* ReaderPostsEnvelope.swift */; };
8B16CE92252502C4007BE5A9 /* RemoteReaderPostTests+V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B16CE91252502C4007BE5A9 /* RemoteReaderPostTests+V2.swift */; };
8B16CE962525045F007BE5A9 /* reader-posts-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 8B16CE952525045F007BE5A9 /* reader-posts-success.json */; };
8B2F4BE524ABB3C70056C08A /* RemoteReaderPostTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B2F4BE424ABB3C70056C08A /* RemoteReaderPostTests.m */; };
Expand Down Expand Up @@ -775,6 +777,8 @@
4A68E3D029406AA0004AC3DC /* RemoteMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteMenu.swift; sourceTree = "<group>"; };
4A68E3D129406AA0004AC3DC /* RemoteMenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteMenuItem.swift; sourceTree = "<group>"; };
4A68E3D229406AA0004AC3DC /* RemoteMenuLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteMenuLocation.swift; sourceTree = "<group>"; };
4A68E40A294922A8004AC3DC /* RemoteReaderPost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteReaderPost.swift; sourceTree = "<group>"; };
4A68E40C294930CC004AC3DC /* RemoteReaderPostTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteReaderPostTests.swift; sourceTree = "<group>"; };
4A68E3D629406DA2004AC3DC /* RemoteUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteUser.swift; sourceTree = "<group>"; };
4A68E3D829406E0D004AC3DC /* RemoteTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteTheme.swift; sourceTree = "<group>"; };
4A68E3DA29406EA0004AC3DC /* RemoteSourcePostAttribution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteSourcePostAttribution.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -994,7 +998,7 @@
82FFBF511F45F04100F4573F /* RemoteBlogJetpackMonitorSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteBlogJetpackMonitorSettings.swift; sourceTree = "<group>"; };
82FFBF551F460DD400F4573F /* BlogJetpackSettingsServiceRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlogJetpackSettingsServiceRemote.swift; sourceTree = "<group>"; };
8B074A4D27AC2FFD003A2EB8 /* dashboard-400-invalid-card.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "dashboard-400-invalid-card.json"; sourceTree = "<group>"; };
8B16CE8D25250039007BE5A9 /* RemoteReaderPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReaderPost.swift; sourceTree = "<group>"; };
8B16CE8D25250039007BE5A9 /* ReaderPostsEnvelope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderPostsEnvelope.swift; sourceTree = "<group>"; };
8B16CE91252502C4007BE5A9 /* RemoteReaderPostTests+V2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RemoteReaderPostTests+V2.swift"; sourceTree = "<group>"; };
8B16CE952525045F007BE5A9 /* reader-posts-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "reader-posts-success.json"; sourceTree = "<group>"; };
8B2F4BE424ABB3C70056C08A /* RemoteReaderPostTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RemoteReaderPostTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1489,6 +1493,7 @@
7430C9BB1F192C0F0051B8E6 /* ReaderTopicServiceRemoteTests.m */,
17CE77F320C701C8001DEA5A /* ReaderSiteSearchServiceRemoteTests.swift */,
8B2F4BE424ABB3C70056C08A /* RemoteReaderPostTests.m */,
4A68E40C294930CC004AC3DC /* RemoteReaderPostTests.swift */,
8B2F4BE824ABC9DC0056C08A /* ReaderPostServiceRemote+CardsTests.swift */,
FACBDD3725ECB4480026705B /* ReaderPostServiceRemote+RelatedPostsTests.swift */,
FA87FE0624EB39C4003FBEE3 /* ReaderPostServiceRemote+SubscriptionTests.swift */,
Expand Down Expand Up @@ -1927,11 +1932,12 @@
74E2294F1F1E741B0085F7F2 /* RemotePublicizeConnection.swift */,
74E2294D1F1E73FE0085F7F2 /* RemotePublicizeService.swift */,
7430C9D61F1933200051B8E6 /* RemoteReaderCrossPostMeta.swift */,
8B16CE8D25250039007BE5A9 /* RemoteReaderPost.swift */,
8B16CE8D25250039007BE5A9 /* ReaderPostsEnvelope.swift */,
8B2F4BEE24ACCC120056C08A /* RemoteReaderCard.swift */,
8B2F4BF024ACE3C30056C08A /* RemoteReaderInterest.swift */,
7430C9A91F1927C50051B8E6 /* RemoteReaderPost.h */,
7430C9AA1F1927C50051B8E6 /* RemoteReaderPost.m */,
4A68E40A294922A8004AC3DC /* RemoteReaderPost.swift */,
FACBDD1D25ECA7F90026705B /* RemoteReaderSimplePost.swift */,
4A68E3DC294070A7004AC3DC /* RemoteReaderSite.swift */,
4A68E3E0294076C1004AC3DC /* RemoteReaderSiteInfo.swift */,
Expand Down Expand Up @@ -3006,7 +3012,7 @@
F9E56DF624EB11EF00916770 /* FeatureFlag.swift in Sources */,
8B2F4BE724ABC8A90056C08A /* ReaderPostServiceRemote+Cards.swift in Sources */,
4A68E3D529406AA0004AC3DC /* RemoteMenuLocation.swift in Sources */,
8B16CE8E25250039007BE5A9 /* RemoteReaderPost.swift in Sources */,
8B16CE8E25250039007BE5A9 /* ReaderPostsEnvelope.swift in Sources */,
74E2294E1F1E73FE0085F7F2 /* RemotePublicizeService.swift in Sources */,
8B749DED25AF3E4600023F03 /* JetpackCapabilitiesServiceRemote.swift in Sources */,
9F3E0BA22087345F009CB5BA /* ServiceRequest.swift in Sources */,
Expand All @@ -3026,6 +3032,7 @@
9F3E0B9E208733C3009CB5BA /* ReaderServiceDeliveryFrequency.swift in Sources */,
74E2295E1F1E777B0085F7F2 /* RemoteSharingButton.swift in Sources */,
93BD27701EE737A8002BB00B /* ServiceRemoteWordPressComREST.m in Sources */,
4A68E40B294922A8004AC3DC /* RemoteReaderPost.swift in Sources */,
E61A51A621B172A900A5F902 /* RemoteWpcomPlan.swift in Sources */,
93BD277F1EE73944002BB00B /* WordPressComOAuthClient.swift in Sources */,
740B23B91F17EC7300067A2A /* PostServiceRemoteREST.m in Sources */,
Expand Down Expand Up @@ -3235,6 +3242,7 @@
803DE81128FFA9C4007D4E9C /* RemoteConfigRemoteTests.swift in Sources */,
74B5F0DE1EF82A9600B411E7 /* BlogServiceRemoteRESTTests.m in Sources */,
ABD95B7F25DD6C4B00735BEE /* CommentServiceRemoteRESTLikesTests.swift in Sources */,
4A68E40D294930CC004AC3DC /* RemoteReaderPostTests.swift in Sources */,
8B749E8225AF7DDA00023F03 /* JetpackCapabilitiesServiceRemoteTests.swift in Sources */,
74E2294B1F1E73340085F7F2 /* SharingServiceRemoteTests.m in Sources */,
FEFFD99B26C1598F00F34231 /* ShareAppContentServiceRemoteTests.swift in Sources */,
Expand Down
17 changes: 17 additions & 0 deletions WordPressKit/ReaderPostsEnvelope.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
Copy link
Contributor

Choose a reason for hiding this comment

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

This import might be unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's true for the current project setup. But I think it's needed for SPM support.


struct ReaderPostsEnvelope: Decodable {
var posts: [RemoteReaderPost]
var nextPageHandle: String?

private enum CodingKeys: String, CodingKey {
case posts
case nextPageHandle = "next_page_handle"
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let postDictionary = try container.decode([String: Any].self, forKey: .posts)
posts = [RemoteReaderPost(dictionary: postDictionary)]
}
}
151 changes: 8 additions & 143 deletions WordPressKit/RemoteReaderPost.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
NSString * const PostRESTKeyDiscoverMetadata = @"discover_metadata";
NSString * const PostRESTKeyDiscussion = @"discussion";
NSString * const PostRESTKeyEditorial = @"editorial";
NSString * const PostRESTKeyEmail = @"email";
NSString * const PostRESTKeyExcerpt = @"excerpt";
NSString * const PostRESTKeyFeaturedMedia = @"featured_media";
NSString * const PostRESTKeyFeaturedImage = @"featured_image";
Expand All @@ -41,10 +40,6 @@
NSString * const PostRESTKeyScore = @"score";
NSString * const PostRESTKeySharingEnabled = @"sharing_enabled";
NSString * const PostRESTKeySiteID = @"site_ID";
NSString * const PostRESTKeySiteIsAtomic = @"site_is_atomic";
NSString * const PostRESTKeySiteIsPrivate = @"site_is_private";
NSString * const PostRESTKeySiteName = @"site_name";
NSString * const PostRESTKeySiteURL = @"site_URL";
NSString * const PostRESTKeySlug = @"slug";
NSString * const PostRESTKeyStatus = @"status";
NSString * const PostRESTKeyTitle = @"title";
Expand Down Expand Up @@ -74,8 +69,6 @@
NSString * const CrossPostMetaXPostOrigin = @"xpost_origin";
NSString * const CrossPostMetaCommentPrefix = @"comment-";

static const NSInteger AvgWordsPerMinuteRead = 250;
static const NSInteger MinutesToReadThreshold = 2;
static const NSUInteger ReaderPostTitleLength = 30;

@implementation RemoteReaderPost
Expand All @@ -95,12 +88,12 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict;
self.author = [self stringOrEmptyString:[authorDict stringForKey:PostRESTKeyNiceName]]; // typically the author's screen name
self.authorAvatarURL = [self stringOrEmptyString:[authorDict stringForKey:PostRESTKeyAvatarURL]];
self.authorDisplayName = [[self stringOrEmptyString:[authorDict stringForKey:PostRESTKeyName]] stringByDecodingXMLCharacters]; // Typically the author's given name
self.authorEmail = [self authorEmailFromAuthorDictionary:authorDict];
self.authorEmail = [RemoteReaderPost authorEmailFromAuthorDictionary:authorDict];
self.authorURL = [self stringOrEmptyString:[authorDict stringForKey:PostRESTKeyURL]];
self.siteIconURL = [self stringOrEmptyString:[dict stringForKeyPath:@"meta.data.site.icon.img"]];
self.blogName = [self siteNameFromPostDictionary:dict];
self.blogName = [RemoteReaderPost siteNameFromPostDictionary:dict];
self.blogDescription = [self siteDescriptionFromPostDictionary:dict];
self.blogURL = [self siteURLFromPostDictionary:dict];
self.blogURL = [RemoteReaderPost siteURLFromPostDictionary:dict];
self.commentCount = [discussionDict numberForKey:PostRESTKeyCommentCount];
self.commentsOpen = [[discussionDict numberForKey:PostRESTKeyCommentsOpen] boolValue];
self.content = [self postContentFromPostDictionary:dict];
Expand All @@ -109,12 +102,12 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict;
self.feedID = [dict numberForKey:PostRESTKeyFeedID];
self.feedItemID = [dict numberForKey:PostRESTKeyFeedItemID];
self.globalID = [self stringOrEmptyString:[dict stringForKey:PostRESTKeyGlobalID]];
self.isBlogAtomic = [self siteIsAtomicFromPostDictionary:dict];
self.isBlogPrivate = [self siteIsPrivateFromPostDictionary:dict];
self.isBlogAtomic = [RemoteReaderPost siteIsAtomicFromPostDictionary:dict];
self.isBlogPrivate = [RemoteReaderPost siteIsPrivateFromPostDictionary:dict];
self.isFollowing = [[dict numberForKey:PostRESTKeyIsFollowing] boolValue];
self.isLiked = [[dict numberForKey:PostRESTKeyILike] boolValue];
self.isReblogged = [[dict numberForKey:PostRESTKeyIsReblogged] boolValue];
self.isWPCom = [self isWPComFromPostDictionary:dict];
self.isWPCom = [RemoteReaderPost isWPComFromPostDictionary:dict];
self.likeCount = [dict numberForKey:PostRESTKeyLikeCount];
self.permalink = [self stringOrEmptyString:[dict stringForKey:PostRESTKeyURL]];
self.postID = [dict numberForKey:PostRESTKeyID];
Expand All @@ -125,7 +118,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict;
self.sortRank = @(self.sortDate.timeIntervalSinceReferenceDate);
self.status = [self stringOrEmptyString:[dict stringForKey:PostRESTKeyStatus]];
self.summary = [self postSummaryFromPostDictionary:dict orPostContent:self.content];
self.tags = [self tagsFromPostDictionary:dict];
self.tags = [RemoteReaderPost tagsFromPostDictionary:dict];
self.isSharingEnabled = [[dict numberForKey:PostRESTKeySharingEnabled] boolValue];
self.isLikesEnabled = [[dict numberForKey:PostRESTKeyLikesEnabled] boolValue];
self.organizationID = [dict numberForKeyPath:PostRESTKeyOrganizationID] ?: @0;
Expand Down Expand Up @@ -157,7 +150,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict;
self.isExternal = [[dict numberForKey:PostRESTKeyIsExternal] boolValue];
self.isJetpack = [[dict numberForKey:PostRESTKeyIsJetpack] boolValue];
self.wordCount = [dict numberForKey:PostRESTKeyWordCount];
self.readingTime = [self readingTimeForWordCount:self.wordCount];
self.readingTime = [RemoteReaderPost readingTimeForWordCount:self.wordCount];

NSDictionary *railcar = [dict dictionaryForKey:PostRESTKeyRailcar];
if (railcar) {
Expand Down Expand Up @@ -281,16 +274,6 @@ - (NSDictionary *)primaryAndSecondaryTagsFromPostDictionary:(NSDictionary *)dict
};
}

- (NSNumber *)readingTimeForWordCount:(NSNumber *)wordCount
{
NSInteger count = [wordCount integerValue];
NSInteger minutesToRead = count / AvgWordsPerMinuteRead;
if (minutesToRead < MinutesToReadThreshold) {
return @(0);
}
return @(minutesToRead);
}

/**
Composes discover attribution if needed.

Expand Down Expand Up @@ -394,55 +377,6 @@ - (NSString *)sanitizeFeaturedImageString:(NSString *)img

#pragma mark - Data sanitization methods

/**
The v1 API result is inconsistent in that it will return a 0 when there is no author email.

@param dict The author dictionary.
@return The author's email address or an empty string.
*/
- (NSString *)authorEmailFromAuthorDictionary:(NSDictionary *)dict
{
NSString *authorEmail = [dict stringForKey:PostRESTKeyEmail];

// if 0 or less than minimum email length. [email protected]
if ([authorEmail isEqualToString:@"0"] || [authorEmail length] < 6) {
authorEmail = @"";
}

return authorEmail;
}

/**
Parse whether the post belongs to a wpcom blog.

@param dict A dictionary representing a post object from the REST API
@return YES if the post belongs to a wpcom blog, else NO
*/
- (BOOL)isWPComFromPostDictionary:(NSDictionary *)dict
{
BOOL isExternal = [[dict numberForKey:PostRESTKeyIsExternal] boolValue];
BOOL isJetpack = [[dict numberForKey:PostRESTKeyIsJetpack] boolValue];

return !isJetpack && !isExternal;
}

/**
Get the tags assigned to a post and return them as a comma separated string.

@param dict A dictionary representing a post object from the REST API.
@return A comma separated list of tags, or an empty string if no tags are found.
*/
- (NSString *)tagsFromPostDictionary:(NSDictionary *)dict
{
NSDictionary *tagsDict = [dict dictionaryForKey:PostRESTKeyTags];
NSArray *tagsList = [NSArray arrayWithArray:[tagsDict allKeys]];
NSString *tags = [tagsList componentsJoinedByString:@", "];
if (tags == nil) {
tags = @"";
}
return tags;
}

/**
Get the date the post should be sorted by.

Expand Down Expand Up @@ -528,32 +462,6 @@ - (NSString *)suitableImageFromPostContent:(NSDictionary *)dict {
return [self stringOrEmptyString:imageToDisplay];
}

/**
Get the name of the post's site.

@param dict A dictionary representing a post object from the REST API.
@return The name of the post's site or an empty string.
*/
- (NSString *)siteNameFromPostDictionary:(NSDictionary *)dict
{
// Blog Name
NSString *siteName = [self stringOrEmptyString:[dict stringForKey:PostRESTKeySiteName]];

// For some endpoints blogname is defined in meta
NSString *metaBlogName = [dict stringForKeyPath:@"meta.data.site.name"];
if (metaBlogName != nil) {
siteName = metaBlogName;
}

// Values set in editorial trumps the rest
NSString *editorialSiteName = [dict stringForKeyPath:@"editorial.blog_name"];
if (editorialSiteName != nil) {
siteName = editorialSiteName;
}

return [self makePlainText:siteName];
}

/**
Get the description of the post's site.

Expand All @@ -566,24 +474,6 @@ - (NSString *)siteDescriptionFromPostDictionary:(NSDictionary *)dict
return [self makePlainText:description];
}

/**
Retrives the post site's URL

@param dict A dictionary representing a post object from the REST API.
@return The URL path of the post's site.
*/
- (NSString *)siteURLFromPostDictionary:(NSDictionary *)dict
{
NSString *siteURL = [self stringOrEmptyString:[dict stringForKey:PostRESTKeySiteURL]];

NSString *metaSiteURL = [dict stringForKeyPath:@"meta.data.site.URL"];
if (metaSiteURL != nil) {
siteURL = metaSiteURL;
}

return siteURL;
}

/**
Retrives the post content from results dictionary

Expand Down Expand Up @@ -623,31 +513,6 @@ - (NSString *)postSummaryFromPostDictionary:(NSDictionary *)dict orPostContent:(
return summary;
}

- (BOOL)siteIsAtomicFromPostDictionary:(NSDictionary *)dict
{
NSNumber *isAtomic = [dict numberForKey:PostRESTKeySiteIsAtomic];

return [isAtomic boolValue];
}

/**
Retrives the privacy preference for the post's site.

@param dict A dictionary representing a post object from the REST API.
@return YES if the site is private.
*/
- (BOOL)siteIsPrivateFromPostDictionary:(NSDictionary *)dict
{
NSNumber *isPrivate = [dict numberForKey:PostRESTKeySiteIsPrivate];

NSNumber *metaIsPrivate = [dict numberForKeyPath:@"meta.data.site.is_private"];
if (metaIsPrivate != nil) {
isPrivate = metaIsPrivate;
}

return [isPrivate boolValue];
}

- (NSArray *)slugsFromDiscoverPostTaxonomies:(NSArray *)discoverPostTaxonomies
{
return [discoverPostTaxonomies wp_map:^id(NSDictionary *dict) {
Expand Down
Loading