Handle undo of edited posts (#251)

This commit is contained in:
Angelo Stavrow 2023-07-23 07:19:52 -04:00 committed by GitHub
parent ba3f44b287
commit 73d219d0ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 68 additions and 12 deletions

View File

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Mac] Added a context-menu item to delete local posts from the post list. - [Mac] Added a context-menu item to delete local posts from the post list.
- [Mac] Added methods to fetch device logs. - [Mac] Added methods to fetch device logs.
- [iOS, Mac] Added a way to search for text across all posts. - [iOS, Mac] Added a way to search for text across all posts.
- [iOS, Mac] Added a way to refresh an edited post from the server copy.
### Changed ### Changed
@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Mac] Updated the URL and minimum version of the WriteFreely Swift package. - [Mac] Updated the URL and minimum version of the WriteFreely Swift package.
- [Mac] Upgraded the Sparkle package to v2. - [Mac] Upgraded the Sparkle package to v2.
- [Mac] The app now prompts you to reach out to our user forums if it detects a crash. - [Mac] The app now prompts you to reach out to our user forums if it detects a crash.
- [iOS, Mac] The app now reverts a post from edited to published status if you undo your changes.
### Fixed ### Fixed

View File

@ -244,6 +244,7 @@ extension WriteFreelyModel {
#if os(macOS) #if os(macOS)
self.selectedPost = cachedPost self.selectedPost = cachedPost
#endif #endif
cachedPost.status = PostStatus.published.rawValue
} }
} catch { } catch {
self.currentError = AppError.genericError(error.localizedDescription) self.currentError = AppError.genericError(error.localizedDescription)
@ -280,7 +281,6 @@ extension WriteFreelyModel {
cachedPost.postId = fetchedPost.postId cachedPost.postId = fetchedPost.postId
cachedPost.rtl = fetchedPost.rtl ?? false cachedPost.rtl = fetchedPost.rtl ?? false
cachedPost.slug = fetchedPost.slug cachedPost.slug = fetchedPost.slug
cachedPost.status = PostStatus.published.rawValue
cachedPost.title = fetchedPost.title ?? "" cachedPost.title = fetchedPost.title ?? ""
cachedPost.updatedDate = fetchedPost.updatedDate cachedPost.updatedDate = fetchedPost.updatedDate
} }

View File

@ -12,6 +12,9 @@ struct PostEditorModel {
@AppStorage(WFDefaults.selectedCollectionURL, store: UserDefaults.shared) var selectedCollectionURL: URL? @AppStorage(WFDefaults.selectedCollectionURL, store: UserDefaults.shared) var selectedCollectionURL: URL?
@AppStorage(WFDefaults.lastDraftURL, store: UserDefaults.shared) var lastDraftURL: URL? @AppStorage(WFDefaults.lastDraftURL, store: UserDefaults.shared) var lastDraftURL: URL?
private(set) var initialPostTitle: String?
private(set) var initialPostBody: String?
#if os(macOS) #if os(macOS)
var postToUpdate: WFAPost? var postToUpdate: WFAPost?
#endif #endif
@ -58,6 +61,21 @@ struct PostEditorModel {
return collection return collection
} }
/// Sets the initial values for title and body on a published post.
///
/// Used to detect if the title and body have changed back to their initial values. If the passed `WFAPost` isn't
/// published, any title and post values already stored are reset to `nil`.
/// - Parameter post: The `WFAPost` for which we're setting initial title/body values.
mutating func setInitialValues(for post: WFAPost) {
if post.status != PostStatus.published.rawValue {
initialPostTitle = nil
initialPostBody = nil
return
}
initialPostTitle = post.title
initialPostBody = post.body
}
private func fetchManagedObject(from objectURL: URL) -> NSManagedObject? { private func fetchManagedObject(from objectURL: URL) -> NSManagedObject? {
let coordinator = LocalStorageManager.standard.container.persistentStoreCoordinator let coordinator = LocalStorageManager.standard.container.persistentStoreCoordinator
guard let managedObjectID = coordinator.managedObjectID(forURIRepresentation: objectURL) else { return nil } guard let managedObjectID = coordinator.managedObjectID(forURIRepresentation: objectURL) else { return nil }

View File

@ -1063,7 +1063,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements"; CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 701; CURRENT_PROJECT_VERSION = 702;
DEVELOPMENT_TEAM = TPPAB4YBA6; DEVELOPMENT_TEAM = TPPAB4YBA6;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist"; INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
@ -1094,7 +1094,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements"; CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 701; CURRENT_PROJECT_VERSION = 702;
DEVELOPMENT_TEAM = TPPAB4YBA6; DEVELOPMENT_TEAM = TPPAB4YBA6;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist"; INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
@ -1237,7 +1237,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 701; CURRENT_PROJECT_VERSION = 702;
DEVELOPMENT_TEAM = TPPAB4YBA6; DEVELOPMENT_TEAM = TPPAB4YBA6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iOS/Info.plist; INFOPLIST_FILE = iOS/Info.plist;
@ -1263,7 +1263,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 701; CURRENT_PROJECT_VERSION = 702;
DEVELOPMENT_TEAM = TPPAB4YBA6; DEVELOPMENT_TEAM = TPPAB4YBA6;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iOS/Info.plist; INFOPLIST_FILE = iOS/Info.plist;

View File

@ -84,7 +84,7 @@ struct PostEditorView: View {
}) })
.accessibilityHint(Text("Choose the blog you want to publish this post to")) .accessibilityHint(Text("Choose the blog you want to publish this post to"))
.disabled(post.body.count == 0) .disabled(post.body.count == 0)
} else { } else if post.status == PostStatus.edited.rawValue {
Button(action: { Button(action: {
if model.account.isLoggedIn { if model.account.isLoggedIn {
publishPost() publishPost()
@ -95,6 +95,14 @@ struct PostEditorView: View {
Label("Publish", systemImage: "paperplane") Label("Publish", systemImage: "paperplane")
}) })
.disabled(post.status == PostStatus.published.rawValue || post.body.count == 0) .disabled(post.status == PostStatus.published.rawValue || post.body.count == 0)
Button(action: {
model.updateFromServer(post: post)
}, label: {
Label("Revert", systemImage: "clock.arrow.circlepath")
})
.accessibilityHint(Text("Replace the edited post with the published version from the server"))
.disabled(post.status != PostStatus.edited.rawValue)
} }
Button(action: { Button(action: {
sharePost() sharePost()
@ -160,6 +168,7 @@ struct PostEditorView: View {
}) })
.onAppear(perform: { .onAppear(perform: {
self.selectedCollection = collections.first { $0.alias == post.collectionAlias } self.selectedCollection = collections.first { $0.alias == post.collectionAlias }
model.editor.setInitialValues(for: post)
if post.status != PostStatus.published.rawValue { if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async { DispatchQueue.main.async {
self.model.editor.saveLastDraft(post) self.model.editor.saveLastDraft(post)
@ -201,9 +210,8 @@ struct PostEditorView: View {
LocalStorageManager.standard.saveContext() LocalStorageManager.standard.saveContext()
model.publish(post: post) model.publish(post: post)
} }
#if os(iOS) model.editor.setInitialValues(for: post)
self.hideKeyboard() self.hideKeyboard()
#endif
} }
private func sharePost() { private func sharePost() {

View File

@ -2,6 +2,7 @@ import SwiftUI
struct PostTextEditingView: View { struct PostTextEditingView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.horizontalSizeClass) var horizontalSizeClass
@EnvironmentObject var model: WriteFreelyModel
@ObservedObject var post: WFAPost @ObservedObject var post: WFAPost
@Binding var updatingTitleFromServer: Bool @Binding var updatingTitleFromServer: Bool
@Binding var updatingBodyFromServer: Bool @Binding var updatingBodyFromServer: Bool
@ -35,13 +36,16 @@ struct PostTextEditingView: View {
) )
.accessibilityLabel(Text("Title (optional)")) .accessibilityLabel(Text("Title (optional)"))
.accessibilityHint(Text("Add or edit the title for your post; use the Return key to skip to the body")) .accessibilityHint(Text("Add or edit the title for your post; use the Return key to skip to the body"))
.onChange(of: post.title) { _ in .onChange(of: post.title) { value in
if post.status == PostStatus.published.rawValue && !updatingTitleFromServer { if post.status == PostStatus.published.rawValue && !updatingTitleFromServer {
post.status = PostStatus.edited.rawValue post.status = PostStatus.edited.rawValue
} }
if updatingTitleFromServer { if updatingTitleFromServer {
updatingTitleFromServer = false updatingTitleFromServer = false
} }
if post.status == PostStatus.edited.rawValue && value == model.editor.initialPostTitle {
post.status = PostStatus.published.rawValue
}
} }
MultilineTextField( MultilineTextField(
"Write...", "Write...",
@ -51,13 +55,16 @@ struct PostTextEditingView: View {
) )
.accessibilityLabel(Text("Body")) .accessibilityLabel(Text("Body"))
.accessibilityHint(Text("Add or edit the body of your post")) .accessibilityHint(Text("Add or edit the body of your post"))
.onChange(of: post.body) { _ in .onChange(of: post.body) { value in
if post.status == PostStatus.published.rawValue && !updatingBodyFromServer { if post.status == PostStatus.published.rawValue && !updatingBodyFromServer {
post.status = PostStatus.edited.rawValue post.status = PostStatus.edited.rawValue
} }
if updatingBodyFromServer { if updatingBodyFromServer {
updatingBodyFromServer = false updatingBodyFromServer = false
} }
if post.status == PostStatus.edited.rawValue && value == model.editor.initialPostBody {
post.status = PostStatus.published.rawValue
}
} }
} }
.onChange(of: titleIsFirstResponder, perform: { value in .onChange(of: titleIsFirstResponder, perform: { value in

View File

@ -31,6 +31,17 @@ struct ActivePostToolbarView: View {
.frame(minWidth: 50, alignment: .center) .frame(minWidth: 50, alignment: .center)
.layoutPriority(1) .layoutPriority(1)
.padding(.horizontal) .padding(.horizontal)
if activePost.status == PostStatus.edited.rawValue {
Button(action: {
model.editor.postToUpdate = activePost
model.updateFromServer(post: activePost)
model.selectedPost = nil
}, label: {
Image(systemName: "clock.arrow.circlepath")
.accessibilityLabel(Text("Revert post"))
.accessibilityHint(Text("Replace the edited post with the published version from the server"))
})
}
if activePost.status == PostStatus.local.rawValue { if activePost.status == PostStatus.local.rawValue {
Menu(content: { Menu(content: {
Label("Publish To:", systemImage: "paperplane") Label("Publish To:", systemImage: "paperplane")
@ -131,6 +142,7 @@ struct ActivePostToolbarView: View {
LocalStorageManager.standard.saveContext() LocalStorageManager.standard.saveContext()
model.publish(post: post) model.publish(post: post)
} }
model.editor.setInitialValues(for: post)
} }
private func openSettingsWindow() { private func openSettingsWindow() {

View File

@ -16,6 +16,7 @@ struct PostEditorView: View {
.padding() .padding()
.background(Color(NSColor.controlBackgroundColor)) .background(Color(NSColor.controlBackgroundColor))
.onAppear(perform: { .onAppear(perform: {
model.editor.setInitialValues(for: post)
if post.status != PostStatus.published.rawValue { if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async { DispatchQueue.main.async {
self.model.editor.saveLastDraft(post) self.model.editor.saveLastDraft(post)

View File

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct PostTextEditingView: View { struct PostTextEditingView: View {
@EnvironmentObject var model: WriteFreelyModel
@ObservedObject var post: WFAPost @ObservedObject var post: WFAPost
@Binding var updatingFromServer: Bool @Binding var updatingFromServer: Bool
@State private var appearance: PostAppearance = .serif @State private var appearance: PostAppearance = .serif
@ -74,9 +75,16 @@ struct PostTextEditingView: View {
private func onTextChange(_ text: String) { private func onTextChange(_ text: String) {
extractTitle(text) extractTitle(text)
if post.status == PostStatus.published.rawValue && !updatingFromServer { if !updatingFromServer {
if post.status == PostStatus.published.rawValue {
post.status = PostStatus.edited.rawValue post.status = PostStatus.edited.rawValue
} }
if post.status == PostStatus.edited.rawValue,
post.title == model.editor.initialPostTitle,
post.body == model.editor.initialPostBody {
post.status = PostStatus.published.rawValue
}
}
if updatingFromServer { if updatingFromServer {
self.updatingFromServer = false self.updatingFromServer = false