Add the ability to import and export saved posts.#25509
Conversation
Generated by 🚫 Danger |
|
| App Name | WordPress | |
| Configuration | Release-Alpha | |
| Build Number | 32261 | |
| Version | PR #25509 | |
| Bundle ID | org.wordpress.alpha | |
| Commit | 0b1882d | |
| Installation URL | 5g8pe0pghi128 |
|
| App Name | Jetpack | |
| Configuration | Release-Alpha | |
| Build Number | 32261 | |
| Version | PR #25509 | |
| Bundle ID | com.jetpack.alpha | |
| Commit | 0b1882d | |
| Installation URL | 56bpei6s865bo |
d2b6618 to
8209a08
Compare
3b1a46f to
06c723d
Compare
🤖 Build Failure AnalysisThis build has failures. Claude has analyzed them - check the build annotations for details. |
06c723d to
6f4c403
Compare
Lets the VM be unit-tested without ContextManager.shared, and marks the alert message strings @published so SwiftUI re-reads them when the isShowing* flag flips.
Adds three Tracks events: - reader_saved_posts_settings_shown when the screen appears - reader_saved_posts_exported on a successful export - reader_saved_posts_imported with imported/skipped/failed counts
| let appName = (Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String) ?? "WordPress" | ||
| let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String |
There was a problem hiding this comment.
These can be force-unwrapped – we'd never ship a release that doesn't have them.
Realistically we should have a helper that does it for you 🤷
| import WordPressData | ||
|
|
||
| /// Handles exporting and importing saved Reader posts as JSON files. | ||
| struct ReaderSavedPostsExporter { |
There was a problem hiding this comment.
The implementation does not quite follow the patterns used widely in the app. I have added a doc in #25551. I wonder if you can prompt AI to rewrite to follow the updated best practices?
There was a problem hiding this comment.
Sounds good! Once that's merged I can have it do another pass.
There was a problem hiding this comment.
If we're taking another look at this, I'd really like to see any serialization and parsing use Codable – String:Any is too loose about the format (which means we don't get compile-time detection of problems, we get runtime ones)
|
Instead of arbitrary JSON, what do you think about using the standard RSS? |
Replaces [String: Any] envelopes with typed Envelope and ExportedPost structs so the JSON format is validated at compile time rather than at runtime.
| coreDataStack: CoreDataStack, | ||
| progress: @escaping (Int, Int) -> Void, | ||
| completion: @escaping (ImportResult) -> Void | ||
| ) { |
There was a problem hiding this comment.
I think making this function async would simplify the implementation:
- You can take
Progress(which SwiftUI supports) as a parameter, and update itscompletedUnitandtotalUnitin this function to surface importing progress. - You can use TaskGroup to import posts in parallel.
- It'd be impossible to forget calling the completion block.
- The implementation uses
coreDataStack.mainContext, which means the function must be called from the main thread. You can enforce that by declaring the function@MainActor. However, I don't think this is necessary, because you should be able to usecoreDataStack.performAndSaveto save Core Data after a post is imported.
|
|
||
| func exportSavedPosts() { | ||
| do { | ||
| guard let fileURL = try exporter.export(context: coreDataStack.mainContext) else { |
There was a problem hiding this comment.
It's probably best to export in a background thread.
| featuredImageURL: (featuredImage?.isEmpty ?? true) ? nil : featuredImage, | ||
| siteID: siteID > 0 ? siteID : nil, | ||
| postID: postID > 0 ? postID : nil, | ||
| isFeed: post.isExternal |
There was a problem hiding this comment.
I think you'll need to include the feed id and feed item id in the JSON, when isExternal is true? When it's true, I think the siteId and postId would be nil.
Refactor ReaderSavedPostsExporter.importPosts into an async function that returns its result directly instead of using progress/completion closures: - Take a Progress parameter and update its unit counts so callers can surface import progress (the view model observes it via KVO). - Import posts concurrently with a bounded TaskGroup instead of recursing sequentially, while still capping concurrency to avoid flooding the API. - Drop the completion block so the result can no longer be forgotten. - Use performQuery/performAndSave on a background context instead of mainContext, removing the implicit main-thread requirement. Update the view model to call the async API and the tests accordingly.
ReaderSavedPostsExporter.export now takes a CoreDataStackSwift and is async. The Core Data fetch happens inside performQuery on a background context (mapping each ReaderPost to a value-type ExportedPost so no managed object escapes the closure), and the JSON encoding and file write also run off the main thread. The view model invokes the export from a Task and updates UI state on resume.
Add an isExporting flag that drives an indeterminate ProgressView with "Preparing export…" text in the section, and gate both Import and Export buttons on a new isBusy computed property so neither can be re-tapped while either operation is in flight.


Summary
{AppName}-Saved-Posts-{date}.jsonfile containing post metadata (URL, title, author, site, date, summary, tags, featured image, siteID, postID) along with the app version that exported it.ReaderPostServicepipeline to create fully-populated Core Data objects. Posts are fetched sequentially to avoid race conditions. Deduplication usespermaLink— posts whose URL is already saved are skipped, so importing the same file multiple times or merging lists from different devices is safe.Test plan
🤖 Generated with Claude Code