diff --git a/CHANGELOG.md b/CHANGELOG.md index 91815d7..a05660b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,93 +1,100 @@ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0a1] +## [Unreleased] + +### Fixed + +- Fixed a link in the change log; added date to the 1.0.0a1 release. + +## [1.0.0a1] - 2020-09-30 ### Added - A support link in the Settings screen lets you access the help forum. - You can now delete local posts. - You are now prompted for action when viewing a post that was deleted from the server. - You can now choose a default font for new local drafts in the Settings screen. - The post editor shows your content in the set typeface. - Placeholder text has been added to the post editor. - [iOS] The URL of published posts can be shared from the post editor via the system share sheet. ### Changed - The collection list now shows the WriteFreely instance name (or just "WriteFreely" if logged out). - The Publish and Reload buttons are disabled if there's no network connection. - The post editor's status badge has been moved to the top of the screen. - The layout of the post editor has been improved to provide a larger editing area on iPhone. - The app now launches to either the last draft you were working on, or a new blank post. - Empty local posts are discarded when you navigate away from the post editor. - Server addresses with an insecure protocol ("http://") are upgraded to a secure protocol ("https://") before login is attempted. - Attempting to publish a post when you're not logged in presents the login form. ### Fixed - Language-related properties "lang" and "rtl" are set for new posts based on the system's locale. - The keyboard is now dismissed on publishing a post. - Server addresses can now be entered without the protocol ("https://") when logging in. - [iPadOS] Fixed a crash when dismissing a blank post. ## [0.1.1] - 2020-09-14 ### Added - Icon asset for App Store. - [iOS] LaunchScreen storyboard added for iPad multitasking requirements . ## [0.1.0] - 2020-09-11 ### Added - Post editor now has a Publish button to publish a post. - Collections sidebar to choose a specific collection (i.e., blog). - Settings to provide the user interface for logging in, setting preferred color scheme. - The WriteFreelyModel type consolidates other models as Published properties in a single EnvironmentObject. - Logging in and out a WriteFreely instance is now possible. - Collections and Posts are now persisted to local storage between app launches. - Content can be reloaded from the server. - Collections and Posts are purged from the database on logout. - Apps now have app icons. ### Changed - Updated license from AGPLv3 to GPLv3. - Types have been renamed to be more consistent. - WriteFreely Swift package version bumped to v0.2.1. - Local posts are now badged as `local` instead of `draft`. ## [0.0.2] - 2020-07-30 ### Added - Basic post list for displaying (local) posts. - Basic post editor for: - Creating a new local draft (title and content only) - Updating a (local) post - Badge for post status (`draft`, `edited`, `published`). ## [0.0.1] - 2020-07-22 ### Added - WriteFreely Swift package. - SwiftLint build phase for both macOS and iOS targets. - Project metadocuments, including: - Project readme - APGL v3 license - Code of conduct - Contributing guide - This changelog -[1.0.0a1]: https://github.com/writeas/writefreely-swiftui-multiplatform/compare/v1.0.0a1...0.1.1 +[Unreleased]: https://github.com/writeas/writefreely-swiftui-multiplatform/compare/v1.0.0a1...HEAD +[1.0.0a1]: https://github.com/writeas/writefreely-swiftui-multiplatform/compare/v0.1.1...v1.0.0a1 [0.1.1]: https://github.com/writeas/writefreely-swiftui-multiplatform/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/writeas/writefreely-swiftui-multiplatform/compare/v0.0.2...v0.1.0 [0.0.2]: https://github.com/writeas/writefreely-swiftui-multiplatform/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/writeas/writefreely-swiftui-multiplatform/releases/tag/v0.0.1 diff --git a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist index 6cd8075..2723ebe 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist @@ -1,19 +1,19 @@ SchemeUserState WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_ orderHint - 1 + 0 WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_ orderHint - 0 + 1 diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift index 98a646a..79273ac 100644 --- a/iOS/PostEditor/PostEditorView.swift +++ b/iOS/PostEditor/PostEditorView.swift @@ -1,245 +1,256 @@ import SwiftUI struct PostEditorView: View { @EnvironmentObject var model: WriteFreelyModel @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.presentationMode) var presentationMode @ObservedObject var post: WFAPost var body: some View { VStack { if post.hasNewerRemoteCopy { HStack { Text("⚠️ Newer copy on server. Replace local copy?") .font(horizontalSizeClass == .compact ? .caption : .body) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)) .background(Color(UIColor.secondarySystemBackground)) .clipShape(Capsule()) .padding(.bottom) } else if post.wasDeletedFromServer { HStack { Text("⚠️ Post deleted from server. Delete local copy?") .font(horizontalSizeClass == .compact ? .caption : .body) .foregroundColor(.secondary) Button(action: { self.presentationMode.wrappedValue.dismiss() DispatchQueue.main.async { model.posts.remove(post) } }, label: { Image(systemName: "trash") }) } .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)) .background(Color(UIColor.secondarySystemBackground)) .clipShape(Capsule()) .padding(.bottom) } switch post.appearance { case "sans": TextField("Title (optional)", text: $post.title) .font(.custom("OpenSans-Regular", size: 26, relativeTo: Font.TextStyle.largeTitle)) .onChange(of: post.title) { _ in if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } } ZStack(alignment: .topLeading) { if post.body.count == 0 { Text("Write...") .foregroundColor(Color(UIColor.placeholderText)) .padding(.horizontal, 4) .padding(.vertical, 8) .font(.custom("OpenSans-Regular", size: 17, relativeTo: Font.TextStyle.body)) } TextEditor(text: $post.body) .font(.custom("OpenSans-Regular", size: 17, relativeTo: Font.TextStyle.body)) .onChange(of: post.body) { _ in if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } } } case "wrap", "mono", "code": TextField("Title (optional)", text: $post.title) .font(.custom("Hack", size: 26, relativeTo: Font.TextStyle.largeTitle)) .onChange(of: post.title) { _ in if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } } ZStack(alignment: .topLeading) { if post.body.count == 0 { Text("Write...") .foregroundColor(Color(UIColor.placeholderText)) .padding(.horizontal, 4) .padding(.vertical, 8) .font(.custom("Hack", size: 17, relativeTo: Font.TextStyle.body)) } TextEditor(text: $post.body) .font(.custom("Hack", size: 17, relativeTo: Font.TextStyle.body)) .onChange(of: post.body) { _ in if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } } } default: TextField("Title (optional)", text: $post.title) .font(.custom("Lora", size: 26, relativeTo: Font.TextStyle.largeTitle)) .onChange(of: post.title) { _ in if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } } ZStack(alignment: .topLeading) { if post.body.count == 0 { Text("Write...") .foregroundColor(Color(UIColor.placeholderText)) .padding(.horizontal, 4) .padding(.vertical, 8) .font(.custom("Lora", size: 17, relativeTo: Font.TextStyle.body)) } TextEditor(text: $post.body) .font(.custom("Lora", size: 17, relativeTo: Font.TextStyle.body)) .onChange(of: post.body) { _ in if post.status == PostStatus.published.rawValue { post.status = PostStatus.edited.rawValue } } } } } .navigationBarTitleDisplayMode(.inline) .padding() .toolbar { ToolbarItem(placement: .principal) { PostEditorStatusToolbarView(post: post) } ToolbarItemGroup(placement: .navigationBarTrailing) { Button(action: { if model.account.isLoggedIn { publishPost() } else { self.model.isPresentingSettingsView = true } }, label: { Image(systemName: "paperplane") }) .disabled( post.status == PostStatus.published.rawValue || !model.hasNetworkConnection || post.body.count == 0 ) Button(action: { sharePost() }, label: { Image(systemName: "square.and.arrow.up") }) .disabled(post.postId == nil) } } .onChange(of: post.hasNewerRemoteCopy, perform: { _ in if post.status == PostStatus.edited.rawValue && !post.hasNewerRemoteCopy { post.status = PostStatus.published.rawValue } }) .onChange(of: post.status, perform: { _ in if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { model.editor.setLastDraft(post) } } else { DispatchQueue.main.async { model.editor.clearLastDraft() } } }) .onDisappear(perform: { if post.title.count == 0 && post.body.count == 0 && post.status == PostStatus.local.rawValue && post.updatedDate == nil && post.postId == nil { DispatchQueue.main.async { model.posts.remove(post) model.posts.loadCachedPosts() } } else if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { LocalStorageManager().saveContext() } } }) } private func publishPost() { DispatchQueue.main.async { LocalStorageManager().saveContext() model.posts.loadCachedPosts() model.publish(post: post) } #if os(iOS) self.hideKeyboard() #endif } private func sharePost() { - guard let urlString = model.selectedPost?.slug != nil ? - "\(model.account.server)/\((model.selectedPost?.collectionAlias)!)/\((model.selectedPost?.slug)!)" : - "\(model.account.server)/\((model.selectedPost?.postId)!)" else { return } + // If the post doesn't have a post ID, it isn't published, and therefore can't be shared, so return early. + guard let postId = post.postId else { return } + + var urlString: String + + if let postSlug = post.slug, + let postCollectionAlias = post.collectionAlias { + // This post is in a collection, so share the URL as server/collectionAlias/postSlug. + urlString = "\(model.account.server)/\((postCollectionAlias))/\((postSlug))" + } else { + // This is a draft post, so share the URL as server/postID + urlString = "\(model.account.server)/\((postId))" + } + guard let data = URL(string: urlString) else { return } let activityView = UIActivityViewController(activityItems: [data], applicationActivities: nil) UIApplication.shared.windows.first?.rootViewController?.present(activityView, animated: true, completion: nil) if UIDevice.current.userInterfaceIdiom == .pad { activityView.popoverPresentationController?.permittedArrowDirections = .up activityView.popoverPresentationController?.sourceView = UIApplication.shared.windows.first activityView.popoverPresentationController?.sourceRect = CGRect( x: UIScreen.main.bounds.width, y: -125, width: 200, height: 200 ) } } } struct PostEditorView_EmptyPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.createdDate = Date() testPost.appearance = "norm" let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } } struct PostEditorView_ExistingPostPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." testPost.createdDate = Date() testPost.appearance = "code" testPost.hasNewerRemoteCopy = true let model = WriteFreelyModel() return PostEditorView(post: testPost) .environment(\.managedObjectContext, context) .environmentObject(model) } }