mirror of
https://github.com/writeas/writefreely-swiftui-multiplatform.git
synced 2024-11-15 01:11:02 +00:00
Merge pull request #32 from writeas/reload-from-server
Implement reload-from-server
This commit is contained in:
commit
3751118f6e
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
118
Shared/PostEditor/PostEditorStatusToolbarView.swift
Normal file
118
Shared/PostEditor/PostEditorStatusToolbarView.swift
Normal 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
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 */,
|
||||
|
Loading…
Reference in New Issue
Block a user