@@ -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 | ||||
@@ -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 | ||||
} | } | ||||
@@ -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 } | ||||
@@ -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; | ||||
@@ -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() { | ||||
@@ -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 | ||||
@@ -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() { | ||||
@@ -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) | ||||
@@ -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,8 +75,15 @@ struct PostTextEditingView: View { | |||||
private func onTextChange(_ text: String) { | private func onTextChange(_ text: String) { | ||||
extractTitle(text) | extractTitle(text) | ||||
if post.status == PostStatus.published.rawValue && !updatingFromServer { | |||||
post.status = PostStatus.edited.rawValue | |||||
if !updatingFromServer { | |||||
if post.status == PostStatus.published.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 { | ||||