diff --git a/Mlem/API/Internal/HierarchicalComment.swift b/Mlem/API/Internal/HierarchicalComment.swift index 47f72c9de..0a409b196 100644 --- a/Mlem/API/Internal/HierarchicalComment.swift +++ b/Mlem/API/Internal/HierarchicalComment.swift @@ -39,6 +39,16 @@ extension HierarchicalComment: Equatable { } } +extension HierarchicalComment: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(commentView.id) + hasher.combine(commentView.comment.updated) + hasher.combine(commentView.counts.upvotes) + hasher.combine(commentView.counts.downvotes) + hasher.combine(commentView.myVote) + } +} + extension [HierarchicalComment] { /// A method to insert an updated `APICommentView` into this array of `HierarchicalComment` /// - Parameter commentView: The `APICommentView` you wish to insert diff --git a/Mlem/Enums/Content Type.swift b/Mlem/Enums/Content Type.swift index 36616fc42..d87a43c16 100644 --- a/Mlem/Enums/Content Type.swift +++ b/Mlem/Enums/Content Type.swift @@ -8,5 +8,5 @@ import Foundation enum ContentType: Int, Codable { - case post, community, user + case post, comment, community, user } diff --git a/Mlem/Models/Trackers/Post Tracker.swift b/Mlem/Models/Trackers/Post Tracker.swift index 21384c40f..4dafb9d7c 100644 --- a/Mlem/Models/Trackers/Post Tracker.swift +++ b/Mlem/Models/Trackers/Post Tracker.swift @@ -165,7 +165,7 @@ class PostTracker: ObservableObject { } @MainActor - private func reset( + func reset( with newItems: [PostModel] = .init(), filteredWith filter: @escaping (_: PostModel) -> Bool = { _ in true } ) { diff --git a/Mlem/Repositories/PersonRepository.swift b/Mlem/Repositories/PersonRepository.swift index 4bc0e9537..34064a385 100644 --- a/Mlem/Repositories/PersonRepository.swift +++ b/Mlem/Repositories/PersonRepository.swift @@ -29,11 +29,24 @@ class PersonRepository { return users } - func loadDetails(for id: Int) async throws -> UserModel { + /// Gets the UserModel for a given user + /// - Parameter id: id of the user to get + /// - Returns: UserModel for the given user + func getUser(for id: Int) async throws -> UserModel { let response = try await apiClient.getPersonDetails(for: id, limit: 1, savedOnly: false) return UserModel(from: response.personView) } + /// Gets full user details for the given user + /// - Parameters: + /// - id: user id to get for + /// - limit: max number of content items to fetch + /// - savedOnly: if present, whether to fetch saved items; calling user must be the requested user + /// - Returns: GetPersonDetailsResponse for the given user + func getUserDetails(for id: Int, limit: Int, savedOnly: Bool = false) async throws -> GetPersonDetailsResponse { + try await apiClient.getPersonDetails(for: id, limit: limit, savedOnly: savedOnly) + } + func getUnreadCounts() async throws -> APIPersonUnreadCounts { do { return try await apiClient.getUnreadCount() diff --git a/Mlem/Views/Tabs/Profile/User View.swift b/Mlem/Views/Tabs/Profile/User View.swift index e4a86a7e2..aa611e43f 100644 --- a/Mlem/Views/Tabs/Profile/User View.swift +++ b/Mlem/Views/Tabs/Profile/User View.swift @@ -17,9 +17,11 @@ struct UserView: View { @Dependency(\.apiClient) var apiClient @Dependency(\.errorHandler) var errorHandler @Dependency(\.notifier) var notifier + @Dependency(\.personRepository) var personRepository // appstorage @AppStorage("shouldShowUserHeaders") var shouldShowUserHeaders: Bool = true + let internetSpeed: InternetSpeed // environment @EnvironmentObject var appState: AppState @@ -41,6 +43,8 @@ struct UserView: View { @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false + self.internetSpeed = internetSpeed + self._userID = State(initialValue: userID) self._userDetails = State(initialValue: userDetails) @@ -147,7 +151,7 @@ struct UserView: View { .navigationBarColor() .headerProminence(.standard) .refreshable { - await tryLoadUser() + await tryReloadUser() }.toolbar { ToolbarItem(placement: .navigationBarTrailing) { accountSwitcher @@ -190,28 +194,50 @@ struct UserView: View { } } .task(priority: .userInitiated) { - await tryLoadUser() + await tryReloadUser() } } - private func tryLoadUser() async { + // swiftlint:disable function_body_length + private func tryReloadUser() async { do { - let authoredContent = try await loadUser(savedItems: false) + let authoredContent = try await personRepository.getUserDetails(for: userID, limit: internetSpeed.pageSize) var savedContentData: GetPersonDetailsResponse? if isShowingOwnProfile() { - savedContentData = try await loadUser(savedItems: true) + savedContentData = try await personRepository.getUserDetails( + for: userID, + limit: internetSpeed.pageSize, + savedOnly: true + ) + } + + if isShowingOwnProfile(), let currentAccount = appState.currentActiveAccount { + // take this opportunity to update the users avatar url to catch changes + // we should be able to shift this down to the repository layer in the future so that we + // catch anytime the app loads the signed in users details from any location in the app 🤞 + // -> we'll need to find a way to stop the state changes this creates from cancelling other in-flight requests + let url = authoredContent.personView.person.avatarUrl + let updatedAccount = SavedAccount( + id: currentAccount.id, + instanceLink: currentAccount.instanceLink, + accessToken: currentAccount.accessToken, + username: currentAccount.username, + storedNickname: currentAccount.storedNickname, + avatarUrl: url + ) + appState.setActiveAccount(updatedAccount) } - privateCommentTracker.add(authoredContent.comments + privateCommentTracker.comments = authoredContent.comments .sorted(by: { $0.comment.published > $1.comment.published }) - .map { HierarchicalComment(comment: $0, children: [], parentCollapsed: false, collapsed: false) }) + .map { HierarchicalComment(comment: $0, children: [], parentCollapsed: false, collapsed: false) } - privatePostTracker.add(authoredContent.posts.map { PostModel(from: $0) }) + privatePostTracker.reset(with: authoredContent.posts.map { PostModel(from: $0) }) if let savedContent = savedContentData { - privateCommentTracker.add(savedContent.comments + privateCommentTracker.comments = savedContent.comments .sorted(by: { $0.comment.published > $1.comment.published }) - .map { HierarchicalComment(comment: $0, children: [], parentCollapsed: false, collapsed: false) }) + .map { HierarchicalComment(comment: $0, children: [], parentCollapsed: false, collapsed: false) } privatePostTracker.add(savedContent.posts.map { PostModel(from: $0) }) } @@ -224,7 +250,7 @@ struct UserView: View { } catch { if userDetails == nil { errorDetails = ErrorDetails(error: error, refresh: { - await tryLoadUser() + await tryReloadUser() return userDetails != nil }) } else { @@ -238,28 +264,7 @@ struct UserView: View { } } } - - private func loadUser(savedItems: Bool) async throws -> GetPersonDetailsResponse { - let response = try await apiClient.getPersonDetails(for: userID, limit: 20, savedOnly: savedItems) - - if isShowingOwnProfile(), let currentAccount = appState.currentActiveAccount { - // take this opportunity to update the users avatar url to catch changes - // we should be able to shift this down to the repository layer in the future so that we - // catch anytime the app loads the signed in users details from any location in the app 🤞 - let url = response.personView.person.avatarUrl - let updatedAccount = SavedAccount( - id: currentAccount.id, - instanceLink: currentAccount.instanceLink, - accessToken: currentAccount.accessToken, - username: currentAccount.username, - storedNickname: currentAccount.storedNickname, - avatarUrl: url - ) - appState.setActiveAccount(updatedAccount) - } - - return response - } + // swiftlint:enable function_body_length } // TODO: darknavi - Move these to a common area for reuse diff --git a/Mlem/Views/Tabs/Profile/UserFeedView.swift b/Mlem/Views/Tabs/Profile/UserFeedView.swift index f7d06917c..de0844a83 100644 --- a/Mlem/Views/Tabs/Profile/UserFeedView.swift +++ b/Mlem/Views/Tabs/Profile/UserFeedView.swift @@ -14,11 +14,17 @@ struct UserFeedView: View { @Binding var selectedTab: UserViewTab - struct FeedItem: Identifiable { - let id = UUID() + struct FeedItem: Identifiable, Hashable { + static func == (lhs: UserFeedView.FeedItem, rhs: UserFeedView.FeedItem) -> Bool { + lhs.hashValue == rhs.hashValue + } + + var id: Int { hashValue } + var uid: ContentModelIdentifier let published: Date let comment: HierarchicalComment? let post: PostModel? + let hashValue: Int } var body: some View { @@ -43,7 +49,7 @@ struct UserFeedView: View { } func content(_ feed: [FeedItem]) -> some View { - ForEach(feed) { feedItem in + ForEach(feed, id: \.uid) { feedItem in if let post = feedItem.post { postEntry(for: post) } @@ -81,7 +87,7 @@ struct UserFeedView: View { Divider() } } - .buttonStyle(.plain) + .buttonStyle(EmptyButtonStyle()) } private func commentEntry(for comment: HierarchicalComment) -> some View { @@ -126,7 +132,13 @@ struct UserFeedView: View { // Create Feed Items .map { - FeedItem(published: $0.commentView.comment.published, comment: $0, post: nil) + FeedItem( + uid: ContentModelIdentifier(contentType: .comment, contentId: $0.commentView.comment.id), + published: $0.commentView.comment.published, + comment: $0, + post: nil, + hashValue: $0.hashValue + ) } } @@ -145,7 +157,13 @@ struct UserFeedView: View { // Create Feed Items .map { - FeedItem(published: $0.post.published, comment: nil, post: $0) + FeedItem( + uid: ContentModelIdentifier(contentType: .post, contentId: $0.postId), + published: $0.post.published, + comment: nil, + post: $0, + hashValue: $0.hashValue + ) } }