Merge pull request #32 from writeas/reload-from-server

Implement reload-from-server
This commit is contained in:
Angelo Stavrow 2020-09-08 09:15:37 -04:00 committed by GitHub
commit 3751118f6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 282 additions and 11 deletions

View File

@ -7,7 +7,7 @@ struct AccountLogoutView: View {
VStack {
Spacer()
VStack {
Text("Logged in as \(model.account.username ?? "Anonymous")")
Text("Logged in as \(model.account.username)")
Text("on \(model.account.server)")
}
Spacer()

View File

@ -7,10 +7,11 @@ enum PostStatus {
case published
}
class Post: Identifiable, ObservableObject {
class Post: Identifiable, ObservableObject, Hashable {
@Published var wfPost: WFPost
@Published var status: PostStatus
@Published var collection: PostCollection
@Published var hasNewerRemoteCopy: Bool = false
let id = UUID()
@ -38,6 +39,16 @@ class Post: Identifiable, ObservableObject {
}
}
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 testPost = Post(
title: "Test Post Title",

View File

@ -1,4 +1,5 @@
import Foundation
import WriteFreely
struct PostStore {
var posts: [Post]
@ -11,7 +12,55 @@ struct PostStore {
posts.append(post)
}
mutating func purge() {
mutating func purgeAllPosts() {
posts = []
}
mutating func update(_ post: Post) {
// Find the local copy in the store
let localCopy = posts.first(where: { $0.id == post.id })
// If there's a local copy, update the updatedDate property of its WFPost
if let localCopy = localCopy {
localCopy.wfPost.updatedDate = Date()
} else {
print("Error: Local copy not found")
}
}
mutating func replace(post: Post, with fetchedPost: WFPost) {
// Find the local copy in the store.
let localCopy = posts.first(where: { $0.id == post.id })
// Replace the local copy's wfPost property with the fetched copy.
if let localCopy = localCopy {
localCopy.wfPost = fetchedPost
DispatchQueue.main.async {
localCopy.hasNewerRemoteCopy = false
localCopy.status = .published
}
} else {
print("Error: Local copy not found")
}
}
mutating func updateStore(with fetchedPosts: [Post]) {
for fetchedPost in fetchedPosts {
// Find the local copy in the store.
let localCopy = posts.first(where: { $0.wfPost.postId == fetchedPost.wfPost.postId })
// If there's a local copy, check which is newer; if not, add the fetched post to the store.
if let localCopy = localCopy {
// We do not discard the local copy; we simply set the hasNewerRemoteCopy flag accordingly.
if let remoteCopyUpdatedDate = fetchedPost.wfPost.updatedDate,
let localCopyUpdatedDate = localCopy.wfPost.updatedDate {
localCopy.hasNewerRemoteCopy = remoteCopyUpdatedDate > localCopyUpdatedDate
} else {
print("Error: could not determine which copy of post is newer")
}
} else {
add(fetchedPost)
}
}
}
}

View File

@ -10,6 +10,7 @@ class WriteFreelyModel: ObservableObject {
@Published var store = PostStore()
@Published var collections = CollectionListModel(with: [])
@Published var isLoggingIn: Bool = false
@Published var selectedPost: Post?
private var client: WFClient?
private let defaults = UserDefaults.standard
@ -26,6 +27,25 @@ class WriteFreelyModel: ObservableObject {
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()
self.fetchUserPosts()
}
}
}
}
@ -80,6 +100,15 @@ extension WriteFreelyModel {
)
}
}
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 {
@ -121,7 +150,7 @@ private extension WriteFreelyModel {
DispatchQueue.main.async {
self.account.logout()
self.collections.clearUserCollection()
self.store.purge()
self.store.purgeAllPosts()
}
} catch {
print("Something went wrong purging the token from the Keychain.")
@ -136,7 +165,7 @@ private extension WriteFreelyModel {
DispatchQueue.main.async {
self.account.logout()
self.collections.clearUserCollection()
self.store.purge()
self.store.purgeAllPosts()
}
} catch {
print("Something went wrong purging the token from the Keychain.")
@ -176,6 +205,7 @@ private extension WriteFreelyModel {
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 {
@ -186,9 +216,10 @@ private extension WriteFreelyModel {
} else {
post = Post(wfPost: fetchedPost)
}
DispatchQueue.main.async {
self.store.add(post)
}
fetchedPostsArray.append(post)
}
DispatchQueue.main.async {
self.store.updateStore(with: fetchedPostsArray)
}
} catch {
print(error)
@ -209,6 +240,18 @@ private extension WriteFreelyModel {
print(error)
}
}
func updateFromServerHandler(result: Result<WFPost, Error>) {
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 {

View File

@ -0,0 +1,118 @@
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)
}
}
}
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

View File

@ -29,7 +29,7 @@ struct PostEditorView: View {
.padding()
.toolbar {
ToolbarItem(placement: .status) {
PostStatusBadgeView(post: post)
PostEditorStatusToolbarView(post: post)
}
ToolbarItem(placement: .primaryAction) {
Button(action: {
@ -47,6 +47,13 @@ struct PostEditorView: View {
addNewPostToStore()
}
})
.onDisappear(perform: {
if post.status == .edited {
DispatchQueue.main.async {
model.store.update(post)
}
}
})
}
private func checkIfNewPost() {
@ -68,9 +75,24 @@ struct PostEditorView_NewLocalDraftPreviews: PreviewProvider {
}
}
struct PostEditorView_ExistingPostPreviews: PreviewProvider {
struct PostEditorView_NewerLocalPostPreviews: PreviewProvider {
static var previews: some View {
PostEditorView(post: testPostData[0])
return PostEditorView(post: testPost)
.environmentObject(WriteFreelyModel())
}
}
struct PostEditorView_NewerRemotePostPreviews: PreviewProvider {
static var previews: some View {
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 PostEditorView(post: newerRemotePost)
.environmentObject(WriteFreelyModel())
}
}

View File

@ -48,9 +48,17 @@ struct PostListView: View {
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)
@ -78,6 +86,12 @@ struct PostListView: View {
}, label: {
Image(systemName: "square.and.pencil")
})
Button(action: {
reloadFromServer()
}, label: {
Image(systemName: "arrow.clockwise")
})
.disabled(!model.account.isLoggedIn)
}
#endif
}
@ -99,6 +113,14 @@ struct PostListView: View {
}
}
}
private func reloadFromServer() {
DispatchQueue.main.async {
model.collections.clearUserCollection()
model.fetchUserCollections()
model.fetchUserPosts()
}
}
}
struct PostList_Previews: PreviewProvider {

View File

@ -34,6 +34,8 @@
1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostListView.swift */; };
1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostListView.swift */; };
1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE8024CB844500FD7257 /* View+Keyboard.swift */; };
1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */; };
1756DBB424FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */; };
1762DCB324EB086C0019C4EB /* CollectionListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1762DCB224EB086C0019C4EB /* CollectionListModel.swift */; };
1762DCB424EB086C0019C4EB /* CollectionListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1762DCB224EB086C0019C4EB /* CollectionListModel.swift */; };
1765F62A24E18EA200C9EBF0 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1765F62924E18EA200C9EBF0 /* SidebarView.swift */; };
@ -90,6 +92,7 @@
1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = "<group>"; };
1756AE7924CB65DF00FD7257 /* PostListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListView.swift; sourceTree = "<group>"; };
1756AE8024CB844500FD7257 /* View+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Keyboard.swift"; sourceTree = "<group>"; };
1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorStatusToolbarView.swift; sourceTree = "<group>"; };
1762DCB224EB086C0019C4EB /* CollectionListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionListModel.swift; sourceTree = "<group>"; };
1765F62924E18EA200C9EBF0 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
17A5388724DDA31F00DEFF9A /* MacAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAccountView.swift; sourceTree = "<group>"; };
@ -166,6 +169,7 @@
isa = PBXGroup;
children = (
1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */,
1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */,
);
path = PostEditor;
sourceTree = "<group>";
@ -546,6 +550,7 @@
17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */,
17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */,
171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */,
1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */,
17120DB224E1E19C002B9F6C /* SettingsHeaderView.swift in Sources */,
1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */,
17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */,
@ -587,6 +592,7 @@
1762DCB424EB086C0019C4EB /* CollectionListModel.swift in Sources */,
17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */,
1756AE6F24CB255B00FD7257 /* PostStore.swift in Sources */,
1756DBB424FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */,
1756AE6C24CB1E4B00FD7257 /* Post.swift in Sources */,
17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */,
1756AE7524CB26FA00FD7257 /* PostCellView.swift in Sources */,