Skip to content

Commit 51c1fa4

Browse files
authored
Add "Email to Subscribers" row to "Publishing" sheet (#24946)
* Add jetpackNewsletterEmailDisabled to PostMetadata * Remove unused encodeMetadata * Rename PostMetadataContainer * Add PostMetadata * Integrate PostMetadataContainer * Add email-sbubscribers row * Update label * Update release notes * Reverse the toggle * Send a string * Add an assertion that only strings are supported
1 parent f47db39 commit 51c1fa4

File tree

11 files changed

+521
-398
lines changed

11 files changed

+521
-398
lines changed

Modules/Sources/WordPressKit/RemotePostParameters.swift

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -233,26 +233,6 @@ struct RemotePostCreateParametersWordPressComEncoder: Encodable {
233233
try container.encode(parameters.isSticky, forKey: .isSticky)
234234
}
235235
}
236-
237-
// - warning: fixme
238-
static func encodeMetadata(_ metadata: Set<RemotePostMetadataItem>) -> [[String: Any]] {
239-
metadata.map { item in
240-
var operation = "update"
241-
if item.key == nil {
242-
if item.id != nil && item.value == nil {
243-
operation = "delete"
244-
} else if item.id == nil && item.value != nil {
245-
operation = "add"
246-
}
247-
}
248-
var dictionary: [String: Any] = [:]
249-
if let id = item.id { dictionary["id"] = id }
250-
if let value = item.value { dictionary["value"] = value }
251-
if let key = item.key { dictionary["key"] = key }
252-
dictionary["operation"] = operation
253-
return dictionary
254-
}
255-
}
256236
}
257237

258238
struct RemotePostUpdateParametersWordPressComMetadata: Encodable {

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
26.5
22
-----
33
* [*] Add "Access" section to "Post Settings" [#24942]
4+
* [*] Add "Email to Subscribers" row to "Publishing" sheet [#24946]
45

56
26.4
67
-----

Sources/WordPressData/Swift/PostHelper+Metadata.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ extension PostHelper {
2323

2424
public static func mapDictionaryToMetadataItems(_ dictionary: [String: Any]) -> RemotePostMetadataItem? {
2525
let id = dictionary["id"]
26+
let value = dictionary["value"]
27+
if let value {
28+
wpAssert(value is String, "only strings are currently supported by WordPressKit")
29+
}
2630
return RemotePostMetadataItem(
2731
id: (id as? String) ?? (id as? NSNumber)?.stringValue,
2832
key: dictionary["key"] as? String,
29-
value: dictionary["value"] as? String
33+
value: value as? String
3034
)
3135
}
3236

Lines changed: 41 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,62 @@
11
import Foundation
2-
import WordPressKit
32
import WordPressShared
43

5-
/// A convenience struct that provides CRUD operations on post metadata.
6-
///
7-
/// ## WordPress Metadata Overview
8-
///
9-
/// WordPress stores custom metadata as key-value pairs associated with posts.
10-
/// Each metadata item contains a string key, a value (which can be any
11-
/// JSON-serializable type), and an optional ID for database tracking.
12-
///
13-
/// ## Expected Format
14-
///
15-
/// Metadata is stored as a JSON array of dictionaries, where each dictionary represents one
16-
/// metadata item:
17-
///
18-
/// ```json
19-
/// [
20-
/// {
21-
/// "key": "_jetpack_newsletter_access",
22-
/// "value": "subscribers",
23-
/// "id": "123"
24-
/// },
25-
/// {
26-
/// "key": "custom_field",
27-
/// "value": "some value"
28-
/// }
29-
/// ]
30-
/// ```
31-
public struct PostMetadata {
32-
public struct Key: ExpressibleByStringLiteral, Hashable {
33-
public let rawValue: String
34-
35-
public init(rawValue: String) {
36-
self.rawValue = rawValue
37-
}
38-
39-
public init(stringLiteral value: String) {
40-
self.rawValue = value
41-
}
42-
}
43-
44-
enum Error: Swift.Error {
45-
case invalidData
46-
}
47-
48-
// Raw JSON dictionaries, keyed by metadata key
49-
private var items: [Key: [String: Any]] = [:]
4+
/// Metadata used by the app.
5+
public struct PostMetadata: Hashable {
6+
/// Gets or sets the Jetpack Newsletter access level as a PostAccessLevel enum
7+
public var accessLevel: JetpackPostAccessLevel?
508

51-
/// Returns all metadata as a dictionary (alias for allItems)
52-
public var values: [[String: Any]] {
53-
Array(items.values)
54-
}
9+
/// Returns `true` if the post is configured to _not_ be sent in an email
10+
/// to subscribers.
11+
public var isJetpackNewsletterEmailDisabled: Bool
5512

5613
/// Initialized metadata with the given post.
5714
public init(_ post: AbstractPost) {
58-
if let data = post.rawMetadata {
59-
do {
60-
let metadata = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
61-
self = PostMetadata(metadata: metadata)
62-
} catch {
63-
wpAssertionFailure("Failed to decode metadata JSON", userInfo: ["error": error.localizedDescription])
64-
self = PostMetadata()
65-
}
66-
} else {
67-
self = PostMetadata()
68-
}
15+
self = PostMetadata(from: PostMetadataContainer(post))
6916
}
7017

71-
/// Initialize with raw metadata Data (non-throwing version for backward compatibility)
72-
/// If the data is invalid, creates an empty PostMetadata
73-
///
74-
/// - Parameter data: The JSON data containing metadata array
75-
public init(data: Data) throws {
76-
let metadata = try JSONSerialization.jsonObject(with: data)
77-
guard let dictionary = metadata as? [[String: Any]] else {
78-
throw Error.invalidData
79-
}
80-
self = PostMetadata(metadata: dictionary)
18+
public init(from container: PostMetadataContainer) {
19+
self.accessLevel = container.accessLevel
20+
self.isJetpackNewsletterEmailDisabled = container.getAdaptiveBool(for: .jetpackNewsletterEmailDisabled)
8121
}
8222

83-
/// Initialize with raw metadata array (same format as JSON data)
84-
/// - Parameter metadata: Array of metadata dictionaries with "key", "value", and optional "id"
85-
public init(metadata: [[String: Any]] = []) {
86-
for item in metadata {
87-
if let key = item["key"] as? String {
88-
self.items[Key(rawValue: key)] = item
89-
}
23+
/// Applies the metadata values to the container and returns them
24+
/// as metadata values.
25+
public func encode(in container: inout PostMetadataContainer) {
26+
let previous = PostMetadata(from: container)
27+
if previous.accessLevel != accessLevel {
28+
container.accessLevel = accessLevel
9029
}
91-
}
92-
93-
// MARK: - Encoding
94-
95-
/// Encodes the metadata back to Data for storage in rawMetadata
96-
/// - Returns: JSON Data representation of the metadata, or nil if empty
97-
public func encode() throws -> Data {
98-
do {
99-
return try JSONSerialization.data(withJSONObject: Array(items.values), options: [])
100-
} catch {
101-
wpAssertionFailure("Failed to encode metadata to JSON", userInfo: ["error": error.localizedDescription])
102-
throw error
30+
if previous.isJetpackNewsletterEmailDisabled != isJetpackNewsletterEmailDisabled {
31+
container.setValue(String(describing: isJetpackNewsletterEmailDisabled), for: .jetpackNewsletterEmailDisabled)
10332
}
10433
}
10534

106-
// MARK: - CRUD
107-
108-
/// Retrieves a metadata value by key with generic type casting
109-
/// - Parameters:
110-
/// - expectedType: The expected type of the value
111-
/// - key: The metadata key to search for
112-
/// - Returns: The value cast to the specified type if found and compatible, nil otherwise
113-
public func getValue<T>(_ expectedType: T.Type, forKey key: Key) -> T? {
114-
guard let dict = items[key], let value = dict["value"] else { return nil }
115-
guard let value = value as? T else {
116-
wpAssertionFailure("unexpected value", userInfo: [
117-
"key": key.rawValue,
118-
"actual_type": String(describing: expectedType),
119-
"expected_type": String(describing: type(of: value))
120-
])
121-
return nil
122-
}
123-
return value
124-
}
125-
126-
/// Retrieves a metadata value by key as String (convenience method)
127-
/// - Parameter key: The metadata key to search for
128-
/// - Returns: The value as String if found and convertible, nil otherwise
129-
public func getString(for key: Key) -> String? {
130-
getValue(String.self, forKey: key)
131-
}
132-
133-
/// Sets or updates a metadata item with any JSON-compatible value
134-
/// - Parameters:
135-
/// - value: The metadata value (must be JSON-compatible)
136-
/// - key: The metadata key
137-
/// - id: Optional metadata ID
138-
public mutating func setValue(_ value: Any, for key: Key, id: String? = nil) {
139-
var dict: [String: Any] = [
140-
"key": key.rawValue,
141-
"value": value
142-
]
143-
// Preserve existing ID if not provided
144-
if let id {
145-
dict["id"] = id
146-
} else if let existingDict = items[key], let existingID = existingDict["id"] {
147-
dict["id"] = existingID
148-
}
149-
guard JSONSerialization.isValidJSONObject(dict) else {
150-
return wpAssertionFailure("invalid value", userInfo: ["type": String(describing: type(of: value))])
151-
}
152-
items[key] = dict
153-
}
154-
155-
/// Removes a metadata item by key
156-
/// - Parameter key: The metadata key to remove
157-
/// - Returns: True if the item was found and removed, false otherwise
158-
@discardableResult
159-
public mutating func removeValue(for key: Key) -> Bool {
160-
items.removeValue(forKey: key) != nil
161-
}
162-
163-
/// Clears all metadata
164-
public mutating func clear() {
165-
items.removeAll()
166-
}
167-
168-
/// Returns the complete dictionary entry for the given key.
35+
/// Returns all metadata values encoded in `PostMetadataContainer` as
36+
/// WordPress metadata fields.
16937
///
170-
/// - Parameter key: The metadata key to retrieve
171-
/// - Returns: The complete metadata dictionary containing "key", "value", and optional "id", or nil if not found
172-
public func entry(forKey key: Key) -> [String: Any]? {
173-
return items[key]
38+
/// - note: It returns _only_ the fields managed by the app so that we
39+
/// don't send more than needed to the server when updating it.
40+
public static func entries(in container: PostMetadataContainer) -> [[String: Any]] {
41+
PostMetadata.allKeys.compactMap(container.entry)
17442
}
175-
}
17643

177-
// MARK: - PostMetadata (Jetpack)
44+
/// Returns all keys managed by the app.
45+
public static let allKeys: [PostMetadataContainer.Key] = [
46+
.jetpackNewsletterAccess,
47+
.jetpackNewsletterEmailDisabled
48+
]
49+
}
17850

179-
extension PostMetadata.Key {
180-
/// Jetpack Newsletter access level metadata key
181-
public static let jetpackNewsletterAccess: PostMetadata.Key = "_jetpack_newsletter_access"
51+
/// Valid access levels for Jetpack Newsletter
52+
public enum JetpackPostAccessLevel: String, CaseIterable, Hashable, Codable {
53+
case everybody = "everybody"
54+
case subscribers = "subscribers"
55+
case paidSubscribers = "paid_subscribers"
18256
}
18357

184-
extension PostMetadata {
185-
/// Gets or sets the Jetpack Newsletter access level as a PostAccessLevel enum
186-
public var accessLevel: JetpackPostAccessLevel? {
58+
private extension PostMetadataContainer {
59+
var accessLevel: JetpackPostAccessLevel? {
18760
get {
18861
guard let value = getString(for: .jetpackNewsletterAccess) else { return nil }
18962
return JetpackPostAccessLevel(rawValue: value)
@@ -198,9 +71,7 @@ extension PostMetadata {
19871
}
19972
}
20073

201-
/// Valid access levels for Jetpack Newsletter
202-
public enum JetpackPostAccessLevel: String, CaseIterable, Hashable, Codable {
203-
case everybody = "everybody"
204-
case subscribers = "subscribers"
205-
case paidSubscribers = "paid_subscribers"
74+
extension PostMetadataContainer.Key {
75+
static let jetpackNewsletterAccess: PostMetadataContainer.Key = "_jetpack_newsletter_access"
76+
static let jetpackNewsletterEmailDisabled: PostMetadataContainer.Key = "_jetpack_dont_email_post_to_subs"
20677
}

0 commit comments

Comments
 (0)