Skip to content
Open
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
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
27.0
-----

* [*] [internal] Jetpack Social: use new publicize API to support Jetpack Social [#25587]

26.9
-----
Expand Down
3 changes: 3 additions & 0 deletions Sources/WordPressData/Swift/Post+CoreDataProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import WordPressKit
public extension Post {

@NSManaged var commentCount: NSNumber?
// Deprecated: superseded by the connection_id-keyed PostSocialSharingDraft stored in post metadata.
@NSManaged var disabledPublicizeConnections: [NSNumber: [String: String]]?
@NSManaged var likeCount: NSNumber?
@NSManaged var postFormat: String?
@NSManaged var postType: String?
@NSManaged var publicID: String?
// Deprecated: superseded by the connection_id-keyed PostSocialSharingDraft stored in post metadata.
@NSManaged var publicizeMessage: String?
// Deprecated: superseded by the connection_id-keyed PostSocialSharingDraft stored in post metadata.
@NSManaged var publicizeMessageID: String?
@NSManaged var tags: String?
@NSManaged var categories: Set<PostCategory>?
Expand Down
54 changes: 36 additions & 18 deletions Sources/WordPressData/Swift/Post.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ public class Post: AbstractPost {
// MARK: - NSManagedObject

public override class func entityName() -> String {
return "Post"
"Post"
}

// MARK: - Format

@objc public func postFormatText() -> String? {
return blog.postFormatText(fromSlug: postFormat)
blog.postFormatText(fromSlug: postFormat)
}

@objc public func setPostFormatText(_ postFormatText: String) {
Expand Down Expand Up @@ -81,7 +81,7 @@ public class Post: AbstractPost {
return
}

let matchingCategories = blogCategories.filter({ return $0.categoryName == categoryName })
let matchingCategories = blogCategories.filter({ $0.categoryName == categoryName })

if !matchingCategories.isEmpty {
newCategories = newCategories.union(matchingCategories)
Expand All @@ -94,18 +94,22 @@ public class Post: AbstractPost {
// MARK: - Sharing

@objc public func canEditPublicizeSettings() -> Bool {
return !self.hasRemote() || self.status != .publish
!self.hasRemote() || self.status != .publish
}

// MARK: - PublicizeConnections

// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata.
// Kept to avoid a Core Data migration and for remaining legacy references.
@objc public func publicizeConnectionDisabledForKeyringID(_ keyringID: NSNumber) -> Bool {
let isKeyringEntryDisabled = disabledPublicizeConnections?[keyringID]?[Constants.publicizeValueKey] == Constants.publicizeDisabledValue
let isKeyringEntryDisabled =
disabledPublicizeConnections?[keyringID]?[Constants.publicizeValueKey] == Constants.publicizeDisabledValue

// try to check in case there's an entry for the PublicizeConnection that's keyed by the connectionID.
guard let connections = blog.connections,
let connection = connections.first(where: { $0.keyringConnectionID == keyringID }),
let existingValue = disabledPublicizeConnections?[connection.connectionID]?[Constants.publicizeValueKey] else {
let connection = connections.first(where: { $0.keyringConnectionID == keyringID }),
let existingValue = disabledPublicizeConnections?[connection.connectionID]?[Constants.publicizeValueKey]
else {
// fall back to keyringID if there is no such entry with the connectionID.
return isKeyringEntryDisabled
}
Expand All @@ -114,30 +118,37 @@ public class Post: AbstractPost {
return isConnectionEntryDisabled || isKeyringEntryDisabled
}

// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata.
// Kept to avoid a Core Data migration and for remaining legacy references.
public func enablePublicizeConnectionWithKeyringID(_ keyringID: NSNumber) {
// if there's another entry keyed by connectionID references to the same connection,
// we need to make sure that the values are kept in sync.
if let connections = blog.connections,
let connection = connections.first(where: { $0.keyringConnectionID == keyringID }),
let _ = disabledPublicizeConnections?[connection.connectionID] {
let connection = connections.first(where: { $0.keyringConnectionID == keyringID }),
let _ = disabledPublicizeConnections?[connection.connectionID]
{
enablePublicizeConnection(keyedBy: connection.connectionID)
}

enablePublicizeConnection(keyedBy: keyringID)
}

// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata.
// Kept to avoid a Core Data migration and for remaining legacy references.
public func disablePublicizeConnectionWithKeyringID(_ keyringID: NSNumber) {
// if there's another entry keyed by connectionID references to the same connection,
// we need to make sure that the values are kept in sync.
if let connections = blog.connections,
let connectionID = connections.first(where: { $0.keyringConnectionID == keyringID })?.connectionID,
let _ = disabledPublicizeConnections?[connectionID] {
let connectionID = connections.first(where: { $0.keyringConnectionID == keyringID })?.connectionID,
let _ = disabledPublicizeConnections?[connectionID]
{
disablePublicizeConnection(keyedBy: connectionID)

// additionally, if the keyring entry doesn't exist, there's no need create both formats.
// we can just update the dictionary's key from connectionID to keyringID instead.
if disabledPublicizeConnections?[keyringID] == nil,
let updatedEntry = disabledPublicizeConnections?[connectionID] {
let updatedEntry = disabledPublicizeConnections?[connectionID]
{
disabledPublicizeConnections?.removeValue(forKey: connectionID)
disabledPublicizeConnections?[keyringID] = updatedEntry
return
Expand All @@ -150,6 +161,7 @@ public class Post: AbstractPost {
/// Marks the Publicize connection with the given id as enabled.
///
/// - Parameter id: The dictionary key for `disabledPublicizeConnections`.
// Deprecated: helper for keyring-keyed publicize code kept for remaining legacy references.
private func enablePublicizeConnection(keyedBy id: NSNumber) {
guard var connection = disabledPublicizeConnections?[id] else {
return
Expand All @@ -169,6 +181,7 @@ public class Post: AbstractPost {
/// Marks the Publicize connection with the given id as disabled.
///
/// - Parameter id: The dictionary key for `disabledPublicizeConnections`.
// Deprecated: helper for keyring-keyed publicize code kept for remaining legacy references.
private func disablePublicizeConnection(keyedBy id: NSNumber) {
if let _ = disabledPublicizeConnections?[id] {
disabledPublicizeConnections?[id]?[Constants.publicizeValueKey] = Constants.publicizeDisabledValue
Expand All @@ -185,13 +198,13 @@ public class Post: AbstractPost {
// MARK: - Comments

@objc public func numberOfComments() -> Int {
return commentCount?.intValue ?? 0
commentCount?.intValue ?? 0
}

// MARK: - Likes

@objc public func numberOfLikes() -> Int {
return likeCount?.intValue ?? 0
likeCount?.intValue ?? 0
}

// MARK: - AbstractPost
Expand All @@ -209,7 +222,7 @@ public class Post: AbstractPost {
}

public func dateForDisplay() -> Date? {
return dateCreated
dateCreated
}

// MARK: - BasePost
Expand All @@ -226,7 +239,8 @@ public class Post: AbstractPost {
if let preview = PostPreviewCache.shared.content[content] {
return preview
}
let preview = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 200).withCollapsedNewlines().trimmedForPreview()
let preview = GutenbergExcerptGenerator.firstParagraph(from: content, maxLength: 200)
.withCollapsedNewlines().trimmedForPreview()
PostPreviewCache.shared.content[content] = preview
return preview
} else {
Expand All @@ -236,12 +250,16 @@ public class Post: AbstractPost {

override public func titleForDisplay() -> String {
var title = postTitle?.trimmingCharacters(in: CharacterSet.whitespaces) ?? ""
title = title
title =
title
.stringByDecodingXMLCharacters()
.strippingHTML()

if title.isEmpty && !hasRemote() && contentPreviewForDisplay().isEmpty {
title = NSLocalizedString("(no title)", comment: "Lets a user know that a local draft does not have a title.")
title = NSLocalizedString(
"(no title)",
comment: "Lets a user know that a local draft does not have a title."
)
}

return title
Expand Down
26 changes: 26 additions & 0 deletions Tests/KeystoneTests/Tests/Features/Posts/PostSettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,32 @@ struct PostSettingsTests {
#expect(post.status == .publish) // Changed
}

@Test("apply preserves stored publicize metadata when social draft is unavailable")
func applyPreservesStoredPublicizeMetadataWhenSocialDraftIsUnavailable() throws {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).build()
post.rawMetadata = try PostMetadataContainer(metadata: [
["key": "_wpas_mess", "value": "Hello", "id": "1"],
["key": "_wpas_skip_publicize_111", "value": "1", "id": "2"],
["key": "_jetpack_newsletter_access", "value": "everybody", "id": "3"]
])
.encode()

var settings = PostSettings(from: post)
settings.socialSharingDraft = nil

settings.apply(to: post)

// With no draft to apply, the existing publicize metadata is left untouched
// (the user's per-connection choices are preserved, not neutralized).
let container = PostMetadataContainer(post)
#expect(container.getString(for: "_wpas_mess") == "Hello")
#expect(container.getString(for: "_wpas_skip_publicize_111") == "1")
#expect(container.entry(forKey: "_wpas_skip_publicize_111")?["id"] as? String == "2")
#expect(container.getString(for: "_jetpack_newsletter_access") == "everybody")
}

// MARK: - makeUpdateParameters Tests

@Test("Creates update parameters for changed properties")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import JetpackSocial
import Testing
@testable import WordPress
@testable import WordPressData

@MainActor
@Suite("PostSettingsViewModel Tests")
struct PostSettingsViewModelTests {
@Test("publish settings preserve publishing fields and strip the social draft when no connections service")
func publishSettingsPreservePublishingFieldsAndStripDraftWithoutConnectionsService() {
let context = ContextManager.forTesting().mainContext
// A plain blog has no WP.com account, so it isn't Publicize-eligible and
// the view model resolves no connections service. The draft is therefore
// stripped (the strip is driven by the missing service, not the status).
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).drafted().build()
let viewModel = PostSettingsViewModel(post: post, context: .publishing)
let publishDate = Date(timeIntervalSince1970: 2_000)

viewModel.settings.status = .publishPrivate
viewModel.settings.password = "secret"
viewModel.settings.publishDate = publishDate
viewModel.settings.socialSharingDraft = PostSocialSharingDraft(customMessage: "Message")

let settings = viewModel.getSettingsToPublish(for: viewModel.settings)

#expect(settings.status == .publishPrivate)
#expect(settings.password == "secret")
#expect(settings.publishDate == publishDate)
#expect(settings.socialSharingDraft == nil)
}

@Test("a private post keeps its social draft so disabled connections survive being made public")
func privateEligiblePostKeepsSocialDraft() {
let context = ContextManager.forTesting().mainContext
// Publicize-eligible blog: WP.com-hosted, with an account and publish
// capability, so the view model resolves a connections service.
let blog = BlogBuilder(context)
.isHostedAtWPcom()
.withAnAccount()
.with(capabilities: [.publishPosts])
.build()
let post = PostBuilder(context, blog: blog).drafted().build()
let viewModel = PostSettingsViewModel(post: post, context: .publishing)

let draft = PostSocialSharingDraft(connectionsByID: ["123": .init(id: "123", enabled: false)])
viewModel.settings.status = .publishPrivate
viewModel.settings.socialSharingDraft = draft

// The draft is retained for a private post: private posts aren't publicized,
// but the disabled connection must survive in case the post is later made public.
#expect(viewModel.getSettingsToPublish(for: viewModel.settings).socialSharingDraft == draft)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Testing
@testable import WordPress
@testable import WordPressData

@Suite("RemotePostCreateParameters Tests")
struct RemotePostCreateParametersTests {
Expand Down Expand Up @@ -40,6 +41,20 @@ struct RemotePostCreateParametersTests {
#expect(parameters.isSticky == true)
}

@Test("Initialization from Post includes social sharing metadata from raw metadata")
func initializationFromPostIncludesSocialSharingMetadataFromRawMetadata() throws {
let post = Post(context: mainContext)
var metadata = PostMetadataContainer()
metadata.setValue("message-a", for: "_wpas_mess")
metadata.setValue("1", for: "_wpas_skip_publicize_123")
post.rawMetadata = try metadata.encode()

let parameters = RemotePostCreateParameters(post: post)

#expect(parameters.metadata.contains(RemotePostMetadataItem(id: nil, key: "_wpas_mess", value: "message-a")))
#expect(parameters.metadata.contains(RemotePostMetadataItem(id: nil, key: "_wpas_skip_publicize_123", value: "1")))
}

@Test("Direct metadata manipulation")
func directMetadataManipulation() {
var parameters = RemotePostCreateParameters(type: "post", status: "draft")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import OHHTTPStubs
import OHHTTPStubsSwift

@testable import WordPress
@testable import WordPressData

@MainActor
class PostCoordinatorTests: CoreDataTestCase {
Expand Down Expand Up @@ -435,7 +436,9 @@ class PostCoordinatorTests: CoreDataTestCase {

// GIVEN an editor revision
let revision = post.createRevision() as! Post
revision.publicizeMessage = "message-a"
var metadata = PostMetadataContainer()
metadata.setValue("message-a", for: "_wpas_mess")
revision.rawMetadata = try metadata.encode()

// GIVEN
stub(condition: isPath("/rest/v1.2/sites/80511/posts/974")) { request in
Expand Down
Loading