Page MenuHomeMusing Studio

No OneTemporary

diff --git a/ActionExtension-iOS/ContentView.swift b/ActionExtension-iOS/ContentView.swift
index 0d38361..3e7b0d6 100644
--- a/ActionExtension-iOS/ContentView.swift
+++ b/ActionExtension-iOS/ContentView.swift
@@ -1,199 +1,198 @@
import SwiftUI
import MobileCoreServices
import UniformTypeIdentifiers
import WriteFreely
enum WFActionExtensionError: Error {
case userCancelledRequest
case couldNotParseInputItems
}
struct ContentView: View {
@Environment(\.extensionContext) private var extensionContext: NSExtensionContext!
@Environment(\.managedObjectContext) private var managedObjectContext
@AppStorage(WFDefaults.defaultFontIntegerKey, store: UserDefaults.shared) var fontIndex: Int = 0
@FetchRequest(
entity: WFACollection.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
) var collections: FetchedResults<WFACollection>
@State private var draftTitle: String = ""
@State private var draftText: String = ""
@State private var isShowingAlert: Bool = false
@State private var selectedBlog: WFACollection?
private var draftsCollectionName: String {
guard UserDefaults.shared.string(forKey: WFDefaults.serverStringKey) == "https://write.as" else {
return "Drafts"
}
return "Anonymous"
}
private var controls: some View {
HStack {
Group {
Button(
action: { extensionContext.cancelRequest(withError: WFActionExtensionError.userCancelledRequest) },
label: { Image(systemName: "xmark.circle").imageScale(.large) }
)
.accessibilityLabel(Text("Cancel"))
Spacer()
Button(
action: {
savePostToCollection(collection: selectedBlog, title: draftTitle, body: draftText)
extensionContext.completeRequest(returningItems: nil, completionHandler: nil)
},
label: { Image(systemName: "square.and.arrow.down").imageScale(.large) }
)
.accessibilityLabel(Text("Create new draft"))
}
.padding()
}
}
var body: some View {
VStack {
controls
Form {
Section(header: Text("Title")) {
switch fontIndex {
case 1:
TextField("Draft Title", text: $draftTitle).font(.custom("OpenSans-Regular", size: 26))
case 2:
TextField("Draft Title", text: $draftTitle).font(.custom("Hack-Regular", size: 26))
default:
TextField("Draft Title", text: $draftTitle).font(.custom("Lora", size: 26))
}
}
Section(header: Text("Content")) {
switch fontIndex {
case 1:
TextEditor(text: $draftText).font(.custom("OpenSans-Regular", size: 17))
case 2:
TextEditor(text: $draftText).font(.custom("Hack-Regular", size: 17))
default:
TextEditor(text: $draftText).font(.custom("Lora", size: 17))
}
}
Section(header: Text("Save To")) {
Button(action: {
self.selectedBlog = nil
}, label: {
HStack {
Text(draftsCollectionName)
.foregroundColor(selectedBlog == nil ? .primary : .secondary)
Spacer()
if selectedBlog == nil {
Image(systemName: "checkmark")
}
}
})
ForEach(collections, id: \.self) { collection in
Button(action: {
self.selectedBlog = collection
}, label: {
HStack {
Text(collection.title)
.foregroundColor(selectedBlog == collection ? .primary : .secondary)
Spacer()
if selectedBlog == collection {
Image(systemName: "checkmark")
}
}
})
}
}
}
.padding(.bottom, 24)
}
.alert(isPresented: $isShowingAlert, content: {
Alert(
title: Text("Something Went Wrong"),
message: Text("WriteFreely can't create a draft with the data received."),
dismissButton: .default(Text("OK"), action: {
extensionContext.cancelRequest(withError: WFActionExtensionError.couldNotParseInputItems)
}))
})
.onAppear {
do {
try getPageDataFromExtensionContext()
} catch {
self.isShowingAlert = true
}
}
}
private func savePostToCollection(collection: WFACollection?, title: String, body: String) {
let post = WFAPost(context: managedObjectContext)
post.createdDate = Date()
post.title = title
post.body = body
post.status = PostStatus.local.rawValue
post.collectionAlias = collection?.alias
switch fontIndex {
case 1:
post.appearance = "sans"
case 2:
post.appearance = "wrap"
default:
post.appearance = "serif"
}
if let languageCode = Locale.current.languageCode {
post.language = languageCode
post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
}
LocalStorageManager.standard.saveContext()
}
private func getPageDataFromExtensionContext() throws {
if let inputItem = extensionContext.inputItems.first as? NSExtensionItem {
if let itemProvider = inputItem.attachments?.first {
let typeIdentifier: String
if #available(iOS 15, *) {
typeIdentifier = UTType.propertyList.identifier
} else {
typeIdentifier = kUTTypePropertyList as String
}
itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { (dict, error) in
- if let error = error {
- print("⚠️", error)
+ if error != nil {
self.isShowingAlert = true
}
guard let itemDict = dict as? NSDictionary else {
return
}
guard let jsValues = itemDict[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary else {
return
}
let pageTitle = jsValues["title"] as? String ?? ""
let pageURL = jsValues["URL"] as? String ?? ""
let pageSelectedText = jsValues["selection"] as? String ?? ""
if pageSelectedText.isEmpty {
// If there's no selected text, create a Markdown link to the webpage.
self.draftText = "[\(pageTitle)](\(pageURL))"
} else {
// If there is selected text, create a Markdown blockquote with the selection
// and add a Markdown link to the webpage.
self.draftText = """
> \(pageSelectedText)
Via: [\(pageTitle)](\(pageURL))
"""
}
}
} else {
throw WFActionExtensionError.couldNotParseInputItems
}
} else {
throw WFActionExtensionError.couldNotParseInputItems
}
}
}
diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index 617385f..ba778d0 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,102 +1,103 @@
import Foundation
import WriteFreely
import Security
import Network
// MARK: - WriteFreelyModel
final class WriteFreelyModel: ObservableObject {
// MARK: - Models
@Published var account = AccountModel()
@Published var preferences = PreferencesModel()
@Published var posts = PostListModel()
@Published var editor = PostEditorModel()
// MARK: - Error handling
@Published var hasError: Bool = false
var currentError: Error? {
didSet {
+ // TODO: Remove print statements for debugging before closing #204.
#if DEBUG
print("⚠️ currentError -> didSet \(currentError?.localizedDescription ?? "nil")")
print(" > hasError was: \(self.hasError)")
#endif
DispatchQueue.main.async {
#if DEBUG
print(" > self.currentError != nil: \(self.currentError != nil)")
#endif
self.hasError = self.currentError != nil
#if DEBUG
print(" > hasError is now: \(self.hasError)")
#endif
}
}
}
// MARK: - State
@Published var isLoggingIn: Bool = false
@Published var isProcessingRequest: Bool = false
@Published var hasNetworkConnection: Bool = true
@Published var selectedPost: WFAPost?
@Published var selectedCollection: WFACollection?
@Published var showAllPosts: Bool = true
@Published var isPresentingDeleteAlert: Bool = false
@Published var postToDelete: WFAPost?
#if os(iOS)
@Published var isPresentingSettingsView: Bool = false
#endif
static var shared = WriteFreelyModel()
// swiftlint:disable line_length
let helpURL = URL(string: "https://discuss.write.as/c/help/5")!
let howToURL = URL(string: "https://discuss.write.as/t/using-the-writefreely-ios-app/1946")!
let reviewURL = URL(string: "https://apps.apple.com/app/id1531530896?action=write-review")!
let licensesURL = URL(string: "https://github.com/writeas/writefreely-swiftui-multiplatform/tree/main/Shared/Resources/Licenses")!
// swiftlint:enable line_length
internal var client: WFClient?
private let defaults = UserDefaults.shared
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
internal var postToUpdate: WFAPost?
init() {
DispatchQueue.main.async {
self.preferences.appearance = self.defaults.integer(forKey: WFDefaults.colorSchemeIntegerKey)
self.preferences.font = self.defaults.integer(forKey: WFDefaults.defaultFontIntegerKey)
self.account.restoreState()
if self.account.isLoggedIn {
guard let serverURL = URL(string: self.account.server) else {
self.currentError = AccountError.invalidServerURL
return
}
do {
guard let token = try self.fetchTokenFromKeychain(
username: self.account.username,
server: self.account.server
) else {
self.currentError = KeychainError.couldNotFetchAccessToken
return
}
self.account.login(WFUser(token: token, username: self.account.username))
self.client = WFClient(for: serverURL)
self.client?.user = self.account.user
self.fetchUserCollections()
self.fetchUserPosts()
} catch {
self.currentError = KeychainError.couldNotFetchAccessToken
return
}
}
}
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
self.hasNetworkConnection = path.status == .satisfied
}
}
monitor.start(queue: queue)
}
}
diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift
index 7a78ad6..4fed230 100644
--- a/iOS/PostEditor/PostEditorView.swift
+++ b/iOS/PostEditor/PostEditorView.swift
@@ -1,268 +1,275 @@
import SwiftUI
struct PostEditorView: View {
@EnvironmentObject var model: WriteFreelyModel
+ @EnvironmentObject var errorHandling: ErrorHandling
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.managedObjectContext) var moc
@Environment(\.presentationMode) var presentationMode
@ObservedObject var post: WFAPost
@State private var updatingTitleFromServer: Bool = false
@State private var updatingBodyFromServer: Bool = false
@State private var selectedCollection: WFACollection?
@FetchRequest(
entity: WFACollection.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
) var collections: FetchedResults<WFACollection>
var body: some View {
VStack {
if post.hasNewerRemoteCopy {
RemoteChangePromptView(
remoteChangeType: .remoteCopyUpdated,
buttonHandler: { model.updateFromServer(post: post) }
)
} else if post.wasDeletedFromServer {
RemoteChangePromptView(
remoteChangeType: .remoteCopyDeleted,
buttonHandler: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.async {
model.posts.remove(post)
}
}
)
}
PostTextEditingView(
post: _post,
updatingTitleFromServer: $updatingTitleFromServer,
updatingBodyFromServer: $updatingBodyFromServer
)
+ .withErrorHandling()
}
.navigationBarTitleDisplayMode(.inline)
.padding()
.toolbar {
ToolbarItem(placement: .principal) {
PostEditorStatusToolbarView(post: post)
}
ToolbarItem(placement: .primaryAction) {
if model.isProcessingRequest {
ProgressView()
} else {
Menu(content: {
if post.status == PostStatus.local.rawValue {
Menu(content: {
Label("Publish to…", systemImage: "paperplane")
Button(action: {
if model.account.isLoggedIn {
post.collectionAlias = nil
publishPost()
} else {
self.model.isPresentingSettingsView = true
}
}, label: {
Text(" \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")")
})
ForEach(collections) { collection in
Button(action: {
if model.account.isLoggedIn {
post.collectionAlias = collection.alias
publishPost()
} else {
self.model.isPresentingSettingsView = true
}
}, label: {
Text(" \(collection.title)")
})
}
}, label: {
Label("Publish…", systemImage: "paperplane")
})
.accessibilityHint(Text("Choose the blog you want to publish this post to"))
.disabled(post.body.count == 0)
} else {
Button(action: {
if model.account.isLoggedIn {
publishPost()
} else {
self.model.isPresentingSettingsView = true
}
}, label: {
Label("Publish", systemImage: "paperplane")
})
.disabled(post.status == PostStatus.published.rawValue || post.body.count == 0)
}
Button(action: {
sharePost()
}, label: {
Label("Share", systemImage: "square.and.arrow.up")
})
.accessibilityHint(Text("Open the system share sheet to share a link to this post"))
.disabled(post.postId == nil)
-// Button(action: {
-// print("Tapped 'Delete...' button")
-// }, label: {
-// Label("Delete…", systemImage: "trash")
-// })
if model.account.isLoggedIn && post.status != PostStatus.local.rawValue {
Section(header: Text("Move To Collection")) {
Label("Move to:", systemImage: "arrowshape.zigzag.right")
Picker(selection: $selectedCollection, label: Text("Move to…")) {
Text(
" \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")"
).tag(nil as WFACollection?)
ForEach(collections) { collection in
Text(" \(collection.title)").tag(collection as WFACollection?)
}
}
}
}
}, label: {
ZStack {
Image("does.not.exist")
.accessibilityHidden(true)
Image(systemName: "ellipsis.circle")
.imageScale(.large)
.accessibilityHidden(true)
}
})
.accessibilityLabel(Text("Menu"))
.accessibilityHint(Text("Opens a context menu to publish, share, or move the post"))
.onTapGesture {
hideKeyboard()
}
.disabled(post.body.count == 0)
}
}
}
.onChange(of: post.hasNewerRemoteCopy, perform: { _ in
if !post.hasNewerRemoteCopy {
updatingTitleFromServer = true
updatingBodyFromServer = true
}
})
.onChange(of: selectedCollection, perform: { [selectedCollection] newCollection in
if post.collectionAlias == newCollection?.alias {
return
} else {
post.collectionAlias = newCollection?.alias
model.move(post: post, from: selectedCollection, to: newCollection)
}
})
.onChange(of: post.status, perform: { value in
if value != PostStatus.published.rawValue {
self.model.editor.saveLastDraft(post)
} else {
self.model.editor.clearLastDraft()
}
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
})
.onAppear(perform: {
self.selectedCollection = collections.first { $0.alias == post.collectionAlias }
if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async {
self.model.editor.saveLastDraft(post)
}
} else {
self.model.editor.clearLastDraft()
}
})
+ .onChange(of: model.hasError) { value in
+ if value {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
+ model.hasError = false
+ }
+ }
.onDisappear(perform: {
self.model.editor.clearLastDraft()
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)
}
} else if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
}
})
}
private func publishPost() {
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
model.publish(post: post)
}
#if os(iOS)
self.hideKeyboard()
#endif
}
private func sharePost() {
// 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 baseURL/postSlug.
let urls = collections.filter { $0.alias == postCollectionAlias }
let baseURL = urls.first?.url ?? "\(model.account.server)/\(postCollectionAlias)/"
urlString = "\(baseURL)\(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.standard.container.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.standard.container.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)
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jan 30, 3:57 AM (23 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3610162

Event Timeline