-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Basic support of custom post types #25157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
Generated by 🚫 Danger |
|
| App Name | Jetpack | |
| Configuration | Release-Alpha | |
| Build Number | 30634 | |
| Version | PR #25157 | |
| Bundle ID | com.jetpack.alpha | |
| Commit | 4bec4e1 | |
| Installation URL | 241qvovlg0m10 |
|
| App Name | WordPress | |
| Configuration | Release-Alpha | |
| Build Number | 30634 | |
| Version | PR #25157 | |
| Bundle ID | org.wordpress.alpha | |
| Commit | 4bec4e1 | |
| Installation URL | 659ahvsmoaug8 |
| import WebKit | ||
| import DesignSystem | ||
|
|
||
| class SimpleGBKViewController: UIViewController { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the editor used for custom posts? Would it work to parameterize the current editor (NewGutenbergViewController) to work with these posts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that'd be the next step. The SimpleGBKViewController was created to quickly test editing custom posts in GBK editor.
af4e4b9 to
014b963
Compare
kean
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, in addition to the inline comments, I have two more high-level suggestions/questions:
1. Navigation
On the web, the CPTs are added directly to the menu (under "Comments" and everything else). The "Custom Post Types" menu looks out of place. It also shouldn't be shown if you have no CPTs (default for WordPress sites). If the app does the same as the web, it solves both issues.
"Custom Post Type" is a bit of a more technical term. I don't think it should be prevalent in the UI. The user only cares to know that their type of content is available with the names they provided.
2. Post Status & Filtering
It's not clear posts in which status it shows. It's probably a good idea to match the web's UX and show "All" by default, but the list then needs to clearly communicate the status of the posts for each individual post.
The safer option is to keep the same UX as in the current post list. It's convenient that you can switch between tabs in one tab.
| public func showCustomPostTypes() { | ||
| Task { | ||
| guard let client = try? WordPressClientFactory.shared.instance(for: .init(blog: blog)), | ||
| let service = await client.service |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use locks – just like in WordPressClientFactory – to ensure thread-safety when accessing .service. It's inconvenient to use async methods when they aren't really needed, and it can lead to the same screen being pushed multiple times if you tap it multiple times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WordPressClient is an actor, so we can't lock it. There is an open PR that also changes WordPressClient, so I don't want to change it too much at this stage. I'll keep a note to myself to see if it can be improved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can simply make the property nonisolated without introducing too many other changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not nonisolated, though. The service uses cache, which is expensive to create (it may create database tables) and should be created only once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it work if WordPressClient creates cache and service in its initializer, and store them in as let properties?
public final class WordPressClient: Sendable {
public let siteURL: URL
public let api: WordPressAPI
public let cache: WordPressApiCache?
public let service: WpSelfHostedService?
public init(api: WordPressAPI, siteURL: URL) {
self.api = api
self.siteURL = siteURL
self.cache = WordPressApiCache.bootstrap()
self.service = try api.createSelfHostedService(cache: cache)
}
}It will have more predictable performance as it will perform the bootstrap on init as opposed to non-deremined point in time when cache or service are needed/accessed. It'll likely once when the user doesn't need to notice anything – e.g. they are switching sites.
It looks like it's also missing error handling. It would be easier to implement if errors are handled once during init as opposed to individual property accessors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have any control over which thread WordPressClient.init is called on, but we don't want to create WordPressApiCache on the main thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you take the current Core Data implementation, it loads the database on app launch when the app has control over it. It seems like the best place to instantiate expensive subsystems.
Just to clarify, I think it's OK to change later. I just don't think it's acceptable to put it in Task when you try a push a new screen. This seems like the least optimal option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm planning to look into removing the Task. But I do want to keep creating the cache instance out of the main thread, though. ContextManager, being legacy code and a shared instance, prevents us from keeping it out of the main thread. But since we are starting new here, I just want to try do the right thing.
|
|
||
| private enum ListItem: Identifiable, Equatable { | ||
| case ready(id: Int64, post: DisplayPost, fullPost: AnyPostWithEditContext) | ||
| case stale(id: Int64, post: DisplayPost) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear what these states mean and why they are exposed to the view layer (CustomPostList): stale, fetching, missing, error, ready? It doesn't seem to make much sense.
From the business logic, you either have a post or you don't. In addition the app might be in the process of sending a request to update one of the posts and it could fail – that's about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These states are part of the mechanism by which the Rust library syncs posts and keeps them up to date with the server.
I reused the states in the Swift code for simplicity's sake. I will add a TODO here and address it in a separate PR.
| self.date = entity.dateGmt | ||
| self.title = entity.title?.raw | ||
| self.excerpt = entity.excerpt?.raw | ||
| ?? String((entity.content.raw ?? entity.content.rendered).prefix(excerptLimit)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ForEach(items) { item in | ||
| listRow(item) | ||
| .task { | ||
| if !isLoadingMore, items.suffix(5).contains(where: { $0.id == item.id }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) It couldn't probably use DataViewPaginatedForEach – doesn't have to, but I think it should work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took the same load more mechanism in DataViewPaginatedForEach here. I don't want to just use it here though. Because I don't want to mix the data view loading mechanism and the one in Rust together, also the reused part is pretty simple and straightforward.
| } | ||
| } | ||
|
|
||
| private struct PostRowView: View { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's extract it to a separate file.
| } | ||
|
|
||
| @ViewBuilder | ||
| private func makeFilterMenu() -> some View { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) It's more idiomatic to implement these as properties (filterMenu). It also doesn't need @ViewBuilder.
(nit) Passing a binding to each individual item is a bit odd: FilterMenuItem(filter: $filter. Can it be implemented as a simple picker?
Picker("", selection: viewModel.makeSortFieldBinding()) {
ForEach([SortField.dateSubscribed, .email, .name], id: \.self) { item in
Text(item.localizedTitle).tag(item)
}
}
.pickerStyle(.inline)
WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostList.swift
Outdated
Show resolved
Hide resolved
| for await hook in updates { | ||
| guard let collection, collection.isRelevantUpdate(hook: hook) else { continue } | ||
|
|
||
| DDLogInfo("WpApiCache update: \(hook.action) to \(hook.table) at row \(hook.rowId)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't say it's clear what' going on in the subscriber. I tried to debug it, but it wasn't really possible with Rust.
When you open the screen it seems to receive two "relevant" updates (what does that mean?).
WpApiCache update: update to listMetadataState at row 1
List info: Optional(WordPressAPIInternal.ListInfo(state: WordPressAPIInternal.ListState.fetchingFirstPage, errorMessage: nil, currentPage: Optional(1), totalPages: Optional(1), totalItems: Optional(1), perPage: 20))
WpApiCache update: update to listMetadataState at row 1
List info: Optional(WordPressAPIInternal.ListInfo(state: WordPressAPIInternal.ListState.idle, errorMessage: nil, currentPage: Optional(1), totalPages: Optional(1), totalItems: Optional(1), perPage: 20))For each updates, it seems to reload and re-map all the items: collection.loadItems().map(ListItem.init) and update the list with animation. Is that how it's supposed to work? Are we planning to add more granular updates? It seems like it would be inefficient for any large collection of items.
(nit) map(ListItem.init) – this happens on the main thread; possible potential performance improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably easier to look into the Rust library directly to understand its syncing mechanism.
From the client's point of view, we need to do two things:
- Call
refresh()orloadNextPage()to trigger the syncing. - Observe the "updates", fetch the most updated data (
listInfo()&loadData()results), and display them on screen.
When you open the screen it seems to receive two "relevant" updates (what does that mean?).
Internally, the Rust library stores the syncing data (metadata + post data) in a database. The Rust code sends an update whenever the database is updated. My expectation is that, regardless of the internal mechanism, the client should receive updates only when it's absolutely necessary to refresh the data displayed on screen. That is not the case at the moment. But I think the Rust library should be improved to reduce the number of updates sent to the app.
For each updates, it seems to reload and re-map all the items. [...] Is that how it's supposed to work? Are we planning to add more granular updates?
By "glandular updates", do you mean like only mapping new items and appending them to the list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "glandular updates", do you mean like only mapping new items and appending them to the list?
Yes, and updated to individual items. This is not strictly necessary with SwiftUI's List diff algorithm, but it might be for Android or for other components or parts of the app.
Core Data has pretty good APIs for observing changes with direct SwiftUI binding for collections (FetchRequest) and for individual items (ObservedObject) where you can dive into any level of granularity if needed. I don't know if there is a similar ORM-ish library for Rust, but I would pick something existing, stable, and documented. This is extremely hard to design and implement well.
collection.isRelevantUpdate(hook: hook)
This whole invocation seems like it should be a one-liner on the view level:
cache.databaseUpdatesPublisher()
.debounce(for: .milliseconds(50), scheduler: DispatchQueue.main)
.values
for await hook in updates {
guard let collection, collection.isRelevantUpdate(hook: hook) else { continue }Maybe something like:
for await update in cache.updates(for: collection)The fact that it needs debounce also points to something problematic. The fact that you need to call collection.loadItems() on every update also is. I seems it leaves a lot to be desired right now – too low-level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, and updated to individual items. [...] Core Data has pretty good APIs for observing changes with direct SwiftUI binding for collections (FetchRequest) and for individual items (ObservedObject) where you can dive into any level of granularity if needed.
I think there two things in play here:
- The Rust library sends minimal updates, similar to the
NSFetchedResultsControlleras you mentioned. - The Swift observer code only map-and-update the
@State var itemsif absolutely necessary, even if the Rust library does not do the above.
I don't think we intend to make the Rust library do 1. It'd be nice for sure, but, as you mentioned, that can be complicated.
As for 2, I think we can try that in the Swift code. I'm not sure how much performance it'd gain. We can definitely try and see.
The fact that it needs debounce also points to something problematic.
Yes. It is. As I mentioned before, at the moment, the Rust library sends updates that are not necessary to update the UI. It's not too frequently, like it may send two events one immediately after another, even though only one is needed. The frequency of updates affects the list UI, but not the performance. The debounce here is to avoid UI glitches (like list flashes), rather than unblocking the main thread
|
|
||
| let listInfo = collection.listInfo() | ||
|
|
||
| NSLog("List info: \(String(describing: listInfo))") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to be DDLog.
| private struct CustomPostSearchResultView: View { | ||
| let client: WordPressClient | ||
| let service: WpSelfHostedService | ||
| let endpoint: PostEndpointType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) There seem to be a potential to warp it in a Service or a ViewModel to reduce the amount of complexity the view needs to know about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have extracted a view model from the previous file. And the code is split into multiple files, which should help with navigating the code.
|
@kean Thanks for the review. I have created a couple of issues to track your suggestions, and I'll address them in separate PRs. |
5070ed6 to
6f27cdd
Compare
|
⬆️ Rebased without any code changes. |
|
🤖 Build Failure AnalysisThis build has failures. Claude has analyzed them - check the build annotations for details. |






Description
This PR adds basic support for custom post types, which is behind a local feature flag (on by default in debug builds).
Please note that, during development, I found a few parsing issues. I suspect there are more. We'll need to try a few more plugins that register custom post types (see 1 below).
Supported features:
Plugins
Here are a few plugins that I tested. You can install them, create some test data, and view them in the app.
Testing instructions
You can use cpt.wpmt.co to view the books on the site. Please note that not all books can be loaded (see p1768466053646389-slack-C06S1QS55K9).
You can install WP Job Manager on test sites, which registers a "job listing" custom post type.