From b418b3ee1883b915c58bc4909b709bc2ff249669 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Jun 2025 20:51:21 -0400 Subject: [PATCH 1/2] Add PostAuthorPicker --- RELEASE-NOTES.txt | 1 + .../PostAuthorSelectorViewController.swift | 78 --------------- .../PostSettingsViewController+Swift.swift | 9 +- .../Post/Views/PostAuthorPicker.swift | 97 +++++++++++++++++++ .../Views/PostAuthorPickerViewModel.swift | 57 +++++++++++ 5 files changed, 159 insertions(+), 83 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Post/PostAuthorSelectorViewController.swift create mode 100644 WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift create mode 100644 WordPress/Classes/ViewRelated/Post/Views/PostAuthorPickerViewModel.swift diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 77b5c4ab1c71..e321cc5a1ffe 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,7 @@ 26.0 ----- * [**] Add new “Subscribers” screen that shows both your email and Reader subscribers [#24513] +* [*] Add new "Author Picker" for "Post Settings" with search and better design [#24621] * [*] Fix an issue with “Stats / Subscribers” sometimes not showing the latest email subscribers [#24513] * [*] Fix an issue with "Stats" / "Subscribers" / "Emails" showing html encoded characters [#24513] diff --git a/WordPress/Classes/ViewRelated/Post/PostAuthorSelectorViewController.swift b/WordPress/Classes/ViewRelated/Post/PostAuthorSelectorViewController.swift deleted file mode 100644 index 882de216502d..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostAuthorSelectorViewController.swift +++ /dev/null @@ -1,78 +0,0 @@ -import UIKit -import WordPressData - -class PostAuthorSelectorViewController: SettingsSelectionViewController { - /// A completion block that is called after the user selects an option. - var completion: (() -> Void)? - - /// Representation of an Author used by the view. - private typealias Author = (displayName: String, userID: NSNumber, avatarURL: String?) - - // MARK: - Constructors - - init(post: AbstractPost) { - let authors = PostAuthorSelectorViewController.sortedActiveAuthors(for: post.blog) - - guard !authors.isEmpty, let currentAuthorID = post.authorID else { - super.init(style: .plain) - return - } - - let authorsDict: [AnyHashable: Any] = [ - "DefaultValue": currentAuthorID, - "Title": NSLocalizedString("Author", comment: "Author label."), - "Titles": authors.map { $0.displayName }, - "Values": authors.map { $0.userID }, - "CurrentValue": currentAuthorID - ] - - super.init(dictionary: authorsDict) - - onItemSelected = { [weak self] authorID in - guard - let authorID = authorID as? NSNumber, - let author = authors.first(where: { $0.userID == authorID }), - !post.isFault, post.managedObjectContext != nil - else { - return - } - - post.authorID = author.userID - post.author = author.displayName - post.authorAvatarURL = author.avatarURL - - self?.completion?() - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override init!(style: UITableView.Style, andDictionary dictionary: [AnyHashable: Any]!) { - super.init(style: style, andDictionary: dictionary) - } - - override init(style: UITableView.Style) { - super.init(style: style) - } - - // MARK: - Class Methods - - /// Sort authors by their display name in lexicographical order, accounting for diacritical marks. - private static func sortedActiveAuthors(for blog: Blog) -> [Author] { - /// Don't include any deleted authors. - guard let activeAuthors = blog.authors?.filter ({ !$0.deletedFromBlog }) else { - return [] - } - - return activeAuthors.compactMap { - /// Require a display name to be available. - guard let displayName = $0.displayName else { - return nil - } - - return (displayName, $0.userID, $0.avatarURL) - }.sorted(by: { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }) - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 762707c6b9f8..264f884bd0f0 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -279,13 +279,12 @@ extension PostSettingsViewController { } @objc public func showPostAuthorSelector() { - let authorVC = PostAuthorSelectorViewController(post: apost) - authorVC.completion = { [weak authorVC] in + let picker = PostAuthorPicker(post: apost) { [weak self] in WPAnalytics.track(.editorPostAuthorChanged, properties: ["via": "settings"]) - authorVC?.dismiss() // It pops VC - self.tableView.reloadData() + self?.tableView.reloadData() } - navigationController?.pushViewController(authorVC, animated: true) + let hostingController = UIHostingController(rootView: picker) + navigationController?.pushViewController(hostingController, animated: true) } @objc public func showTagsPicker() { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift b/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift new file mode 100644 index 000000000000..0e2177594b0d --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift @@ -0,0 +1,97 @@ +import SwiftUI +import WordPressData +import WordPressUI +import WordPressShared + +struct PostAuthorPicker: View { + @StateObject private var viewModel: PostAuthorPickerViewModel + @State private var searchText = "" + @Environment(\.dismiss) private var dismiss + + init(post: AbstractPost, onSelection: @escaping () -> Void) { + _viewModel = StateObject(wrappedValue: PostAuthorPickerViewModel(post: post, onSelection: onSelection)) + } + + var body: some View { + List { + ForEach(filteredAuthors) { author in + Button { + viewModel.selectAuthor(author) + dismiss() + } label: { + AuthorRow(author: author, isSelected: viewModel.isSelected(author)) + } + .buttonStyle(.plain) + } + } + .environment(\.defaultMinListRowHeight, 54) + .listStyle(.plain) + .searchable(text: $searchText) + .navigationTitle(Strings.title) + .navigationBarTitleDisplayMode(.inline) + } + + private var filteredAuthors: [PostAuthorPickerViewModel.AuthorItem] { + if searchText.isEmpty { + return viewModel.authors + } + return viewModel.authors.search(searchText) { author in + // Combine display name and username for search + var searchableText = author.displayName + if let username = author.username { + searchableText += " @\(username)" + } + return searchableText + } + } +} + +private struct AuthorRow: View { + let author: PostAuthorPickerViewModel.AuthorItem + let isSelected: Bool + + var body: some View { + HStack(spacing: 12) { + AvatarView(style: .single(author.avatarURL), diameter: 36) + + VStack(alignment: .leading) { + Text(author.displayName) + .font(.callout.weight(.medium)) + + if let username = author.username { + Text("@\(username)") + .font(.footnote) + .foregroundColor(.secondary) + } + } + .lineLimit(1) + + Spacer() + + if isSelected { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + .fontWeight(.medium) + } + } + .contentShape(Rectangle()) + } +} + +private enum Strings { + static let title = NSLocalizedString( + "postAuthorPicker.title", + value: "Author", + comment: "Title for the post author selection screen" + ) +} + +#if DEBUG +struct PostAuthorPicker_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + PostAuthorPicker(post: AbstractPost(), onSelection: {}) + } + } +} +#endif diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPickerViewModel.swift b/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPickerViewModel.swift new file mode 100644 index 000000000000..83593b91f632 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPickerViewModel.swift @@ -0,0 +1,57 @@ +import Foundation +import WordPressData +import Combine + +@MainActor +final class PostAuthorPickerViewModel: ObservableObject { + struct AuthorItem: Identifiable { + let id: NSNumber + let displayName: String + let username: String? + let avatarURL: URL? + + init(from blogAuthor: BlogAuthor) { + self.id = blogAuthor.userID + self.displayName = blogAuthor.displayName ?? "" + self.username = blogAuthor.username + self.avatarURL = blogAuthor.avatarURL.flatMap { URL(string: $0) } + } + } + + @Published private(set) var authors: [AuthorItem] = [] + + private let post: AbstractPost + private let onSelection: () -> Void + private let currentAuthorID: NSNumber? + + init(post: AbstractPost, onSelection: @escaping () -> Void) { + self.post = post + self.onSelection = onSelection + self.currentAuthorID = post.authorID + + loadAuthors() + } + + func selectAuthor(_ author: AuthorItem) { + guard !post.isFault, post.managedObjectContext != nil else { return } + + post.authorID = author.id + post.author = author.displayName + post.authorAvatarURL = author.avatarURL?.absoluteString + + onSelection() + } + + func isSelected(_ author: AuthorItem) -> Bool { + author.id == currentAuthorID + } + + private func loadAuthors() { + authors = (post.blog.authors ?? []) + .filter { !$0.deletedFromBlog } + .map(AuthorItem.init) + .sorted { + $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending + } + } +} From c2911cd1c3f6dc402d5bfc7b484c4e77d876ee31 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 26 Jun 2025 08:07:45 -0400 Subject: [PATCH 2/2] Remvoe preview --- .../ViewRelated/Post/Views/PostAuthorPicker.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift b/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift index 0e2177594b0d..c4956967dea4 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostAuthorPicker.swift @@ -85,13 +85,3 @@ private enum Strings { comment: "Title for the post author selection screen" ) } - -#if DEBUG -struct PostAuthorPicker_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - PostAuthorPicker(post: AbstractPost(), onSelection: {}) - } - } -} -#endif