Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F13779591
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
24 KB
Subscribers
None
View Options
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
Details
Attached
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
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment