Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F10455349
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
49 KB
Subscribers
None
View Options
diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
index beb186b..ec1521d 100644
--- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
+++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
@@ -1,288 +1,288 @@
import Foundation
import WriteFreely
extension WriteFreelyModel {
func loginHandler(result: Result<WFUser, Error>) {
DispatchQueue.main.async {
self.isLoggingIn = false
}
do {
let user = try result.get()
fetchUserCollections()
fetchUserPosts()
do {
try saveTokenToKeychain(user.token, username: user.username, server: account.server)
DispatchQueue.main.async {
self.account.login(user)
}
} catch {
self.currentError = KeychainError.couldNotStoreAccessToken
}
} catch WFError.notFound {
self.currentError = AccountError.usernameNotFound
} catch WFError.unauthorized {
self.currentError = AccountError.invalidPassword
} catch {
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == -1003 {
self.currentError = AccountError.serverNotFound
} else {
self.currentError = error
}
}
}
func logoutHandler(result: Result<Bool, Error>) {
do {
_ = try result.get()
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
do {
try LocalStorageManager.standard.purgeUserCollections()
try self.posts.purgePublishedPosts()
} catch {
self.currentError = error
}
}
} catch {
self.currentError = KeychainError.couldNotPurgeAccessToken
}
} catch WFError.notFound {
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
// purging the token from the Keychain, destroying the client object, and setting the AccountModel to its
// logged-out state.
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
do {
try LocalStorageManager.standard.purgeUserCollections()
try self.posts.purgePublishedPosts()
} catch {
self.currentError = error
}
}
} catch {
self.currentError = KeychainError.couldNotPurgeAccessToken
}
} catch {
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
// so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're
// logged in, try calling the logout function again and see what we get.
// Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties.
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == NSURLErrorCannotParseResponse {
if account.isLoggedIn {
self.logout()
}
}
}
}
func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let fetchedCollections = try result.get()
for fetchedCollection in fetchedCollections {
DispatchQueue.main.async {
let localCollection = WFACollection(context: LocalStorageManager.standard.container.viewContext)
localCollection.alias = fetchedCollection.alias
localCollection.blogDescription = fetchedCollection.description
localCollection.email = fetchedCollection.email
localCollection.isPublic = fetchedCollection.isPublic ?? false
localCollection.styleSheet = fetchedCollection.styleSheet
localCollection.title = fetchedCollection.title
localCollection.url = fetchedCollection.url
}
}
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch WFError.unauthorized {
self.currentError = AccountError.genericAuthError
self.logout()
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
}
// swiftlint:disable function_body_length
func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
let request = WFAPost.createFetchRequest()
do {
let locallyCachedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request)
do {
var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue }
let fetchedPosts = try result.get()
for fetchedPost in fetchedPosts {
if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) {
DispatchQueue.main.async {
managedPost.wasDeletedFromServer = false
if let fetchedPostUpdatedDate = fetchedPost.updatedDate,
let localPostUpdatedDate = managedPost.updatedDate {
managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate
} else {
self.currentError = AppError.genericError(
"Error updating post: could not determine which copy of post is newer."
)
}
if managedPost.collectionAlias != fetchedPost.collectionAlias {
// The post has been moved so we update the managed post's collectionAlias property.
DispatchQueue.main.async {
if self.navState.selectedPost == managedPost {
self.navState.selectedPost = nil
}
managedPost.collectionAlias = fetchedPost.collectionAlias
}
}
postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId })
}
} else {
DispatchQueue.main.async {
let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext)
self.importData(from: fetchedPost, into: managedPost)
managedPost.collectionAlias = fetchedPost.collectionAlias
managedPost.wasDeletedFromServer = false
}
}
}
DispatchQueue.main.async {
for post in postsToDelete { post.wasDeletedFromServer = true }
LocalStorageManager.standard.saveContext()
}
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
} catch WFError.unauthorized {
self.currentError = AccountError.genericAuthError
self.logout()
} catch {
self.currentError = LocalStoreError.couldNotFetchPosts("cached")
}
}
// swiftlint:enable function_body_length
func publishHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
// If this is an updated post, check it against postToUpdate.
if let updatingPost = self.postToUpdate {
importData(from: fetchedPost, into: updatingPost)
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} else {
// Otherwise if it's a newly-published post, find it in the local store.
let request = WFAPost.createFetchRequest()
let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body)
if let fetchedPostTitle = fetchedPost.title {
let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle)
request.predicate = NSCompoundPredicate(
andPredicateWithSubpredicates: [
matchTitlePredicate,
matchBodyPredicate
]
)
} else {
request.predicate = matchBodyPredicate
}
do {
let cachedPostsResults = try LocalStorageManager.standard.container.viewContext.fetch(request)
guard let cachedPost = cachedPostsResults.first else { return }
importData(from: fetchedPost, into: cachedPost)
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch {
self.currentError = LocalStoreError.couldNotFetchPosts("cached")
}
}
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
}
func updateFromServerHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
#if os(iOS)
guard let cachedPost = self.navState.selectedPost else { return }
#else
guard let cachedPost = self.editor.postToUpdate else { return }
#endif
importData(from: fetchedPost, into: cachedPost)
cachedPost.hasNewerRemoteCopy = false
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
#if os(macOS)
- self.selectedPost = cachedPost
+ self.navState.selectedPost = cachedPost
#endif
cachedPost.status = PostStatus.published.rawValue
}
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
}
func movePostHandler(result: Result<Bool, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let succeeded = try result.get()
if succeeded {
if let post = navState.selectedPost {
updateFromServer(post: post)
} else {
return
}
}
} catch {
DispatchQueue.main.async {
LocalStorageManager.standard.container.viewContext.rollback()
}
self.currentError = AppError.genericError(error.localizedDescription)
}
}
private func importData(from fetchedPost: WFPost, into cachedPost: WFAPost) {
cachedPost.appearance = fetchedPost.appearance
cachedPost.body = fetchedPost.body
cachedPost.createdDate = fetchedPost.createdDate
cachedPost.language = fetchedPost.language
cachedPost.postId = fetchedPost.postId
cachedPost.rtl = fetchedPost.rtl ?? false
cachedPost.slug = fetchedPost.slug
cachedPost.status = PostStatus.published.rawValue
cachedPost.title = fetchedPost.title ?? ""
cachedPost.updatedDate = fetchedPost.updatedDate
}
}
diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift
index e67e145..6c26e4f 100644
--- a/Shared/Navigation/ContentView.swift
+++ b/Shared/Navigation/ContentView.swift
@@ -1,115 +1,118 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
#if os(macOS)
WFNavigation(
collectionList: {
CollectionListView()
.withErrorHandling()
.toolbar {
if #available(macOS 13, *) {
EmptyView()
} else {
Button(
action: {
NSApp.keyWindow?.contentViewController?.tryToPerform(
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil
)
},
label: { Image(systemName: "sidebar.left") }
)
.help("Toggle the sidebar's visibility.")
}
Spacer()
Button(action: {
withAnimation {
// Un-set the currently selected post
- self.model.selectedPost = nil
+ self.model.navState.selectedPost = nil
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
DispatchQueue.main.async {
// Load the new post in the editor
- self.model.selectedPost = managedPost
+ self.model.navState.selectedPost = managedPost
}
}
}, label: { Image(systemName: "square.and.pencil") })
.help("Create a new local draft.")
}
.frame(width: 200)
},
postList: {
ZStack {
- PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
- .withErrorHandling()
- .frame(width: 300)
+ PostListView(
+ selectedCollection: model.navState.selectedCollection,
+ showAllPosts: model.navState.showAllPosts
+ )
+ .withErrorHandling()
+ .frame(width: 300)
if model.isProcessingRequest {
ZStack {
Color(NSColor.controlBackgroundColor).opacity(0.75)
ProgressView()
}
}
}
},
postDetail: {
NoSelectedPostView(isConnected: $model.hasNetworkConnection)
}
)
.environmentObject(model)
.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
}
}
#else
WFNavigation(
collectionList: {
CollectionListView()
.withErrorHandling()
},
postList: {
PostListView(
selectedCollection: model.navState.selectedCollection,
showAllPosts: model.navState.showAllPosts
)
.withErrorHandling()
},
postDetail: {
NoSelectedPostView(isConnected: $model.hasNetworkConnection)
}
)
.environmentObject(model)
.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
}
}
#endif
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return ContentView()
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/Shared/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift
index 8d98614..27b12ad 100644
--- a/Shared/PostEditor/PostEditorStatusToolbarView.swift
+++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift
@@ -1,106 +1,106 @@
import SwiftUI
struct PostEditorStatusToolbarView: View {
@EnvironmentObject var model: WriteFreelyModel
@ObservedObject var post: WFAPost
var body: some View {
if post.hasNewerRemoteCopy {
#if os(iOS)
PostStatusBadgeView(post: post)
#else
HStack {
HStack {
Text("⚠️ Newer copy on server. Replace local copy?")
.font(.callout)
.foregroundColor(.secondary)
Button(action: {
model.editor.postToUpdate = post
model.updateFromServer(post: post)
DispatchQueue.main.async {
- model.selectedPost = nil
+ model.navState.selectedPost = nil
}
}, label: {
Image(systemName: "square.and.arrow.down")
})
.accessibilityLabel(Text("Update post"))
.accessibilityHint(Text("Replace this post with the server version"))
}
.padding(.horizontal)
.background(Color.primary.opacity(0.1))
.clipShape(Capsule())
.padding(.trailing)
PostStatusBadgeView(post: post)
}
#endif
} else if post.wasDeletedFromServer && post.status != PostStatus.local.rawValue {
#if os(iOS)
PostStatusBadgeView(post: post)
#else
HStack {
HStack {
Text("⚠️ Post deleted from server. Delete local copy?")
.font(.callout)
.foregroundColor(.secondary)
Button(action: {
- model.selectedPost = nil
+ model.navState.selectedPost = nil
DispatchQueue.main.async {
model.posts.remove(post)
}
}, label: {
Image(systemName: "trash")
})
.accessibilityLabel(Text("Delete"))
.accessibilityHint(Text("Delete this post from your Mac"))
}
.padding(.horizontal)
.background(Color.primary.opacity(0.1))
.clipShape(Capsule())
.padding(.trailing)
PostStatusBadgeView(post: post)
}
#endif
} else {
PostStatusBadgeView(post: post)
}
}
}
struct PESTView_StandardPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
let testPost = WFAPost(context: context)
testPost.status = PostStatus.published.rawValue
return PostEditorStatusToolbarView(post: testPost)
.environmentObject(model)
}
}
struct PESTView_OutdatedLocalCopyPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
let updatedPost = WFAPost(context: context)
updatedPost.status = PostStatus.published.rawValue
updatedPost.hasNewerRemoteCopy = true
return PostEditorStatusToolbarView(post: updatedPost)
.environmentObject(model)
}
}
struct PESTView_DeletedRemoteCopyPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
let deletedPost = WFAPost(context: context)
deletedPost.status = PostStatus.published.rawValue
deletedPost.wasDeletedFromServer = true
return PostEditorStatusToolbarView(post: deletedPost)
.environmentObject(model)
}
}
diff --git a/Shared/PostList/PostListFilteredView.swift b/Shared/PostList/PostListFilteredView.swift
index 3af723a..0bbdab4 100644
--- a/Shared/PostList/PostListFilteredView.swift
+++ b/Shared/PostList/PostListFilteredView.swift
@@ -1,143 +1,143 @@
import SwiftUI
struct PostListFilteredView: View {
@EnvironmentObject var model: WriteFreelyModel
@Binding var postCount: Int
@FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults<WFACollection>
var fetchRequest: FetchRequest<WFAPost>
init(collection: WFACollection?, showAllPosts: Bool, postCount: Binding<Int>) {
if showAllPosts {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)]
)
} else {
if let collectionAlias = collection?.alias {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
predicate: NSPredicate(format: "collectionAlias == %@", collectionAlias)
)
} else {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
predicate: NSPredicate(format: "collectionAlias == nil")
)
}
}
_postCount = postCount
}
var body: some View {
#if os(iOS)
if #available(iOS 15, *) {
SearchablePostListFilteredView(
postCount: $postCount,
collections: collections,
fetchRequest: fetchRequest,
onDelete: delete(_:)
)
.environmentObject(model)
.onAppear(perform: {
self.postCount = fetchRequest.wrappedValue.count
})
.onChange(of: fetchRequest.wrappedValue.count, perform: { value in
self.postCount = value
})
} else {
List(selection: $model.navState.selectedPost) {
ForEach(fetchRequest.wrappedValue, id: \.self) { post in
NavigationLink(
destination: PostEditorView(post: post),
tag: post,
selection: $model.navState.selectedPost,
label: {
if model.navState.showAllPosts {
if let collection = collections.filter({ $0.alias == post.collectionAlias }).first {
PostCellView(post: post, collectionName: collection.title)
} else {
// swiftlint:disable:next line_length
let collectionName = model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
PostCellView(post: post, collectionName: collectionName)
}
} else {
PostCellView(post: post)
}
})
.deleteDisabled(post.status != PostStatus.local.rawValue)
}
.onDelete(perform: { indexSet in
for index in indexSet {
let post = fetchRequest.wrappedValue[index]
delete(post)
}
})
}
.onAppear(perform: {
self.postCount = fetchRequest.wrappedValue.count
})
.onChange(of: fetchRequest.wrappedValue.count, perform: { value in
self.postCount = value
})
}
#else
SearchablePostListFilteredView(
postCount: $postCount,
collections: collections,
fetchRequest: fetchRequest,
onDelete: delete(_:)
)
.environmentObject(model)
.alert(isPresented: $model.isPresentingDeleteAlert) {
Alert(
title: Text("Delete Post?"),
message: Text("This action cannot be undone."),
primaryButton: .cancel {
model.postToDelete = nil
},
secondaryButton: .destructive(Text("Delete"), action: {
if let postToDelete = model.postToDelete {
- model.selectedPost = nil
+ model.navState.selectedPost = nil
DispatchQueue.main.async {
model.editor.clearLastDraft()
model.posts.remove(postToDelete)
}
model.postToDelete = nil
}
})
)
}
.onDeleteCommand(perform: {
- guard let selectedPost = model.selectedPost else { return }
+ guard let selectedPost = model.navState.selectedPost else { return }
if selectedPost.status == PostStatus.local.rawValue {
model.postToDelete = selectedPost
model.isPresentingDeleteAlert = true
}
})
#endif
}
func delete(_ post: WFAPost) {
DispatchQueue.main.async {
if post == model.navState.selectedPost {
model.navState.selectedPost = nil
model.editor.clearLastDraft()
}
model.posts.remove(post)
}
}
}
struct PostListFilteredView_Previews: PreviewProvider {
static var previews: some View {
return PostListFilteredView(
collection: nil,
showAllPosts: false,
postCount: .constant(999)
)
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift
index d47cafa..e1c4ac6 100644
--- a/Shared/PostList/PostListView.swift
+++ b/Shared/PostList/PostListView.swift
@@ -1,197 +1,197 @@
import SwiftUI
import Combine
struct PostListView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@Environment(\.managedObjectContext) var managedObjectContext
@State private var postCount: Int = 0
@State private var filteredListViewId: Int = 0
var selectedCollection: WFACollection?
var showAllPosts: Bool
#if os(iOS)
private var frameHeight: CGFloat {
var height: CGFloat = 50
let bottom = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0
height += bottom
return height
}
#endif
var body: some View {
#if os(iOS)
ZStack(alignment: .bottom) {
PostListFilteredView(
collection: selectedCollection,
showAllPosts: showAllPosts,
postCount: $postCount
)
.id(self.filteredListViewId)
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.toolbar {
ToolbarItem(placement: .primaryAction) {
ZStack {
// We have to add a Spacer as a sibling view to the Button in some kind of Stack so that any
// a11y modifiers are applied as expected: bug report filed as FB8956392.
if #unavailable(iOS 16) {
Spacer()
}
Button(action: {
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
self.model.navState.showAllPosts = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.model.navState.selectedPost = managedPost
}
}
}, label: {
ZStack {
Image("does.not.exist")
.accessibilityHidden(true)
Image(systemName: "square.and.pencil")
.accessibilityHidden(true)
.imageScale(.large) // These modifiers compensate for the resizing
.padding(.vertical, 12) // done to the Image (and the button tap target)
.padding(.leading, 12) // by the SwiftUI layout system from adding a
.padding(.trailing, 8) // Spacer in this ZStack (FB8956392).
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.accessibilityLabel(Text("Compose"))
.accessibilityHint(Text("Compose a new local draft"))
}
}
}
VStack {
HStack(spacing: 0) {
Button(action: {
model.isPresentingSettingsView = true
}, label: {
Image(systemName: "gear")
.padding(.vertical, 4)
.padding(.horizontal, 8)
})
.accessibilityLabel(Text("Settings"))
.accessibilityHint(Text("Open the Settings sheet"))
.sheet(
isPresented: $model.isPresentingSettingsView,
onDismiss: { model.isPresentingSettingsView = false },
content: {
SettingsView()
.environmentObject(model)
}
)
Spacer()
Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts")
.foregroundColor(.secondary)
Spacer()
if model.isProcessingRequest {
ProgressView()
.padding(.vertical, 4)
.padding(.horizontal, 8)
} else {
if model.hasNetworkConnection {
Button(action: {
DispatchQueue.main.async {
model.fetchUserCollections()
model.fetchUserPosts()
}
}, label: {
Image(systemName: "arrow.clockwise")
.padding(.vertical, 4)
.padding(.horizontal, 8)
})
.accessibilityLabel(Text("Refresh Posts"))
.accessibilityHint(Text("Fetch changes from the server"))
.disabled(!model.account.isLoggedIn)
} else {
Image(systemName: "wifi.exclamationmark")
.padding(.vertical, 4)
.padding(.horizontal, 8)
.foregroundColor(.secondary)
}
}
}
.padding(.top, 8)
.padding(.horizontal, 8)
Spacer()
}
.frame(height: frameHeight)
.background(Color(UIColor.systemGray5))
.overlay(Divider(), alignment: .top)
}
.ignoresSafeArea(.all, edges: .bottom)
.onAppear {
// Set the selected collection and whether or not we want to show all posts
model.navState.selectedCollection = selectedCollection
model.navState.showAllPosts = showAllPosts
// We use this to invalidate and refresh the view, so that new posts created outside of the app (e.g.,
// in the action extension) show up.
withAnimation {
self.filteredListViewId += 1
}
}
.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
}
}
#else
PostListFilteredView(
collection: selectedCollection,
showAllPosts: showAllPosts,
postCount: $postCount
)
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
- if model.selectedPost != nil {
- ActivePostToolbarView(activePost: model.selectedPost!)
+ if model.navState.selectedPost != nil {
+ ActivePostToolbarView(activePost: model.navState.selectedPost!)
}
}
}
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.onAppear {
- model.selectedCollection = selectedCollection
- model.showAllPosts = showAllPosts
+ model.navState.selectedCollection = selectedCollection
+ model.navState.showAllPosts = showAllPosts
}
.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
}
}
#endif
}
}
struct PostListView_Previews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return PostListView(showAllPosts: true)
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/macOS/Navigation/ActivePostToolbarView.swift b/macOS/Navigation/ActivePostToolbarView.swift
index 911689d..bc454fe 100644
--- a/macOS/Navigation/ActivePostToolbarView.swift
+++ b/macOS/Navigation/ActivePostToolbarView.swift
@@ -1,152 +1,152 @@
import SwiftUI
struct ActivePostToolbarView: View {
@EnvironmentObject var model: WriteFreelyModel
@ObservedObject var activePost: WFAPost
@State private var isPresentingSharingServicePicker: 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 {
HStack {
if model.account.isLoggedIn &&
activePost.status != PostStatus.local.rawValue &&
!(activePost.wasDeletedFromServer || activePost.hasNewerRemoteCopy) {
Section(header: Text("Move To:")) {
Picker(selection: $selectedCollection, label: Text("Move To…"), content: {
Text("\(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")")
.tag(nil as WFACollection?)
Divider()
ForEach(collections) { collection in
Text("\(collection.title)").tag(collection as WFACollection?)
}
})
}
}
PostEditorStatusToolbarView(post: activePost)
.frame(minWidth: 50, alignment: .center)
.layoutPriority(1)
.padding(.horizontal)
if activePost.status == PostStatus.edited.rawValue {
Button(action: {
model.editor.postToUpdate = activePost
model.updateFromServer(post: activePost)
- model.selectedPost = nil
+ model.navState.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 {
Menu(content: {
Label("Publish To:", systemImage: "paperplane")
Divider()
Button(action: {
if model.account.isLoggedIn {
withAnimation {
activePost.collectionAlias = nil
publishPost(activePost)
}
} else {
openSettingsWindow()
}
}, label: {
Text("\(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")")
})
ForEach(collections) { collection in
Button(action: {
if model.account.isLoggedIn {
withAnimation {
activePost.collectionAlias = collection.alias
publishPost(activePost)
}
} else {
openSettingsWindow()
}
}, label: {
Text("\(collection.title)")
})
}
}, label: {
Label("Publish…", systemImage: "paperplane")
})
- .disabled(model.selectedPost?.body.isEmpty ?? true)
+ .disabled(model.navState.selectedPost?.body.isEmpty ?? true)
.help("Publish the post to the web.\(model.account.isLoggedIn ? "" : " You must be logged in to do this.")") // swiftlint:disable:this line_length
} else {
HStack(spacing: 4) {
Button(
action: {
self.isPresentingSharingServicePicker = true
},
label: { Image(systemName: "square.and.arrow.up") }
)
.disabled(activePost.status == PostStatus.local.rawValue)
.help("Copy the post's URL to your Mac's pasteboard.")
.background(
PostEditorSharingPicker(
isPresented: $isPresentingSharingServicePicker,
sharingItems: createPostUrl()
)
)
Button(action: { publishPost(activePost) }, label: { Image(systemName: "paperplane") })
.disabled(activePost.body.isEmpty || activePost.status == PostStatus.published.rawValue)
.help("Publish the post to the web.\(model.account.isLoggedIn ? "" : " You must be logged in to do this.")") // swiftlint:disable:this line_length
}
}
}
.onAppear(perform: {
self.selectedCollection = collections.first { $0.alias == activePost.collectionAlias }
})
.onChange(of: selectedCollection, perform: { [selectedCollection] newCollection in
if activePost.collectionAlias == newCollection?.alias {
return
} else {
withAnimation {
activePost.collectionAlias = newCollection?.alias
model.move(post: activePost, from: selectedCollection, to: newCollection)
}
}
})
}
private func createPostUrl() -> [NSURL] {
- guard let postId = model.selectedPost?.postId else { return [] }
+ guard let postId = model.navState.selectedPost?.postId else { return [] }
var urlString: String
- if let postSlug = model.selectedPost?.slug,
- let postCollectionAlias = model.selectedPost?.collectionAlias {
+ if let postSlug = model.navState.selectedPost?.slug,
+ let postCollectionAlias = model.navState.selectedPost?.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 [] }
return [data as NSURL]
}
private func publishPost(_ post: WFAPost) {
- if post != model.selectedPost {
+ if post != model.navState.selectedPost {
return
}
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
model.publish(post: post)
}
model.editor.setInitialValues(for: post)
}
private func openSettingsWindow() {
guard let menuItem = NSApplication.shared.mainMenu?.item(at: 0)?.submenu?.item(at: 2) else { return }
NSApplication.shared.sendAction(menuItem.action!, to: menuItem.target, from: nil)
}
}
diff --git a/macOS/Navigation/HelpCommands.swift b/macOS/Navigation/HelpCommands.swift
index c465aeb..d2cff07 100644
--- a/macOS/Navigation/HelpCommands.swift
+++ b/macOS/Navigation/HelpCommands.swift
@@ -1,47 +1,47 @@
import SwiftUI
struct HelpCommands: Commands {
@ObservedObject var model: WriteFreelyModel
private let logger = Logging(for: String(describing: PostCommands.self))
var body: some Commands {
CommandGroup(replacing: .help) {
Button("Visit Support Forum") {
NSWorkspace().open(model.helpURL)
}
Button(action: createLogsPost, label: { Text("Generate Log for Support") })
}
}
private func createLogsPost() {
logger.log("Generating local log post...")
// Show the spinner going in the post list
model.isProcessingRequest = true
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Unset selected post and collection and navigate to local drafts.
- self.model.selectedPost = nil
- self.model.selectedCollection = nil
- self.model.showAllPosts = false
+ self.model.navState.selectedPost = nil
+ self.model.navState.selectedCollection = nil
+ self.model.navState.showAllPosts = false
// Create the new log post.
let newLogPost = model.editor.generateNewLocalPost(withFont: 2)
newLogPost.title = "Logs For Support"
var postBody: [String] = [
"WriteFreely-Multiplatform v\(Bundle.main.appMarketingVersion) (\(Bundle.main.appBuildVersion))",
"Generated \(Date())",
""
]
postBody.append(contentsOf: logger.fetchLogs())
newLogPost.body = postBody.joined(separator: "\n")
// Hide the spinner in the post list and set the log post as active
self.model.isProcessingRequest = false
- self.model.selectedPost = newLogPost
+ self.model.navState.selectedPost = newLogPost
logger.log("Generated local log post.")
}
}
}
diff --git a/macOS/Navigation/PostCommands.swift b/macOS/Navigation/PostCommands.swift
index 1a2b7b6..0c8bc32 100644
--- a/macOS/Navigation/PostCommands.swift
+++ b/macOS/Navigation/PostCommands.swift
@@ -1,35 +1,35 @@
import SwiftUI
struct PostCommands: Commands {
@ObservedObject var model: WriteFreelyModel
var body: some Commands {
CommandMenu("Post") {
Button("Find In Posts") {
if let toolbar = NSApp.keyWindow?.toolbar,
let search = toolbar.items.first(where: {
$0.itemIdentifier.rawValue == "com.apple.SwiftUI.search"
}) as? NSSearchToolbarItem {
search.beginSearchInteraction()
}
}
.keyboardShortcut("f", modifiers: [.command, .shift])
Group {
Button(action: sendPostUrlToPasteboard, label: { Text("Copy Link To Published Post") })
- .disabled(model.selectedPost?.status == PostStatus.local.rawValue)
+ .disabled(model.navState.selectedPost?.status == PostStatus.local.rawValue)
}
- .disabled(model.selectedPost == nil || !model.account.isLoggedIn)
+ .disabled(model.navState.selectedPost == nil || !model.account.isLoggedIn)
}
}
private func sendPostUrlToPasteboard() {
- guard let activePost = model.selectedPost else { return }
+ guard let activePost = model.navState.selectedPost else { return }
guard let postId = activePost.postId else { return }
guard let urlString = activePost.slug != nil ?
"\(model.account.server)/\((activePost.collectionAlias)!)/\((activePost.slug)!)" :
"\(model.account.server)/\((postId))" else { return }
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(urlString, forType: .string)
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jan 31, 9:37 AM (1 h, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3144790
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment