Page MenuHomeMusing Studio

No OneTemporary

diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift
index b412fdb..88ae92b 100644
--- a/Shared/ErrorHandling/ErrorConstants.swift
+++ b/Shared/ErrorHandling/ErrorConstants.swift
@@ -1,169 +1,173 @@
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: - User Defaults Errors
enum UserDefaultsError: Error {
case couldNotMigrateStandardDefaults
}
extension UserDefaultsError: LocalizedError {
public var errorDescription: String? {
switch self {
case .couldNotMigrateStandardDefaults:
return NSLocalizedString("Could not migrate user defaults to group container", comment: "")
}
}
}
// MARK: - Local Store Errors
enum LocalStoreError: Error {
case couldNotSaveContext
case couldNotFetchCollections
case couldNotFetchPosts(String = "")
- case couldNotPurgePublishedPosts
+ case couldNotPurgePosts(String = "")
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 .couldNotPurgePosts(let postFilter):
+ if postFilter.isEmpty {
+ return NSLocalizedString("Failed to purge \(postFilter) posts from local store.", comment: "")
+ } else {
+ return NSLocalizedString("Failed to purge 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(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(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 c1d19d9..b6dc7f3 100644
--- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
+++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
@@ -1,261 +1,269 @@
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()
+ do {
+ 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()
LocalStorageManager.standard.purgeUserCollections()
- self.posts.purgePublishedPosts()
+ do {
+ 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)
}
}
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."
)
}
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")
}
}
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()
guard let cachedPost = self.selectedPost else { return }
importData(from: fetchedPost, into: cachedPost)
cachedPost.hasNewerRemoteCopy = false
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} 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 = 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/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift
index db0ff4a..edd545c 100644
--- a/Shared/PostList/PostListModel.swift
+++ b/Shared/PostList/PostListModel.swift
@@ -1,126 +1,126 @@
import SwiftUI
import CoreData
class PostListModel: ObservableObject {
func remove(_ post: WFAPost) {
withAnimation {
LocalStorageManager.standard.container.viewContext.delete(post)
LocalStorageManager.standard.saveContext()
}
}
- func purgePublishedPosts() {
+ func purgePublishedPosts() throws {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFAPost")
fetchRequest.predicate = NSPredicate(format: "status != %i", 0)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try LocalStorageManager.standard.container.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch {
- print("Error: Failed to purge cached posts.")
+ throw LocalStoreError.couldNotPurgePosts("cached")
}
}
func getBodyPreview(of post: WFAPost) -> String {
var elidedPostBody: String = ""
// Strip any markdown from the post body.
let strippedPostBody = stripMarkdown(from: post.body)
// Extract lede from post.
elidedPostBody = extractLede(from: strippedPostBody)
return elidedPostBody
}
}
private extension PostListModel {
func stripMarkdown(from string: String) -> String {
var strippedString = string
strippedString = stripHeadingOctothorpes(from: strippedString)
strippedString = stripImages(from: strippedString, keepAltText: true)
return strippedString
}
func stripHeadingOctothorpes(from string: String) -> String {
let newLines = CharacterSet.newlines
var processedComponents: [String] = []
let components = string.components(separatedBy: newLines)
for component in components {
if component.isEmpty {
continue
}
var newString = component
while newString.first == "#" {
newString.removeFirst()
}
if newString.hasPrefix(" ") {
newString.removeFirst()
}
processedComponents.append(newString)
}
let headinglessString = processedComponents.joined(separator: "\n\n")
return headinglessString
}
func stripImages(from string: String, keepAltText: Bool = false) -> String {
let pattern = #"!\[[\"]?(.*?)[\"|]?\]\(.*?\)"#
var processedComponents: [String] = []
let components = string.components(separatedBy: .newlines)
for component in components {
if component.isEmpty { continue }
var processedString: String = component
if keepAltText {
let regex = try? NSRegularExpression(pattern: pattern, options: [])
if let matches = regex?.matches(
in: component, options: [], range: NSRange(location: 0, length: component.utf16.count)
) {
for match in matches {
if let range = Range(match.range(at: 1), in: component) {
processedString = "\(component[range])"
}
}
}
} else {
let range = component.startIndex..<component.endIndex
processedString = component.replacingOccurrences(
of: pattern,
with: "",
options: .regularExpression,
range: range
)
}
if processedString.isEmpty { continue }
processedComponents.append(processedString)
}
return processedComponents.joined(separator: "\n\n")
}
func extractLede(from string: String) -> String {
let truncatedString = string.prefix(80)
let terminatingPunctuation = ".。?"
let terminatingCharacters = CharacterSet(charactersIn: terminatingPunctuation).union(.newlines)
var lede: String = ""
let sentences = truncatedString.components(separatedBy: terminatingCharacters)
if let firstSentence = (sentences.filter { !$0.isEmpty }).first {
if truncatedString.count > firstSentence.count {
if terminatingPunctuation.contains(truncatedString[firstSentence.endIndex]) {
lede = String(truncatedString[...firstSentence.endIndex])
} else {
lede = firstSentence
}
} else if truncatedString.count == firstSentence.count {
if string.count > 80 {
if let endOfStringIndex = truncatedString.lastIndex(of: " ") {
lede = truncatedString[..<endOfStringIndex] + "…"
}
} else {
lede = firstSentence
}
}
}
return lede
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 6, 4:47 AM (1 h, 47 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3474175

Event Timeline