Skip to content
Merged
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
75 changes: 75 additions & 0 deletions Sources/WordPressData/Blog+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Foundation
import CoreData
import WordPressShared

extension Blog {

// MARK: - Post Formats

/// Returns an array of post format keys sorted with "standard" first, then alphabetically
@objc public var sortedPostFormats: [String] {
guard let postFormats = postFormats as? [String: String], !postFormats.isEmpty else {
return []
}

var sortedFormats: [String] = []

// Add standard format first if it exists
if postFormats[PostFormatStandard] != nil {
sortedFormats.append(PostFormatStandard)
}

// Add remaining formats sorted by their display names
let nonStandardFormats = postFormats
.filter { $0.key != PostFormatStandard }
.sorted { $0.value.localizedCaseInsensitiveCompare($1.value) == .orderedAscending }
.map { $0.key }

sortedFormats.append(contentsOf: nonStandardFormats)

return sortedFormats
}

/// Returns an array of post format display names sorted with "Standard" first, then alphabetically
@objc public var sortedPostFormatNames: [String] {
guard let postFormats = postFormats as? [String: String] else {
return []
}
return sortedPostFormats.compactMap { postFormats[$0] }
}

/// Returns the default post format display text
@objc public var defaultPostFormatText: String? {
postFormatText(fromSlug: settings?.defaultPostFormat)
}

// MARK: - Connections

/// Returns an array of PublicizeConnection objects sorted by service name, then by external name
@objc public var sortedConnections: [PublicizeConnection] {
guard let connections = Array(connections ?? []) as? [PublicizeConnection] else {
return []
}
return connections.sorted { lhs, rhs in
// First sort by service name (case insensitive, localized)
let serviceComparison = lhs.service.localizedCaseInsensitiveCompare(rhs.service)
if serviceComparison != .orderedSame {
return serviceComparison == .orderedAscending
}
// Then sort by external name (case insensitive)
return lhs.externalName.caseInsensitiveCompare(rhs.externalName) == .orderedAscending
}
}

// MARK: - Roles

/// Returns an array of roles sorted by order.
public var sortedRoles: [Role] {
guard let roles = Array(roles ?? []) as? [Role] else {
return []
}
return roles.sorted { lhs, rhs in
(lhs.order?.intValue ?? 0) < (rhs.order?.intValue ?? 0)
}
}
}
50 changes: 0 additions & 50 deletions Sources/WordPressData/Objective-C/Blog.m
Original file line number Diff line number Diff line change
Expand Up @@ -281,56 +281,6 @@ - (NSArray *)sortedCategories
return [[self.categories allObjects] sortedArrayUsingDescriptors:sortDescriptors];
}

- (NSArray *)sortedPostFormats
{
if ([self.postFormats count] == 0) {
return @[];
}

NSMutableArray *sortedFormats = [NSMutableArray arrayWithCapacity:[self.postFormats count]];

if (self.postFormats[PostFormatStandard]) {
[sortedFormats addObject:PostFormatStandard];
}

NSArray *sortedNonStandardFormats = [[self.postFormats keysSortedByValueUsingSelector:@selector(localizedCaseInsensitiveCompare:)] wp_filter:^BOOL(id obj) {
return ![obj isEqual:PostFormatStandard];
}];

[sortedFormats addObjectsFromArray:sortedNonStandardFormats];

return [NSArray arrayWithArray:sortedFormats];
}

- (NSArray *)sortedPostFormatNames
{
return [[self sortedPostFormats] wp_map:^id(NSString *key) {
return self.postFormats[key];
}];
}

- (NSArray *)sortedConnections
{
NSSortDescriptor *sortServiceDescriptor = [[NSSortDescriptor alloc] initWithKey:@"service"
ascending:YES
selector:@selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor *sortExternalNameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"externalName"
ascending:YES
selector:@selector(caseInsensitiveCompare:)];
NSArray *sortDescriptors = @[sortServiceDescriptor, sortExternalNameDescriptor];
return [[self.connections allObjects] sortedArrayUsingDescriptors:sortDescriptors];
}

- (NSArray<Role *> *)sortedRoles
{
return [self.roles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES]]];
}

- (NSString *)defaultPostFormatText
{
return [self postFormatTextFromSlug:self.settings.defaultPostFormat];
}

- (BOOL)hasMappedDomain {
if (![self isHostedAtWPcom]) {
return NO;
Expand Down
4 changes: 0 additions & 4 deletions Sources/WordPressData/Objective-C/include/Blog.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,6 @@ typedef NS_ENUM(NSInteger, SiteVisibility) {


// Readonly Properties
@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormatNames;
@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormats;
@property (nonatomic, weak, readonly, nullable) NSArray *sortedConnections;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Claude rewrote these too, so I went with it.

@property (nonatomic, readonly, nullable) NSArray<Role *> *sortedRoles;
@property (nonatomic, strong, readonly, nullable) WordPressOrgXMLRPCApi *xmlrpcApi;
@property (nonatomic, strong, readonly, nullable) WordPressOrgRestApi *selfHostedSiteRestApi;
@property (nonatomic, weak, readonly, nullable) NSString *version;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ final class PersonViewController: UITableViewController {
return
}

roleViewController.roles = blog.sortedRoles?.map({ $0.toUnmanaged() }) ?? []
roleViewController.roles = blog.sortedRoles.map({ $0.toUnmanaged() }) ?? []
roleViewController.selectedRole = person.role
roleViewController.onChange = { [weak self] newRole in
self?.updateUserRole(newRole)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import WordPressData
extension PostEditor {

func disableSocialConnectionsIfNecessary() {
let connections = self.post.blog.sortedConnections
guard RemoteFeatureFlag.jetpackSocialImprovements.enabled(),
let post = self.post as? Post,
let remainingShares = self.post.blog.sharingLimit?.remaining,
let connections = self.post.blog.sortedConnections as? [PublicizeConnection],
remainingShares < connections.count else {
return
}

for connection in connections {
post.disablePublicizeConnectionWithKeyringID(connection.keyringConnectionID)
}
}

}
149 changes: 149 additions & 0 deletions WordPress/Classes/ViewRelated/Post/PostFormatPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import SwiftUI
import WordPressData
import WordPressFlux
import WordPressUI

@MainActor
struct PostFormatPicker: View {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If there will be more similar use cases, I'll look into generalizing this.

@ObservedObject private var post: Post
@State private var selction: String
@State private var formats: [String]
@State private var isLoading = false
@State private var error: Error?

private let blog: Blog
private let onSubmit: (String) -> Void

static var title: String { Strings.title }

init(post: Post, onSubmit: @escaping (String) -> Void) {
self.post = post
self.blog = post.blog
let formats = post.blog.sortedPostFormatNames
self._formats = State(initialValue: formats)
self._selction = State(initialValue: post.postFormatText() ?? "")
Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to suggest moving this to onAppear like you did in #24597 (comment). However, I just realised that changes to these properties may get reverted when the onAppear method is called again (see #24597 (comment)).

self.onSubmit = onSubmit
}

var body: some View {
Group {
if formats.isEmpty {
if isLoading {
ProgressView()
} else if let error {
EmptyStateView.failure(error: error) {
refreshPostFormats()
}
} else {
emptyStateView
}
} else {
formView
}
}
.navigationTitle(Strings.title)
.navigationBarTitleDisplayMode(.inline)
.refreshable {
await refreshPostFormats()
}
.onAppear {
if formats.isEmpty {
refreshPostFormats()
}
}
}

private func refreshPostFormats() {
Task {
await refreshPostFormats()
}
}

private var formView: some View {
Form {
ForEach(formats, id: \.self) { format in
Button(action: { selectFormat(format) }) {
HStack {
Text(format)
Spacer()
if selction == format {
Image(systemName: "checkmark")
.fontWeight(.medium)
.foregroundColor(Color(UIAppColor.primary))
}
}
}
.foregroundColor(.primary)
}
}
}

private var emptyStateView: some View {
EmptyStateView(
Strings.emptyStateTitle,
systemImage: Strings.emptyStateDescription,
description: "questionmark.folder"
)
}

private func selectFormat(_ format: String) {
selction = format
onSubmit(format)
}

private func refreshPostFormats() async {
isLoading = true
error = nil

let blogService = BlogService(coreDataStack: ContextManager.shared)
do {
try await blogService.syncPostFormats(for: post.blog)
self.formats = post.blog.sortedPostFormatNames
} catch {
self.error = error
if !formats.isEmpty {
Notice(error: error, title: Strings.errorTitle).post()
}
}

isLoading = false
}
}

private extension BlogService {
@MainActor func syncPostFormats(for blog: Blog) async throws {
try await withUnsafeThrowingContinuation { continuation in
syncPostFormats(for: blog, success: {
continuation.resume()
}, failure: { error in
continuation.resume(throwing: error)
})
}
}
}

private enum Strings {
static let title = NSLocalizedString(
"postFormatPicker.navigationTitle",
value: "Post Format",
comment: "Navigation bar title for the Post Format picker"
)

static let emptyStateTitle = NSLocalizedString(
"postFormatPicker.emptyState.title",
value: "No Post Formats Available",
comment: "Empty state title when no post formats are available"
)

static let emptyStateDescription = NSLocalizedString(
"postFormatPicker.emptyState.description",
value: "Post formats haven't been configured for this site.",
comment: "Empty state description when no post formats are available"
)

static let errorTitle = NSLocalizedString(
"postFormatPicker.refreshErrorMessage",
value: "Failed to refresh post formats",
comment: "Error message when post formats refresh fails"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,28 @@ extension PostSettingsViewController {
}
}

// MARK: - PostSettingsViewController (Post Format)

extension PostSettingsViewController {
@objc public func showPostFormatSelector() {
guard let post = apost as? Post else {
return wpAssertionFailure("expected post type")
}
let pickerView = PostFormatPicker(post: post) { [weak self] format in
guard let self else { return }
if post.postFormatText() != format {
WPAnalytics.track(.editorPostFormatChanged, properties: ["via": "settings"])
post.setPostFormatText(format)
}
self.navigationController?.popViewController(animated: true)
self.tableView.reloadData()
}
let pickerVC = UIHostingController(rootView: pickerView)
pickerVC.title = PostFormatPicker.title
navigationController?.pushViewController(pickerVC, animated: true)
}
}

private enum Strings {
static let warningPostWillBePublishedAlertMessage = NSLocalizedString("postSettings.warningPostWillBePublishedAlertMessage", value: "By changing the visibility to 'Private', the post will be published immediately", comment: "An alert message explaning that by changing the visibility to private, the post will be published immediately to your site")
}
Loading