diff --git a/Shared/Models/Post.swift b/Shared/Models/Post.swift index 9f42d9f..0308698 100644 --- a/Shared/Models/Post.swift +++ b/Shared/Models/Post.swift @@ -1,112 +1,50 @@ import Foundation import WriteFreely enum PostStatus { case local case edited case published } class Post: Identifiable, ObservableObject, Hashable { @Published var wfPost: WFPost @Published var status: PostStatus - @Published var collection: PostCollection? + @Published var collection: WFACollection? @Published var hasNewerRemoteCopy: Bool = false let id = UUID() init( title: String = "Title", body: String = "Write your post here...", createdDate: Date = Date(), status: PostStatus = .draft, - collection: PostCollection? = nil + collection: WFACollection? = nil ) { self.wfPost = WFPost(body: body, title: title, createdDate: createdDate) self.status = status self.collection = collection } - convenience init(wfPost: WFPost, in collection: PostCollection? = nil) { + convenience init(wfPost: WFPost, in collection: WFACollection? = nil) { self.init( title: wfPost.title ?? "", body: wfPost.body, createdDate: wfPost.createdDate ?? Date(), status: .published, collection: collection ) self.wfPost = wfPost } } extension Post { static func == (lhs: Post, rhs: Post) -> Bool { return lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } } - -#if DEBUG -let userCollection1 = PostCollection(title: "Collection 1") -let userCollection2 = PostCollection(title: "Collection 2") -let userCollection3 = PostCollection(title: "Collection 3") - -let testPost = Post( - title: "Test Post Title", - body: """ - Here's some cool sample body text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ultrices \ - posuere dignissim. Vestibulum a libero tempor, lacinia nulla vitae, congue purus. Nunc ac nulla quam. Duis \ - tincidunt eros augue, et volutpat tortor pulvinar ut. Nullam sit amet maximus urna. Phasellus non dignissim lacus.\ - Nulla ac posuere ex. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec \ - non molestie mauris. Suspendisse potenti. Vivamus at erat turpis. - - Pellentesque porttitor gravida tincidunt. Sed vitae eros non metus aliquam hendrerit. Aliquam sed risus suscipit \ - turpis dictum dictum. Duis lacus lectus, dictum vel felis in, rhoncus fringilla felis. Nunc id dolor nisl. Aliquam \ - euismod purus elit. Nullam egestas neque leo, sed aliquet ligula ultrices nec. - """, - createdDate: Date() -) - -let testPostData = [ - Post( - title: "My First Post", - body: "Look at me, creating a first post! That's cool.", - createdDate: Date(timeIntervalSince1970: 1595429452), - status: .published, - collection: userCollection1 - ), - Post( - title: "Post 2: The Quickening", - body: "See, here's the rule about Highlander jokes: _there can be only one_.", - createdDate: Date(timeIntervalSince1970: 1595514125), - status: .edited, - collection: userCollection1 - ), - Post( - title: "The Post Revolutions", - body: "I can never keep the Matrix movie order straight. Why not just call them part 2 and part 3?", - createdDate: Date(timeIntervalSince1970: 1595600006) - ), - Post( - title: "Episode IV: A New Post", - body: "How many movies does this person watch? How many movie-title jokes will they make?", - createdDate: Date(timeIntervalSince1970: 1596219877), - status: .published, - collection: userCollection2 - ), - Post( - title: "Fast (Post) Five", - body: "Look, it was either a Fast and the Furious reference, or a Resident Evil reference." - ), - Post( - title: "Post: The Final Chapter", - body: "And there you have it, a Resident Evil movie reference.", - createdDate: Date(timeIntervalSince1970: 1596043684), - status: .edited, - collection: userCollection3 - ) -] -#endif diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 4096de8..67cfc64 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -1,327 +1,325 @@ import Foundation import WriteFreely import Security // MARK: - WriteFreelyModel class WriteFreelyModel: ObservableObject { @Published var account = AccountModel() @Published var preferences = PreferencesModel() @Published var store = PostStore() @Published var collections = CollectionListModel() @Published var isLoggingIn: Bool = false @Published var selectedPost: Post? private var client: WFClient? private let defaults = UserDefaults.standard init() { // Set the color scheme based on what's been saved in UserDefaults. DispatchQueue.main.async { self.preferences.appearance = self.defaults.integer(forKey: self.preferences.colorSchemeIntegerKey) } #if DEBUG // for post in testPostData { store.add(post) } #endif DispatchQueue.main.async { self.account.restoreState() if self.account.isLoggedIn { guard let serverURL = URL(string: self.account.server) else { print("Server URL not found") return } guard let token = self.fetchTokenFromKeychain( username: self.account.username, server: self.account.server ) else { print("Could not fetch token from Keychain") return } self.account.login(WFUser(token: token, username: self.account.username)) self.client = WFClient(for: serverURL) self.client?.user = self.account.user - self.collections.clearUserCollection() - self.fetchUserCollections() + if self.collections.userCollections.count == 0 { + self.fetchUserCollections() + } self.fetchUserPosts() } } } } // MARK: - WriteFreelyModel API extension WriteFreelyModel { func login(to server: URL, as username: String, password: String) { isLoggingIn = true account.server = server.absoluteString client = WFClient(for: server) client?.login(username: username, password: password, completion: loginHandler) } func logout() { guard let loggedInClient = client else { do { try purgeTokenFromKeychain(username: account.username, server: account.server) account.logout() } catch { fatalError("Failed to log out persisted state") } return } loggedInClient.logout(completion: logoutHandler) } func fetchUserCollections() { guard let loggedInClient = client else { return } loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler) } func fetchUserPosts() { guard let loggedInClient = client else { return } loggedInClient.getPosts(completion: fetchUserPostsHandler) } func publish(post: Post) { guard let loggedInClient = client else { return } if let existingPostId = post.wfPost.postId { // This is an existing post. loggedInClient.updatePost( postId: existingPostId, updatedPost: post.wfPost, completion: publishHandler ) } else { // This is a new local draft. loggedInClient.createPost( - post: post.wfPost, in: post.collection?.wfCollection?.alias, completion: publishHandler + post: post.wfPost, in: post.collection?.alias, completion: publishHandler ) } } func updateFromServer(post: Post) { guard let loggedInClient = client else { return } guard let postId = post.wfPost.postId else { return } DispatchQueue.main.async { self.selectedPost = post } loggedInClient.getPost(byId: postId, completion: updateFromServerHandler) } } private extension WriteFreelyModel { func loginHandler(result: Result) { DispatchQueue.main.async { self.isLoggingIn = false } do { let user = try result.get() fetchUserCollections() fetchUserPosts() saveTokenToKeychain(user.token, username: user.username, server: account.server) DispatchQueue.main.async { self.account.login(user) } } catch WFError.notFound { DispatchQueue.main.async { self.account.currentError = AccountError.usernameNotFound } } catch WFError.unauthorized { DispatchQueue.main.async { self.account.currentError = AccountError.invalidPassword } } catch { if (error as NSError).domain == NSURLErrorDomain, (error as NSError).code == -1003 { DispatchQueue.main.async { self.account.currentError = AccountError.serverNotFound } } } } func logoutHandler(result: Result) { do { _ = try result.get() do { try purgeTokenFromKeychain(username: account.user?.username, server: account.server) client = nil DispatchQueue.main.async { self.account.logout() self.collections.clearUserCollection() self.store.purgeAllPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } 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() self.collections.clearUserCollection() + self.account.logout() self.store.purgeAllPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } 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>) { + DispatchQueue.main.async { + self.collections.loadCachedUserCollections() + } do { let fetchedCollections = try result.get() - var fetchedCollectionsArray: [PostCollection] = [] for fetchedCollection in fetchedCollections { - let postCollection = PostCollection(title: fetchedCollection.title) - postCollection.wfCollection = fetchedCollection - fetchedCollectionsArray.append(postCollection) - DispatchQueue.main.async { let localCollection = WFACollection(context: PersistenceManager.persistentContainer.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 { -// self.collections = CollectionListModel(with: fetchedCollectionsArray) PersistenceManager().saveContext() } } catch { print(error) } } func fetchUserPostsHandler(result: Result<[WFPost], Error>) { do { let fetchedPosts = try result.get() var fetchedPostsArray: [Post] = [] for fetchedPost in fetchedPosts { var post: Post if let matchingAlias = fetchedPost.collectionAlias { - let postCollection = PostCollection(title: ( + let matchingCachedCollection = ( collections.userCollections.filter { $0.alias == matchingAlias } - ).first?.title ?? "NO TITLE") - post = Post(wfPost: fetchedPost, in: postCollection) + ).first + post = Post(wfPost: fetchedPost, in: matchingCachedCollection) } else { post = Post(wfPost: fetchedPost) } fetchedPostsArray.append(post) } DispatchQueue.main.async { self.store.updateStore(with: fetchedPostsArray) } } catch { print(error) } } func publishHandler(result: Result) { do { let wfPost = try result.get() let foundPostIndex = store.posts.firstIndex(where: { $0.wfPost.title == wfPost.title && $0.wfPost.body == wfPost.body }) guard let index = foundPostIndex else { return } DispatchQueue.main.async { self.store.posts[index].wfPost = wfPost } } catch { print(error) } } func updateFromServerHandler(result: Result) { do { let fetchedPost = try result.get() DispatchQueue.main.async { guard let selectedPost = self.selectedPost else { return } self.store.replace(post: selectedPost, with: fetchedPost) } } catch { print(error) } } } private extension WriteFreelyModel { // MARK: - Keychain Helpers func saveTokenToKeychain(_ token: String, username: String?, server: String) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecValueData as String: token.data(using: .utf8)!, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server ] let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecDuplicateItem || status == errSecSuccess else { fatalError("Error storing in Keychain with OSStatus: \(status)") } } func purgeTokenFromKeychain(username: String?, server: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { fatalError("Error deleting from Keychain with OSStatus: \(status)") } } func fetchTokenFromKeychain(username: String?, server: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, kSecReturnData as String: true ] var secItem: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &secItem) guard status != errSecItemNotFound else { return nil } guard status == errSecSuccess else { fatalError("Error fetching from Keychain with OSStatus: \(status)") } guard let existingSecItem = secItem as? [String: Any], let tokenData = existingSecItem[kSecValueData as String] as? Data, let token = String(data: tokenData, encoding: .utf8) else { return nil } return token } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 1213f1e..6121a89 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -1,38 +1,38 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var model: WriteFreelyModel var body: some View { NavigationView { SidebarView() - PostListView(selectedCollection: CollectionListModel.allPostsCollection) + PostListView(selectedCollection: nil) Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) } .environmentObject(model) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) userCollection1.title = "Collection 1" userCollection2.title = "Collection 2" userCollection3.title = "Collection 3" let model = WriteFreelyModel() model.collections = CollectionListModel() for post in testPostData { model.store.add(post) } return ContentView() .environmentObject(model) .environment(\.managedObjectContext, PersistenceManager.persistentContainer.viewContext) } } diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift index 4cd21cc..741cee7 100644 --- a/Shared/PostCollection/CollectionListModel.swift +++ b/Shared/PostCollection/CollectionListModel.swift @@ -1,25 +1,37 @@ import SwiftUI import CoreData class CollectionListModel: ObservableObject { @Published var userCollections = [WFACollection]() - static let allPostsCollection = PostCollection(title: "All Posts") - static let draftsCollection = PostCollection(title: "Drafts") - init() { -// let request = WFACollection.createFetchRequest() -// request.sortDescriptors = [] -// do { -// userCollections = try PersistenceManager.persistentContainer.viewContext.fetch(request) -// } catch { -// print("Error: Failed to fetch user collections from local store") -// userCollections = [] -// } + loadCachedUserCollections() + } + + func loadCachedUserCollections() { + let request = WFACollection.createFetchRequest() + let sort = NSSortDescriptor(key: "title", ascending: true) + request.sortDescriptors = [sort] + + userCollections = [] + do { + let cachedCollections = try PersistenceManager.persistentContainer.viewContext.fetch(request) + userCollections.append(contentsOf: cachedCollections) + } catch { + print("Error: Failed to fetch cached user collections.") + } } func clearUserCollection() { + // Make sure the userCollections property is properly populated. + // FIXME: Without this, sometimes the userCollections array is empty. + loadCachedUserCollections() + + for userCollection in userCollections { + PersistenceManager.persistentContainer.viewContext.delete(userCollection) + } + PersistenceManager().saveContext() + userCollections = [] - // Clear collections from CoreData store. } } diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index 6c71177..0485d8a 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -1,50 +1,53 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel @Environment(\.managedObjectContext) var moc - @FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults + @FetchRequest( + entity: WFACollection.entity(), + sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)] + ) var collections: FetchedResults var body: some View { List { - NavigationLink(destination: PostListView(selectedCollection: CollectionListModel.allPostsCollection)) { - Text(CollectionListModel.allPostsCollection.title) - } - NavigationLink(destination: PostListView(selectedCollection: CollectionListModel.draftsCollection)) { - Text(CollectionListModel.draftsCollection.title) +// NavigationLink(destination: PostListView(selectedCollection: CollectionListModel.allPostsCollection)) { +// Text(CollectionListModel.allPostsCollection.title) +// } + NavigationLink(destination: PostListView(selectedCollection: nil)) { + Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts") } Section(header: Text("Your Blogs")) { ForEach(collections, id: \.alias) { collection in NavigationLink( - destination: PostListView(selectedCollection: PostCollection(title: collection.title)) + destination: PostListView(selectedCollection: collection) ) { Text(collection.title) } } } } .navigationTitle("Collections") .listStyle(SidebarListStyle()) } } struct CollectionSidebar_Previews: PreviewProvider { @Environment(\.managedObjectContext) var moc static var previews: some View { let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) userCollection1.title = "Collection 1" userCollection2.title = "Collection 2" userCollection3.title = "Collection 3" let model = WriteFreelyModel() model.collections = CollectionListModel() return CollectionListView() .environmentObject(model) .environment(\.managedObjectContext, PersistenceManager.persistentContainer.viewContext) } } diff --git a/Shared/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift index ca8ea77..8c09962 100644 --- a/Shared/PostEditor/PostEditorStatusToolbarView.swift +++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift @@ -1,118 +1,136 @@ import SwiftUI struct PostEditorStatusToolbarView: View { #if os(iOS) @Environment(\.horizontalSizeClass) var horizontalSizeClass #endif @EnvironmentObject var model: WriteFreelyModel @ObservedObject var post: Post var body: some View { if post.hasNewerRemoteCopy { #if os(iOS) if horizontalSizeClass == .compact { VStack { PostStatusBadgeView(post: post) HStack { Text("⚠️ Newer copy on server. Replace local copy?") .font(.caption) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } .padding(.bottom) } .padding(.top) } else { HStack { PostStatusBadgeView(post: post) .padding(.trailing) Text("⚠️ Newer copy on server. Replace local copy?") .font(.callout) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } } #else HStack { PostStatusBadgeView(post: post) .padding(.trailing) Text("⚠️ Newer copy on server. Replace local copy?") .font(.callout) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } #endif } else { PostStatusBadgeView(post: post) } } } +#if DEBUG +let testPost = Post( + title: "Test Post Title", + body: """ + Here's some cool sample body text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ultrices \ + posuere dignissim. Vestibulum a libero tempor, lacinia nulla vitae, congue purus. Nunc ac nulla quam. Duis \ + tincidunt eros augue, et volutpat tortor pulvinar ut. Nullam sit amet maximus urna. Phasellus non dignissim lacus.\ + Nulla ac posuere ex. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec \ + non molestie mauris. Suspendisse potenti. Vivamus at erat turpis. + + Pellentesque porttitor gravida tincidunt. Sed vitae eros non metus aliquam hendrerit. Aliquam sed risus suscipit \ + turpis dictum dictum. Duis lacus lectus, dictum vel felis in, rhoncus fringilla felis. Nunc id dolor nisl. Aliquam \ + euismod purus elit. Nullam egestas neque leo, sed aliquet ligula ultrices nec. + """, + createdDate: Date() +) +#endif + struct ToolbarView_LocalPreviews: PreviewProvider { static var previews: some View { let model = WriteFreelyModel() let post = testPost return PostEditorStatusToolbarView(post: post) .environmentObject(model) } } struct ToolbarView_RemotePreviews: PreviewProvider { static var previews: some View { let model = WriteFreelyModel() let newerRemotePost = Post( title: testPost.wfPost.title ?? "", body: testPost.wfPost.body, createdDate: testPost.wfPost.createdDate ?? Date(), status: testPost.status, collection: testPost.collection ) newerRemotePost.hasNewerRemoteCopy = true return PostEditorStatusToolbarView(post: newerRemotePost) .environmentObject(model) } } #if os(iOS) struct ToolbarView_CompactLocalPreviews: PreviewProvider { static var previews: some View { let model = WriteFreelyModel() let post = testPost return PostEditorStatusToolbarView(post: post) .environmentObject(model) .environment(\.horizontalSizeClass, .compact) } } #endif #if os(iOS) struct ToolbarView_CompactRemotePreviews: PreviewProvider { static var previews: some View { let model = WriteFreelyModel() let newerRemotePost = Post( title: testPost.wfPost.title ?? "", body: testPost.wfPost.body, createdDate: testPost.wfPost.createdDate ?? Date(), status: testPost.status, collection: testPost.collection ) newerRemotePost.hasNewerRemoteCopy = true return PostEditorStatusToolbarView(post: newerRemotePost) .environmentObject(model) .environment(\.horizontalSizeClass, .compact) } } #endif diff --git a/Shared/PostList/PostCellView.swift b/Shared/PostList/PostCellView.swift index 8bb68cf..00122ff 100644 --- a/Shared/PostList/PostCellView.swift +++ b/Shared/PostList/PostCellView.swift @@ -1,35 +1,40 @@ import SwiftUI struct PostCellView: View { @ObservedObject var post: Post var body: some View { HStack { VStack(alignment: .leading) { Text(post.wfPost.title ?? "") .font(.headline) .lineLimit(1) Text(buildDateString(from: post.wfPost.createdDate ?? Date())) .font(.caption) .lineLimit(1) } Spacer() PostStatusBadgeView(post: post) } .padding(5) } func buildDateString(from date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .long dateFormatter.timeStyle = .short return dateFormatter.string(from: date) } } struct PostCell_Previews: PreviewProvider { static var previews: some View { - PostCellView(post: testPost) + let testPost = Post( + title: "Test Post Title", + body: "Here's some cool sample body text.", + createdDate: Date() + ) + return PostCellView(post: testPost) } } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index bffd1f8..d5e96eb 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -1,139 +1,202 @@ import SwiftUI struct PostListView: View { @EnvironmentObject var model: WriteFreelyModel - @State var selectedCollection: PostCollection + @State var selectedCollection: WFACollection? #if os(iOS) @State private var isPresentingSettings = false #endif var body: some View { #if os(iOS) GeometryReader { geometry in List { ForEach(showPosts(for: selectedCollection)) { post in NavigationLink( destination: PostEditorView(post: post) ) { PostCellView( post: post ) } } } .environmentObject(model) - .navigationTitle(selectedCollection.title) + .navigationTitle( + selectedCollection?.title ?? (model.account.server == "https://write.as" ? "Anonymous" : "Drafts") + ) .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { let post = Post() model.store.add(post) }, label: { Image(systemName: "square.and.pencil") }) } ToolbarItem(placement: .bottomBar) { HStack { Button(action: { isPresentingSettings = true }, label: { Image(systemName: "gear") }).sheet( isPresented: $isPresentingSettings, onDismiss: { isPresentingSettings = false }, content: { SettingsView(isPresented: $isPresentingSettings) } ) .padding(.leading) Spacer() Text(pluralizedPostCount(for: showPosts(for: selectedCollection))) .foregroundColor(.secondary) Spacer() Button(action: { reloadFromServer() }, label: { Image(systemName: "arrow.clockwise") }) .disabled(!model.account.isLoggedIn) } .padding() .frame(width: geometry.size.width) } } } #else //if os(macOS) List { ForEach(showPosts(for: selectedCollection)) { post in NavigationLink( destination: PostEditorView(post: post) ) { PostCellView( post: post ) } } } - .navigationTitle(selectedCollection.title) + .navigationTitle( + selectedCollection?.title ?? (model.account.server == "https://write.as" ? "Anonymous" : "Drafts") + ) .navigationSubtitle(pluralizedPostCount(for: showPosts(for: selectedCollection))) .toolbar { Button(action: { let post = Post() model.store.add(post) }, label: { Image(systemName: "square.and.pencil") }) Button(action: { reloadFromServer() }, label: { Image(systemName: "arrow.clockwise") }) .disabled(!model.account.isLoggedIn) } #endif } private func pluralizedPostCount(for posts: [Post]) -> String { if posts.count == 1 { return "1 post" } else { return "\(posts.count) posts" } } - private func showPosts(for collection: PostCollection) -> [Post] { + private func showPosts(for collection: WFACollection?) -> [Post] { var posts: [Post] - if collection == CollectionListModel.allPostsCollection { - posts = model.store.posts - } else if collection == CollectionListModel.draftsCollection { - posts = model.store.posts.filter { $0.collection == nil } + + if let selectedCollection = collection { + posts = model.store.posts.filter { $0.wfPost.collectionAlias == selectedCollection.alias } } else { - posts = model.store.posts.filter { $0.collection?.title == collection.title } + posts = model.store.posts.filter { $0.wfPost.collectionAlias == nil } } + +// for post in model.store.posts { +// print("Post '\(post.wfPost.title ?? "Untitled")' in \(post.collection?.title ?? "No collection")") +// } +// if collection == CollectionListModel.allPostsCollection { +// posts = model.store.posts +// } else if collection == CollectionListModel.draftsCollection { +// posts = model.store.posts.filter { $0.collection == nil } +// } else { +// posts = model.store.posts.filter { $0.collection == collection } +// } + return posts } private func reloadFromServer() { DispatchQueue.main.async { model.collections.clearUserCollection() model.fetchUserCollections() model.fetchUserPosts() } } } struct PostList_Previews: PreviewProvider { static var previews: some View { + let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + + userCollection1.title = "Collection 1" + userCollection2.title = "Collection 2" + userCollection3.title = "Collection 3" + + let testPostData = [ + Post( + title: "My First Post", + body: "Look at me, creating a first post! That's cool.", + createdDate: Date(timeIntervalSince1970: 1595429452), + status: .published, + collection: userCollection1 + ), + Post( + title: "Post 2: The Quickening", + body: "See, here's the rule about Highlander jokes: _there can be only one_.", + createdDate: Date(timeIntervalSince1970: 1595514125), + status: .edited, + collection: userCollection1 + ), + Post( + title: "The Post Revolutions", + body: "I can never keep the Matrix movie order straight. Why not just call them part 2 and part 3?", + createdDate: Date(timeIntervalSince1970: 1595600006) + ), + Post( + title: "Episode IV: A New Post", + body: "How many movies does this person watch? How many movie-title jokes will they make?", + createdDate: Date(timeIntervalSince1970: 1596219877), + status: .published, + collection: userCollection2 + ), + Post( + title: "Fast (Post) Five", + body: "Look, it was either a Fast and the Furious reference, or a Resident Evil reference." + ), + Post( + title: "Post: The Final Chapter", + body: "And there you have it, a Resident Evil movie reference.", + createdDate: Date(timeIntervalSince1970: 1596043684), + status: .edited, + collection: userCollection3 + ) + ] + let model = WriteFreelyModel() for post in testPostData { model.store.add(post) } return Group { - PostListView(selectedCollection: CollectionListModel.allPostsCollection) + PostListView(selectedCollection: userCollection1) .environmentObject(model) } } } diff --git a/Shared/PostList/PostStatusBadgeView.swift b/Shared/PostList/PostStatusBadgeView.swift index 3698fea..466d914 100644 --- a/Shared/PostList/PostStatusBadgeView.swift +++ b/Shared/PostList/PostStatusBadgeView.swift @@ -1,57 +1,103 @@ import SwiftUI struct PostStatusBadgeView: View { @ObservedObject var post: Post var body: some View { let (badgeLabel, badgeColor) = setupBadgeProperties(for: post.status) Text(badgeLabel) .font(.caption) .fontWeight(.semibold) .foregroundColor(.white) .textCase(.uppercase) .lineLimit(1) .padding(EdgeInsets(top: 2.5, leading: 7.5, bottom: 2.5, trailing: 7.5)) .background(badgeColor) .clipShape(RoundedRectangle(cornerRadius: 5.0, style: .circular)) } func setupBadgeProperties(for status: PostStatus) -> (String, Color) { var badgeLabel: String var badgeColor: Color switch status { case .local: badgeLabel = "local" badgeColor = Color(red: 0.75, green: 0.5, blue: 0.85, opacity: 1.0) case .edited: badgeLabel = "edited" badgeColor = Color(red: 0.75, green: 0.7, blue: 0.1, opacity: 1.0) case .published: badgeLabel = "published" badgeColor = .gray } return (badgeLabel, badgeColor) } } +#if DEBUG +let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) +let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) +let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext) + +let testPostData = [ + Post( + title: "My First Post", + body: "Look at me, creating a first post! That's cool.", + createdDate: Date(timeIntervalSince1970: 1595429452), + status: .published, + collection: userCollection1 + ), + Post( + title: "Post 2: The Quickening", + body: "See, here's the rule about Highlander jokes: _there can be only one_.", + createdDate: Date(timeIntervalSince1970: 1595514125), + status: .edited, + collection: userCollection1 + ), + Post( + title: "The Post Revolutions", + body: "I can never keep the Matrix movie order straight. Why not just call them part 2 and part 3?", + createdDate: Date(timeIntervalSince1970: 1595600006) + ), + Post( + title: "Episode IV: A New Post", + body: "How many movies does this person watch? How many movie-title jokes will they make?", + createdDate: Date(timeIntervalSince1970: 1596219877), + status: .published, + collection: userCollection2 + ), + Post( + title: "Fast (Post) Five", + body: "Look, it was either a Fast and the Furious reference, or a Resident Evil reference." + ), + Post( + title: "Post: The Final Chapter", + body: "And there you have it, a Resident Evil movie reference.", + createdDate: Date(timeIntervalSince1970: 1596043684), + status: .edited, + collection: userCollection3 + ) +] +#endif + struct PostStatusBadge_LocalDraftPreviews: PreviewProvider { static var previews: some View { + userCollection1.title = "Collection 1" PostStatusBadgeView(post: testPostData[2]) } } struct PostStatusBadge_EditedPreviews: PreviewProvider { static var previews: some View { - Group { - PostStatusBadgeView(post: testPostData[1]) - } + userCollection1.title = "Collection 1" + return PostStatusBadgeView(post: testPostData[1]) } } struct PostStatusBadge_PublishedPreviews: PreviewProvider { static var previews: some View { PostStatusBadgeView(post: testPostData[0]) } }