Page MenuHomeMusing Studio

No OneTemporary

diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift
index 5843048..fe22f19 100644
--- a/Shared/Account/AccountView.swift
+++ b/Shared/Account/AccountView.swift
@@ -1,40 +1,40 @@
import SwiftUI
struct AccountView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
if model.account.isLoggedIn {
HStack {
Spacer()
AccountLogoutView()
.withErrorHandling()
Spacer()
}
.padding()
} else {
AccountLoginView()
.withErrorHandling()
.padding(.top)
}
EmptyView()
.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)
+ self.errorHandling.handle(error: AppError.genericError(""))
}
model.hasError = false
}
}
}
}
struct AccountLogin_Previews: PreviewProvider {
static var previews: some View {
AccountView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift
index eb1f037..2962246 100644
--- a/Shared/ErrorHandling/ErrorConstants.swift
+++ b/Shared/ErrorHandling/ErrorConstants.swift
@@ -1,150 +1,154 @@
import Foundation
// MARK: - Network Errors
enum NetworkError: Error {
case noConnectionError
}
extension NetworkError: LocalizedError {
public var errorDescription: String? {
switch self {
case .noConnectionError:
return NSLocalizedString(
"There is no internet connection at the moment. Please reconnect or try again later.",
comment: ""
)
}
}
}
// MARK: - Keychain Errors
enum KeychainError: Error {
case couldNotStoreAccessToken
case couldNotPurgeAccessToken
case couldNotFetchAccessToken
}
extension KeychainError: LocalizedError {
public var errorDescription: String? {
switch self {
case .couldNotStoreAccessToken:
return NSLocalizedString("There was a problem storing your access token in the Keychain.", comment: "")
case .couldNotPurgeAccessToken:
return NSLocalizedString("Something went wrong purging the token from the Keychain.", comment: "")
case .couldNotFetchAccessToken:
return NSLocalizedString("Something went wrong fetching the token from the Keychain.", comment: "")
}
}
}
// MARK: - Account Errors
enum AccountError: Error {
case invalidPassword
case usernameNotFound
case serverNotFound
case invalidServerURL
case unknownLoginError
case genericAuthError
}
extension AccountError: LocalizedError {
public var errorDescription: String? {
switch self {
case .serverNotFound:
return NSLocalizedString(
"The server could not be found. Please check the information you've entered and try again.",
comment: ""
)
case .invalidPassword:
return NSLocalizedString(
"Invalid password. Please check that you've entered your password correctly and try logging in again.",
comment: ""
)
case .usernameNotFound:
return NSLocalizedString(
"Username not found. Did you use your email address by mistake?",
comment: ""
)
case .invalidServerURL:
return NSLocalizedString(
"Please enter a valid instance domain name. It should look like \"https://example.com\" or \"write.as\".", // swiftlint:disable:this line_length
comment: ""
)
case .genericAuthError:
return NSLocalizedString("Something went wrong, please try logging in again.", comment: "")
case .unknownLoginError:
return NSLocalizedString("An unknown error occurred while trying to login.", comment: "")
}
}
}
// MARK: - Local Store Errors
enum LocalStoreError: Error {
case couldNotSaveContext
case couldNotFetchCollections
case couldNotFetchPosts(String)
case couldNotPurgePublishedPosts
case couldNotPurgeCollections
case couldNotLoadStore(String)
case couldNotMigrateStore(String)
case couldNotDeleteStoreAfterMigration(String)
case genericError(String)
}
extension LocalStoreError: LocalizedError {
public var errorDescription: String? {
switch self {
case .couldNotSaveContext:
return NSLocalizedString("Error saving context", comment: "")
case .couldNotFetchCollections:
return NSLocalizedString("Failed to fetch blogs from local store.", comment: "")
case .couldNotFetchPosts(let postFilter):
if postFilter.isEmpty {
return NSLocalizedString("Failed to fetch posts from local store.", comment: "")
} else {
return NSLocalizedString("Failed to fetch \(postFilter) posts from local store.", comment: "")
}
case .couldNotPurgePublishedPosts:
return NSLocalizedString("Failed to purge published posts from local store.", comment: "")
case .couldNotPurgeCollections:
return NSLocalizedString("Failed to purge cached collections", comment: "")
case .couldNotLoadStore(let errorDescription):
return NSLocalizedString("Something went wrong loading local store: \(errorDescription)", comment: "")
case .couldNotMigrateStore(let errorDescription):
return NSLocalizedString("Something went wrong migrating local store: \(errorDescription)", comment: "")
case .couldNotDeleteStoreAfterMigration(let errorDescription):
return NSLocalizedString("Something went wrong deleting old store: \(errorDescription)", comment: "")
case .genericError(let customContent):
if customContent.isEmpty {
return NSLocalizedString("Something went wrong accessing device storage", comment: "")
} else {
return NSLocalizedString(customContent, comment: "")
}
}
}
}
// MARK: - Application Errors
enum AppError: Error {
case couldNotGetLoggedInClient
case couldNotGetPostId
- case genericError
+ case genericError(String)
}
extension AppError: LocalizedError {
public var errorDescription: String? {
switch self {
case .couldNotGetLoggedInClient:
return NSLocalizedString("Something went wrong trying to access the WriteFreely client.", comment: "")
case .couldNotGetPostId:
return NSLocalizedString("Something went wrong trying to get the post's unique ID.", comment: "")
- case .genericError:
- return NSLocalizedString("Something went wrong", comment: "")
+ case .genericError(let customContent):
+ if customContent.isEmpty {
+ return NSLocalizedString("Something went wrong", comment: "")
+ } else {
+ return NSLocalizedString(customContent, comment: "")
+ }
}
}
}
diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
index 6d0d147..c1d19d9 100644
--- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
+++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
@@ -1,257 +1,261 @@
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()
LocalStorageManager.standard.purgeUserCollections()
self.posts.purgePublishedPosts()
}
} catch {
- print(KeychainError.couldNotPurgeAccessToken.localizedDescription)
+ 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()
LocalStorageManager.standard.purgeUserCollections()
self.posts.purgePublishedPosts()
}
} catch {
- print(KeychainError.couldNotPurgeAccessToken.localizedDescription)
+ 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 {
- print(error)
+ self.currentError = AppError.genericError(error.localizedDescription)
}
}
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 { print("Error: could not determine which copy of post is newer") }
+ } else {
+ self.currentError = AppError.genericError(
+ "Error updating post: could not determine which copy of post is newer."
+ )
+ }
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 {
- print(error)
+ self.currentError = AppError.genericError(error.localizedDescription)
}
} catch WFError.unauthorized {
self.currentError = AccountError.genericAuthError
self.logout()
} catch {
- print("Error: Failed to fetch cached posts")
+ self.currentError = LocalStoreError.couldNotFetchPosts("cached")
}
}
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 {
- print("Error: Failed to fetch cached posts")
+ self.currentError = LocalStoreError.couldNotFetchPosts("cached")
}
}
} catch {
- print(error)
+ 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()
guard let cachedPost = self.selectedPost else { return }
importData(from: fetchedPost, into: cachedPost)
cachedPost.hasNewerRemoteCopy = false
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch {
- print(error)
+ 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 = selectedPost {
updateFromServer(post: post)
} else {
return
}
}
} catch {
DispatchQueue.main.async {
LocalStorageManager.standard.container.viewContext.rollback()
}
- print(error)
+ 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 f1e82a0..ca13ff5 100644
--- a/Shared/Navigation/ContentView.swift
+++ b/Shared/Navigation/ContentView.swift
@@ -1,88 +1,88 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
NavigationView {
#if os(macOS)
CollectionListView()
.withErrorHandling()
.toolbar {
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
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Load the new post in the editor
self.model.selectedPost = managedPost
}
}
}, label: { Image(systemName: "square.and.pencil") })
.help("Create a new local draft.")
}
#else
CollectionListView()
.withErrorHandling()
#endif
#if os(macOS)
ZStack {
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
.withErrorHandling()
if model.isProcessingRequest {
ZStack {
Color(NSColor.controlBackgroundColor).opacity(0.75)
ProgressView()
}
}
}
#else
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
.withErrorHandling()
#endif
Text("Select a post, or create a new local draft.")
.foregroundColor(.secondary)
EmptyView()
.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)
+ self.errorHandling.handle(error: AppError.genericError(""))
}
model.hasError = false
}
}
}
.environmentObject(model)
}
}
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/PostList/PostListView.swift b/Shared/PostList/PostListView.swift
index 2158aa4..16e1f45 100644
--- a/Shared/PostList/PostListView.swift
+++ b/Shared/PostList/PostListView.swift
@@ -1,186 +1,186 @@
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) {
// 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.
ZStack {
Spacer()
Button(action: {
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
self.model.showAllPosts = false
self.model.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 {
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)
}
}
.padding(.top, 8)
.padding(.horizontal, 8)
Spacer()
}
.frame(height: frameHeight)
.background(Color(UIColor.systemGray5))
.overlay(Divider(), alignment: .top)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
// 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
}
}
}
.ignoresSafeArea(.all, edges: .bottom)
.onAppear {
model.selectedCollection = selectedCollection
model.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)
+ 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!)
}
}
}
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.onAppear {
model.selectedCollection = selectedCollection
model.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)
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 30, 2:57 AM (6 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3464905

Event Timeline