diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 3c44e2c02..7bc8c65e3 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 03EEEAF32AB8DCDF0087F8D8 /* CommunityResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EEEAF22AB8DCDF0087F8D8 /* CommunityResultView.swift */; }; 03EEEAF72AB8ED3C0087F8D8 /* SearchTabPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EEEAF62AB8ED3C0087F8D8 /* SearchTabPicker.swift */; }; 03EEEAF92ABB985D0087F8D8 /* CommunityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EEEAF82ABB985D0087F8D8 /* CommunityModel.swift */; }; + 03FD64FF2AE53D0E00957AA9 /* CommunityModel+ContentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD64FE2AE53D0E00957AA9 /* CommunityModel+ContentModel.swift */; }; 500C168E2A66FAAB006F243B /* HapticManager+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500C168D2A66FAAB006F243B /* HapticManager+Dependency.swift */; }; 5016A2B12A67EB8600B257E8 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5016A2B02A67EB8600B257E8 /* UIViewController.swift */; }; 5016A2B32A67EC0700B257E8 /* NotificationDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5016A2B22A67EC0700B257E8 /* NotificationDisplayer.swift */; }; @@ -243,7 +244,6 @@ 63D24EDC2A169F12005CCA81 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 63D24EDB2A169F12005CCA81 /* MarkdownUI */; }; 63D24EDE2A169F2A005CCA81 /* Markdown View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63D24EDD2A169F2A005CCA81 /* Markdown View.swift */; }; 63DF71F12A02999C002AC14E /* App Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF71F02A02999C002AC14E /* App Constants.swift */; }; - 63E5D38D2A13BCDE00EC1FBD /* Community Search Result Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E5D38C2A13BCDE00EC1FBD /* Community Search Result Tracker.swift */; }; 63E5D3922A13CF2300EC1FBD /* Favorite Community Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E5D3912A13CF2300EC1FBD /* Favorite Community Tracker.swift */; }; 63E5D3942A13CF3600EC1FBD /* Favorite Community.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E5D3932A13CF3600EC1FBD /* Favorite Community.swift */; }; 63F0C7A22A0519BA00A18C5D /* PostSortType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0C7A12A0519BA00A18C5D /* PostSortType.swift */; }; @@ -550,6 +550,7 @@ 03EEEAF22AB8DCDF0087F8D8 /* CommunityResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityResultView.swift; sourceTree = ""; }; 03EEEAF62AB8ED3C0087F8D8 /* SearchTabPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTabPicker.swift; sourceTree = ""; }; 03EEEAF82ABB985D0087F8D8 /* CommunityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityModel.swift; sourceTree = ""; }; + 03FD64FE2AE53D0E00957AA9 /* CommunityModel+ContentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+ContentModel.swift"; sourceTree = ""; }; 500C168D2A66FAAB006F243B /* HapticManager+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HapticManager+Dependency.swift"; sourceTree = ""; }; 5016A2B02A67EB8600B257E8 /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; 5016A2B22A67EC0700B257E8 /* NotificationDisplayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDisplayer.swift; sourceTree = ""; }; @@ -729,7 +730,6 @@ 63D24ED82A169A5F005CCA81 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; 63D24EDD2A169F2A005CCA81 /* Markdown View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Markdown View.swift"; sourceTree = ""; }; 63DF71F02A02999C002AC14E /* App Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App Constants.swift"; sourceTree = ""; }; - 63E5D38C2A13BCDE00EC1FBD /* Community Search Result Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Community Search Result Tracker.swift"; sourceTree = ""; }; 63E5D3912A13CF2300EC1FBD /* Favorite Community Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite Community Tracker.swift"; sourceTree = ""; }; 63E5D3932A13CF3600EC1FBD /* Favorite Community.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite Community.swift"; sourceTree = ""; }; 63F0C7A12A0519BA00A18C5D /* PostSortType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSortType.swift; sourceTree = ""; }; @@ -1088,22 +1088,22 @@ path = User; sourceTree = ""; }; - 030D00862AD1BB0D00953B1D /* User */ = { + 030D00832AD0842900953B1D /* Results */ = { isa = PBXGroup; children = ( - 03B7AAF22ABEF85200068B23 /* UserModel.swift */, - 030D00872AD1BB2600953B1D /* UserModel+ContentModel.swift */, + 03EEEAF22AB8DCDF0087F8D8 /* CommunityResultView.swift */, + 03B7AAF42ABEFA7A00068B23 /* UserResultView.swift */, ); - path = User; + path = Results; sourceTree = ""; }; - 030D00832AD0842900953B1D /* Results */ = { + 030D00862AD1BB0D00953B1D /* User */ = { isa = PBXGroup; children = ( - 03EEEAF22AB8DCDF0087F8D8 /* CommunityResultView.swift */, - 03B7AAF42ABEFA7A00068B23 /* UserResultView.swift */, + 03B7AAF22ABEF85200068B23 /* UserModel.swift */, + 030D00872AD1BB2600953B1D /* UserModel+ContentModel.swift */, ); - path = Results; + path = User; sourceTree = ""; }; 030E86422AC6F6CB000283A6 /* Search Bar */ = { @@ -1189,6 +1189,15 @@ path = TabBar; sourceTree = ""; }; + 03FD64FD2AE538C600957AA9 /* Community */ = { + isa = PBXGroup; + children = ( + 03EEEAF82ABB985D0087F8D8 /* CommunityModel.swift */, + 03FD64FE2AE53D0E00957AA9 /* CommunityModel+ContentModel.swift */, + ); + path = Community; + sourceTree = ""; + }; 504ECBA82AB27C4C006C0B96 /* Onboarding */ = { isa = PBXGroup; children = ( @@ -1876,7 +1885,6 @@ CDF8425F2A49EA2A00723DA0 /* Inbox */, 6386E02E2A03ED39006B3C1D /* Comment Tracker.swift */, 63344C4E2A07BD2A001BC616 /* Filters Tracker.swift */, - 63E5D38C2A13BCDE00EC1FBD /* Community Search Result Tracker.swift */, 63E5D3912A13CF2300EC1FBD /* Favorite Community Tracker.swift */, 63F0C7A72A0522FC00A18C5D /* Saved Account Tracker.swift */, 6DA61F862A5720EA001EA633 /* RecentSearchesTracker.swift */, @@ -2350,7 +2358,7 @@ children = ( CDEBC3272A9A57F200518D9D /* Content Model Identifier.swift */, CDEBC3292A9A580B00518D9D /* Post Model.swift */, - 03EEEAF82ABB985D0087F8D8 /* CommunityModel.swift */, + 03FD64FD2AE538C600957AA9 /* Community */, 030D00862AD1BB0D00953B1D /* User */, 03B7AAF02ABE404300068B23 /* ContentModel.swift */, CDEBC32B2A9A582500518D9D /* Votes Model.swift */, @@ -2979,7 +2987,6 @@ 03B643572A6864CD00F65700 /* TabBarSettingsView.swift in Sources */, CDF842642A49EAFA00723DA0 /* GetPersonMentions.swift in Sources */, 6D405B052A43E82300C65F9C /* Sidebar Header.swift in Sources */, - 63E5D38D2A13BCDE00EC1FBD /* Community Search Result Tracker.swift in Sources */, 50811B302A92049B006BA3F2 /* APICommunityView+Mock.swift in Sources */, CDA217F32A63202600BDA173 /* NSFW Overlay.swift in Sources */, 6372184E2A3A2AAD008C4816 /* APIPersonView.swift in Sources */, @@ -3062,6 +3069,7 @@ 63E5D3922A13CF2300EC1FBD /* Favorite Community Tracker.swift in Sources */, B1B78D642A51D53900F72485 /* AppDelegate.swift in Sources */, 03A1B3F42A83F46200AB0DE0 /* ShareButtonView.swift in Sources */, + 03FD64FF2AE53D0E00957AA9 /* CommunityModel+ContentModel.swift in Sources */, 6DA61F812A55B83F001EA633 /* SearchView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mlem/Extensions/View - Handle Lemmy Links.swift b/Mlem/Extensions/View - Handle Lemmy Links.swift index f3b7a2377..c18c98be0 100644 --- a/Mlem/Extensions/View - Handle Lemmy Links.swift +++ b/Mlem/Extensions/View - Handle Lemmy Links.swift @@ -25,28 +25,19 @@ struct HandleLemmyLinksDisplay: ViewModifier { content .navigationDestination(for: AppRoute.self) { route in switch route { - case .apiCommunity(let community): + case .community(let community): FeedView(community: community, feedType: .all, sortType: defaultPostSorting) .environmentObject(appState) .environmentObject(filtersTracker) - .environmentObject(CommunitySearchResultsTracker()) - case .apiCommunityView(let context): - FeedView(community: context.community, feedType: .all, sortType: defaultPostSorting) - .environmentObject(appState) - .environmentObject(filtersTracker) - .environmentObject(CommunitySearchResultsTracker()) case .communityLinkWithContext(let context): FeedView(community: context.community, feedType: context.feedType, sortType: defaultPostSorting) .environmentObject(appState) .environmentObject(filtersTracker) - .environmentObject(CommunitySearchResultsTracker()) case .communitySidebarLinkWithContext(let context): CommunitySidebarView( - community: context.community, - communityDetails: context.communityDetails + community: context.community ) .environmentObject(filtersTracker) - .environmentObject(CommunitySearchResultsTracker()) case .apiPostView(let post): let postModel = PostModel(from: post) let postTracker = PostTracker( diff --git a/Mlem/Models/Composers/PostEditor.swift b/Mlem/Models/Composers/PostEditor.swift index ee745fe74..8509888d6 100644 --- a/Mlem/Models/Composers/PostEditor.swift +++ b/Mlem/Models/Composers/PostEditor.swift @@ -9,23 +9,36 @@ import Foundation import SwiftUI struct PostEditorModel: Identifiable { - var id: Int { community.id } + var id: Int { community.communityId } - let community: APICommunity - let postTracker: PostTracker + let community: CommunityModel + var postTracker: PostTracker! let editPost: PostModel? var responseCallback: ((PostModel) -> Void)? init( - community: APICommunity, + community: CommunityModel, postTracker: PostTracker? = nil, - editPost: PostModel? = nil, responseCallback: ((PostModel) -> Void)? = nil ) { self.community = community - self.editPost = editPost + self.editPost = nil self.responseCallback = responseCallback - + self.initialiseTracker(postTracker) + } + + init( + post: PostModel, + postTracker: PostTracker? = nil, + responseCallback: ((PostModel) -> Void)? = nil + ) { + self.editPost = post + self.community = post.community + self.responseCallback = responseCallback + self.initialiseTracker(postTracker) + } + + private mutating func initialiseTracker(_ postTracker: PostTracker?) { @AppStorage("upvoteOnSave") var upvoteOnSave = false if let postTracker { self.postTracker = postTracker diff --git a/Mlem/Models/Content/Community/CommunityModel+ContentModel.swift b/Mlem/Models/Content/Community/CommunityModel+ContentModel.swift new file mode 100644 index 000000000..0dc26d174 --- /dev/null +++ b/Mlem/Models/Content/Community/CommunityModel+ContentModel.swift @@ -0,0 +1,19 @@ +// +// CommunityModel+ContentModel.swift +// Mlem +// +// Created by Sjmarf on 22/10/2023. +// + +import Foundation + +extension CommunityModel: ContentModel { + var uid: ContentModelIdentifier { .init(contentType: .community, contentId: communityId) } + var imageUrls: [URL] { + if let url = avatar { + return [url.withIcon64Parameters] + } + return [] + } + var searchResultScore: Int { self.subscriberCount ?? 0 } +} diff --git a/Mlem/Models/Content/Community/CommunityModel.swift b/Mlem/Models/Content/Community/CommunityModel.swift new file mode 100644 index 000000000..a619233c3 --- /dev/null +++ b/Mlem/Models/Content/Community/CommunityModel.swift @@ -0,0 +1,182 @@ +// +// CommunityModel.swift +// Mlem +// +// Created by Sjmarf on 20/09/2023. +// + +import Dependencies +import Foundation + +struct CommunityModel { + @Dependency(\.apiClient) private var apiClient + @Dependency(\.errorHandler) var errorHandler + @Dependency(\.hapticManager) var hapticManager + @Dependency(\.communityRepository) var communityRepository + + enum CommunityError: Error { + case noData + } + + @available(*, deprecated, message: "Use attributes of the CommunityModel directly instead.") + var community: APICommunity + + // Ids + let communityId: Int + let instanceId: Int + + // Text + let name: String + let displayName: String + let description: String? + + // Images + let avatar: URL? + let banner: URL? + + // State + var nsfw: Bool + var local: Bool + var removed: Bool + var deleted: Bool + var hidden: Bool + var postingRestrictedToMods: Bool + + // From APICommunityView + var blocked: Bool? + var subscribed: Bool? + var subscriberCount: Int? + + // Dates + let creationDate: Date + let updatedDate: Date? + + // URLs + let communityUrl: URL + + // These values are only available via GetCommunityResponse + var site: APISite? + var moderators: [APICommunityModeratorView]? + var discussionLanguages: [Int]? + var defaultPostLanguage: Int? + + init(from response: GetCommunityResponse) { + self.init(from: response.communityView) + self.site = response.site + self.moderators = response.moderators + self.discussionLanguages = response.discussionLanguages + self.defaultPostLanguage = response.defaultPostLanguage + } + + init(from response: CommunityResponse) { + self.init(from: response.communityView) + self.discussionLanguages = response.discussionLanguages + } + + init(from communityView: APICommunityView) { + self.init(from: communityView.community) + self.subscriberCount = communityView.counts.subscribers + self.subscribed = communityView.subscribed != .notSubscribed ? true : false + self.blocked = communityView.blocked + } + + init(from community: APICommunity) { + self.community = community + + self.communityId = community.id + self.instanceId = community.instanceId + + self.name = community.name + self.displayName = community.title + self.description = community.description + + self.avatar = community.iconUrl + self.banner = community.bannerUrl + + self.nsfw = community.nsfw + self.local = community.local + self.removed = community.removed + self.deleted = community.deleted + self.hidden = community.hidden + self.postingRestrictedToMods = community.postingRestrictedToMods + + self.creationDate = community.published + self.updatedDate = community.updated + + self.communityUrl = community.actorId + } + + mutating func toggleSubscribe(_ callback: @escaping (_ item: Self) -> Void = { _ in }) async throws { + guard let subscribed, let subscriberCount else { + throw CommunityError.noData + } + self.subscribed = !subscribed + if subscribed { + self.subscriberCount = subscriberCount + 1 + } else { + self.subscriberCount = subscriberCount - 1 + } + RunLoop.main.perform { [self] in + callback(self) + } + do { + let response = try await apiClient.followCommunity(id: communityId, shouldFollow: !subscribed) + RunLoop.main.perform { + callback(CommunityModel(from: response)) + } + } catch { + hapticManager.play(haptic: .failure, priority: .high) + let phrase = (self.subscribed ?? false) ? "unsubscribe from" : "subscribe to" + errorHandler.handle( + .init(title: "Failed to \(phrase) community", style: .toast, underlyingError: error) + ) + } + } + + mutating func toggleBlock(_ callback: @escaping (_ item: Self) -> Void = { _ in }) async throws { + guard let blocked else { + throw CommunityError.noData + } + self.blocked = !blocked + RunLoop.main.perform { [self] in + callback(self) + } + do { + let response: BlockCommunityResponse + if !blocked { + response = try await communityRepository.blockCommunity(id: communityId) + } else { + response = try await communityRepository.unblockCommunity(id: communityId) + } + RunLoop.main.perform { + callback(CommunityModel(from: response.communityView)) + } + } catch { + hapticManager.play(haptic: .failure, priority: .high) + errorHandler.handle(error) + let phrase = !blocked ? "block" : "unblock" + errorHandler.handle( + .init(title: "Failed to \(phrase) community", style: .toast, underlyingError: error) + ) + } + } +} + +extension CommunityModel: Identifiable { + var id: Int { hashValue } +} + +extension CommunityModel: Hashable { + static func == (lhs: CommunityModel, rhs: CommunityModel) -> Bool { + return lhs.hashValue == rhs.hashValue + } + + /// Hashes all fields for which state changes should trigger view updates. + func hash(into hasher: inout Hasher) { + hasher.combine(uid) + hasher.combine(subscribed) + hasher.combine(subscriberCount) + hasher.combine(blocked) + hasher.combine(moderators?.map { $0.moderator.id } ?? []) + } +} diff --git a/Mlem/Models/Content/CommunityModel.swift b/Mlem/Models/Content/CommunityModel.swift deleted file mode 100644 index 4457fbedd..000000000 --- a/Mlem/Models/Content/CommunityModel.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// CommunityModel.swift -// Mlem -// -// Created by Sjmarf on 20/09/2023. -// - -import Dependencies -import Foundation - -struct CommunityModel { - @Dependency(\.apiClient) private var apiClient - @Dependency(\.errorHandler) var errorHandler - @Dependency(\.hapticManager) var hapticManager - - let communityId: Int - var community: APICommunity - var subscribed: Bool - var subscriberCount: Int - - /// Creates a CommunityModel from an APICommunityView - /// - Parameter apiCommunityView: APICommunityView to create a CommunityModel representation of - init(from apiCommunityView: APICommunityView) { - self.communityId = apiCommunityView.community.id - self.community = apiCommunityView.community - self.subscriberCount = apiCommunityView.counts.subscribers - self.subscribed = apiCommunityView.subscribed != .notSubscribed ? true : false - } - - mutating func toggleSubscribe(_ callback: @escaping (_ item: Self) -> Void = { _ in }) async { - subscribed.toggle() - if subscribed { - subscriberCount += 1 - } else { - subscriberCount -= 1 - } - RunLoop.main.perform { [self] in - callback(self) - } - do { - try await apiClient.followCommunity(id: communityId, shouldFollow: subscribed) - } catch { - hapticManager.play(haptic: .failure, priority: .high) - errorHandler.handle(error) - } - } -} - -extension CommunityModel: Identifiable { - var id: Int { hashValue } -} - -extension CommunityModel: Hashable { - static func == (lhs: CommunityModel, rhs: CommunityModel) -> Bool { - return lhs.hashValue == rhs.hashValue - } - - /// Hashes all fields for which state changes should trigger view updates. - func hash(into hasher: inout Hasher) { - hasher.combine(uid) - hasher.combine(subscribed) - } -} - -extension CommunityModel: ContentModel { - var uid: ContentModelIdentifier { .init(contentType: .community, contentId: communityId) } - var imageUrls: [URL] { - if let url = community.iconUrl { - return [url.withIcon64Parameters] - } - return [] - } - var searchResultScore: Int { self.subscriberCount } -} diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index cdd2e42a7..f5883d86c 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -13,7 +13,7 @@ struct PostModel { let postId: Int let post: APIPost let creator: UserModel - let community: APICommunity + let community: CommunityModel var votes: VotesModel let numReplies: Int let saved: Bool @@ -29,7 +29,7 @@ struct PostModel { self.postId = apiPostView.post.id self.post = apiPostView.post self.creator = UserModel(from: apiPostView.creator) - self.community = apiPostView.community + self.community = CommunityModel(from: apiPostView.community) self.votes = VotesModel(from: apiPostView.counts, myVote: apiPostView.myVote) self.numReplies = apiPostView.counts.comments self.saved = apiPostView.saved @@ -55,7 +55,7 @@ struct PostModel { postId: Int? = nil, post: APIPost? = nil, creator: UserModel? = nil, - community: APICommunity? = nil, + community: CommunityModel? = nil, votes: VotesModel? = nil, numReplies: Int? = nil, saved: Bool? = nil, diff --git a/Mlem/Models/Content/User/UserModel.swift b/Mlem/Models/Content/User/UserModel.swift index 8c9fae621..8093f5094 100644 --- a/Mlem/Models/Content/User/UserModel.swift +++ b/Mlem/Models/Content/User/UserModel.swift @@ -100,7 +100,7 @@ struct UserModel { func getFlairs( postContext: APIPost? = nil, commentContext: APIComment? = nil, - communityContext: GetCommunityResponse? = nil + communityContext: CommunityModel? = nil ) -> [UserFlair] { var ret: [UserFlair] = .init() if let post = postContext, post.creatorId == self.userId { @@ -114,7 +114,9 @@ struct UserModel { } if let comment = commentContext, comment.distinguished { ret.append(.moderator) - } else if let community = communityContext, community.moderators.contains(where: { $0.moderator.id == userId }) { + } else if let community = communityContext, + let moderators = community.moderators, + moderators.contains(where: { $0.moderator.id == userId }) { ret.append(.moderator) } if isBot { diff --git a/Mlem/Models/Navigation Contexts/Community Link.swift b/Mlem/Models/Navigation Contexts/Community Link.swift index ed32d9376..d3d52f7d6 100644 --- a/Mlem/Models/Navigation Contexts/Community Link.swift +++ b/Mlem/Models/Navigation Contexts/Community Link.swift @@ -20,6 +20,6 @@ struct CommunityLinkWithContext: Equatable, Identifiable, Hashable { var id: Int { hashValue } - let community: APICommunity? + let community: CommunityModel? let feedType: FeedType } diff --git a/Mlem/Models/Navigation Contexts/Community Sidebar Link.swift b/Mlem/Models/Navigation Contexts/Community Sidebar Link.swift index 97019a1b2..6b4b056e9 100644 --- a/Mlem/Models/Navigation Contexts/Community Sidebar Link.swift +++ b/Mlem/Models/Navigation Contexts/Community Sidebar Link.swift @@ -17,8 +17,7 @@ struct CommunitySidebarLinkWithContext: Equatable, Identifiable, Hashable { hasher.combine(id) } - var id: String { communityDetails?.communityView.community.id.description ?? UUID().uuidString } + var id: String { community.communityId.description } - let community: APICommunity - let communityDetails: GetCommunityResponse? + let community: CommunityModel } diff --git a/Mlem/Models/Trackers/Community Search Result Tracker.swift b/Mlem/Models/Trackers/Community Search Result Tracker.swift deleted file mode 100644 index 27ee28f1c..000000000 --- a/Mlem/Models/Trackers/Community Search Result Tracker.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Community Search Result Tracker.swift -// Mlem -// -// Created by David Bureš on 16.05.2023. -// - -import Foundation - -class CommunitySearchResultsTracker: ObservableObject { - @Published var foundCommunities: [APICommunityView] = .init() - @Published var isLoading: Bool = false -} diff --git a/Mlem/Models/Trackers/Post Tracker.swift b/Mlem/Models/Trackers/Post Tracker.swift index 73f158562..d84dfeb64 100644 --- a/Mlem/Models/Trackers/Post Tracker.swift +++ b/Mlem/Models/Trackers/Post Tracker.swift @@ -227,7 +227,7 @@ class PostTracker: ObservableObject { @MainActor func removeCommunityPosts(from communityId: Int) { filter { - $0.community.id != communityId + $0.community.communityId != communityId } } @@ -432,7 +432,7 @@ class PostTracker: ObservableObject { for post in newPosts { // preload user and community avatars--fetching both because we don't know which we'll need, but these are super tiny // so it's probably not an API crime, right? - if let communityAvatarLink = post.community.iconUrl { + if let communityAvatarLink = post.community.avatar { imageRequests.append(ImageRequest(url: communityAvatarLink.withIcon64Parameters)) } diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index 16668c27a..ca983937f 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -11,16 +11,15 @@ import Foundation /// /// For simple (i.e. linear) navigation flows, you may wish to define a separate set of routes. For example, see `OnboardingRoutes`. enum AppRoute: Routable { - case apiCommunityView(APICommunityView) - case apiCommunity(APICommunity) - case communityLinkWithContext(CommunityLinkWithContext) case communitySidebarLinkWithContext(CommunitySidebarLinkWithContext) case apiPostView(APIPostView) case apiPost(APIPost) - @available(*, deprecated, message: "Use userProfile instead.") + case community(CommunityModel) + + @available(*, deprecated, message: "Use .userProfile instead.") case apiPerson(APIPerson) case userProfile(UserModel) @@ -39,10 +38,6 @@ enum AppRoute: Routable { // swiftlint:disable cyclomatic_complexity static func makeRoute(_ value: V) throws -> AppRoute where V: Hashable { switch value { - case let value as APICommunityView: - return .apiCommunityView(value) - case let value as APICommunity: - return .apiCommunity(value) case let value as CommunityLinkWithContext: return .communityLinkWithContext(value) case let value as CommunitySidebarLinkWithContext: @@ -51,6 +46,8 @@ enum AppRoute: Routable { return .apiPostView(value) case let value as APIPost: return .apiPost(value) + case let value as CommunityModel: + return .community(value) case let value as APIPerson: return .apiPerson(value) case let value as UserModel: diff --git a/Mlem/Views/Shared/Composer/PostComposerView.swift b/Mlem/Views/Shared/Composer/PostComposerView.swift index 363f70a04..ce6ac9056 100644 --- a/Mlem/Views/Shared/Composer/PostComposerView.swift +++ b/Mlem/Views/Shared/Composer/PostComposerView.swift @@ -49,7 +49,7 @@ struct PostComposerView: View { } else { let response = try await apiClient.createPost( - communityId: editModel.community.id, + communityId: editModel.community.communityId, name: postTitle.trimmed, nsfw: isNSFW, body: postBody.trimmed, @@ -75,7 +75,7 @@ struct PostComposerView_Previews: PreviewProvider { NavigationStack { PostComposerView( editModel: PostEditorModel( - community: .mock(id: 1, name: "mlem") + community: CommunityModel(from: .mock(id: 1, name: "mlem")) ) ) } diff --git a/Mlem/Views/Shared/Composer/PostDetailEditorView.swift b/Mlem/Views/Shared/Composer/PostDetailEditorView.swift index 54461e712..d724b9b7f 100644 --- a/Mlem/Views/Shared/Composer/PostDetailEditorView.swift +++ b/Mlem/Views/Shared/Composer/PostDetailEditorView.swift @@ -33,7 +33,7 @@ struct PostDetailEditorView: View { @Environment(\.dismiss) var dismiss - var community: APICommunity + var community: CommunityModel var onSubmit: () async throws -> Void @Binding var postTitle: String @@ -54,7 +54,7 @@ struct PostDetailEditorView: View { @FocusState private var focusedField: Field? init( - community: APICommunity, + community: CommunityModel, postTitle: Binding, postURL: Binding, postBody: Binding, diff --git a/Mlem/Views/Shared/Links/AvatarView.swift b/Mlem/Views/Shared/Links/AvatarView.swift index 11b79280c..4cfd4ce8c 100644 --- a/Mlem/Views/Shared/Links/AvatarView.swift +++ b/Mlem/Views/Shared/Links/AvatarView.swift @@ -17,14 +17,14 @@ struct AvatarView: View { let clipAvatar: Bool let blurAvatar: Bool - init(community: APICommunity, avatarSize: CGFloat, lineColor: Color? = nil) { + init(community: CommunityModel, avatarSize: CGFloat, lineColor: Color? = nil) { @AppStorage("shouldBlurNsfw") var shouldBlurNsfw = true self.type = .community - self.url = community.iconUrl + self.url = community.avatar self.avatarSize = avatarSize self.lineColor = lineColor ?? Color(UIColor.secondarySystemBackground) - self.clipAvatar = AvatarView.shouldClipCommunityAvatar(url: community.iconUrl) + self.clipAvatar = AvatarView.shouldClipCommunityAvatar(url: community.avatar) self.blurAvatar = shouldBlurNsfw && community.nsfw } diff --git a/Mlem/Views/Shared/Links/Community/CommunityLabelView.swift b/Mlem/Views/Shared/Links/Community/CommunityLabelView.swift index 43ca79ae0..c072578ff 100644 --- a/Mlem/Views/Shared/Links/Community/CommunityLabelView.swift +++ b/Mlem/Views/Shared/Links/Community/CommunityLabelView.swift @@ -11,7 +11,7 @@ struct CommunityLabelView: View { @AppStorage("shouldShowCommunityIcons") var shouldShowCommunityIcons: Bool = true // parameters - let community: APICommunity + let community: CommunityModel let serverInstanceLocation: ServerInstanceLocation let overrideShowAvatar: Bool? // if present, shows or hides the avatar according to value; otherwise uses system setting @@ -27,11 +27,24 @@ struct CommunityLabelView: View { return shouldShowCommunityIcons } } - + + @available(*, deprecated, message: "Provide a CommunityModel rather than an APICommunity.") init( community: APICommunity, serverInstanceLocation: ServerInstanceLocation, overrideShowAvatar: Bool? = nil + ) { + self.init( + community: CommunityModel(from: community), + serverInstanceLocation: serverInstanceLocation, + overrideShowAvatar: overrideShowAvatar + ) + } + + init( + community: CommunityModel, + serverInstanceLocation: ServerInstanceLocation, + overrideShowAvatar: Bool? = nil ) { self.community = community self.serverInstanceLocation = serverInstanceLocation @@ -77,7 +90,7 @@ struct CommunityLabelView: View { @ViewBuilder private var communityInstance: some View { - if let host = community.actorId.host() { + if let host = community.communityUrl.host() { Text("@\(host)") .dynamicTypeSize(.small ... .accessibility2) .lineLimit(1) diff --git a/Mlem/Views/Shared/Links/Community/CommunityLinkView.swift b/Mlem/Views/Shared/Links/Community/CommunityLinkView.swift index 6efff86dc..21aa81d7a 100644 --- a/Mlem/Views/Shared/Links/Community/CommunityLinkView.swift +++ b/Mlem/Views/Shared/Links/Community/CommunityLinkView.swift @@ -8,16 +8,31 @@ import Foundation import SwiftUI struct CommunityLinkView: View { - let community: APICommunity + let community: CommunityModel let serverInstanceLocation: ServerInstanceLocation let extraText: String? let overrideShowAvatar: Bool? // if present, shows or hides avatar according to value; otherwise uses system setting + @available(*, deprecated, message: "Provide a CommunityModel rather than an APICommunity.") init( community: APICommunity, serverInstanceLocation: ServerInstanceLocation = .bottom, overrideShowAvatar: Bool? = nil, extraText: String? = nil + ) { + self.init( + community: CommunityModel(from: community), + serverInstanceLocation: serverInstanceLocation, + overrideShowAvatar: overrideShowAvatar, + extraText: extraText + ) + } + + init( + community: CommunityModel, + serverInstanceLocation: ServerInstanceLocation = .bottom, + overrideShowAvatar: Bool? = nil, + extraText: String? = nil ) { self.community = community self.serverInstanceLocation = serverInstanceLocation @@ -27,7 +42,7 @@ struct CommunityLinkView: View { var body: some View { // NavigationLink(value: community) { - NavigationLink(.apiCommunity(community)) { + NavigationLink(.community(community)) { HStack { CommunityLabelView( community: community, diff --git a/Mlem/Views/Shared/Links/User/UserLabelView.swift b/Mlem/Views/Shared/Links/User/UserLabelView.swift index 36f93aa76..26e8205dd 100644 --- a/Mlem/Views/Shared/Links/User/UserLabelView.swift +++ b/Mlem/Views/Shared/Links/User/UserLabelView.swift @@ -18,10 +18,10 @@ struct UserLabelView: View { // to pick the correct flair @State var postContext: APIPost? @State var commentContext: APIComment? - @State var communityContext: GetCommunityResponse? + @State var communityContext: CommunityModel? var blurAvatar: Bool { postContext?.nsfw ?? false || - communityContext?.communityView.community.nsfw ?? false + communityContext?.nsfw ?? false } @available(*, deprecated, message: "Provide a UserModel rather than an APIPerson.") @@ -31,7 +31,7 @@ struct UserLabelView: View { overrideShowAvatar: Bool? = nil, postContext: APIPost? = nil, commentContext: APIComment? = nil, - communityContext: GetCommunityResponse? = nil + communityContext: CommunityModel? = nil ) { self.init( user: UserModel(from: person), @@ -49,7 +49,7 @@ struct UserLabelView: View { overrideShowAvatar: Bool? = nil, postContext: APIPost? = nil, commentContext: APIComment? = nil, - communityContext: GetCommunityResponse? = nil + communityContext: CommunityModel? = nil ) { self.user = user self.serverInstanceLocation = serverInstanceLocation diff --git a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift index d731aa01a..1204dc523 100644 --- a/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift +++ b/Mlem/Views/Shared/Posts/ExpandedPostLogic.swift @@ -158,9 +158,8 @@ extension ExpandedPost { enabled: true ) { editorTracker.openEditor(with: PostEditorModel( - community: post.community, + post: post, postTracker: postTracker, - editPost: post, responseCallback: updatePost )) }) diff --git a/Mlem/Views/Shared/Posts/Feed Post.swift b/Mlem/Views/Shared/Posts/Feed Post.swift index 9e365b99b..462023363 100644 --- a/Mlem/Views/Shared/Posts/Feed Post.swift +++ b/Mlem/Views/Shared/Posts/Feed Post.swift @@ -249,9 +249,9 @@ struct FeedPost: View { func blockCommunity() async { // TODO: migrate to communityRepository do { - let response = try await apiClient.blockCommunity(id: post.community.id, shouldBlock: true) + let response = try await apiClient.blockCommunity(id: post.community.communityId, shouldBlock: true) if response.blocked { - postTracker.removeCommunityPosts(from: post.community.id) + postTracker.removeCommunityPosts(from: post.community.communityId) await notifier.add(.success("Blocked \(post.community.name)")) } } catch { @@ -274,9 +274,8 @@ struct FeedPost: View { func editPost() { editorTracker.openEditor(with: PostEditorModel( - community: post.community, - postTracker: postTracker, - editPost: post + post: post, + postTracker: postTracker )) } @@ -351,7 +350,7 @@ struct FeedPost: View { replyToPost() }) - if appState.isCurrentAccountId(post.creator.id) { + if appState.isCurrentAccountId(post.creator.userId) { // edit ret.append(MenuFunction.standardMenuFunction( text: "Edit", @@ -366,7 +365,7 @@ struct FeedPost: View { ret.append(MenuFunction.standardMenuFunction( text: "Delete", imageName: Icons.delete, - destructiveActionPrompt: "Are you sure you want to delete this post? This cannot be undone.", + destructiveActionPrompt: "Are you sure you want to delete this post? This cannot be undone.", enabled: !post.post.deleted ) { Task(priority: .userInitiated) { diff --git a/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift b/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift index 01db2de3b..95383b996 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift +++ b/Mlem/Views/Tabs/Feeds/Community List/Components/CommunityListRowViews.swift @@ -68,10 +68,10 @@ struct CommuntiyFeedRowView: View { private var pathValue: AnyHashable { if navigationContext == .sidebar { - return CommunityLinkWithContext(community: community, feedType: .subscribed) + return CommunityLinkWithContext(community: CommunityModel(from: community), feedType: .subscribed) } else { // Do not use enum route path in sidebar: It doesn't work, and I have no idea why =/ [2023.09] - return AppRoute.communityLinkWithContext(.init(community: community, feedType: .subscribed)) + return AppRoute.communityLinkWithContext(.init(community: CommunityModel(from: community), feedType: .subscribed)) } } diff --git a/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift b/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift index 3fa711272..d3d13ba43 100644 --- a/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift +++ b/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift @@ -13,29 +13,19 @@ struct CommunitySidebarView: View { @Dependency(\.errorHandler) var errorHandler // parameters - let community: APICommunity - @State var communityDetails: GetCommunityResponse? + @State var community: CommunityModel @State private var selectionSection = 0 var shouldShowCommunityHeaders: Bool = true @State private var errorMessage: String? var body: some View { - Section { - if let loadedDetails = communityDetails { - view(for: loadedDetails) - } else if let shownError = errorMessage { - errorView(errorDetails: shownError) - } else { - LoadingView(whatIsLoading: .communityDetails) - } - } + Section { view } .navigationTitle("Sidebar") .navigationBarTitleDisplayMode(.inline) .task(priority: .userInitiated) { - // Load community details if they weren't provided - // when we loaded - if communityDetails == nil { + // Load community details if they weren't provided already + if community.moderators == nil { await loadCommunity() } }.refreshable { @@ -46,9 +36,10 @@ struct CommunitySidebarView: View { private func loadCommunity() async { do { errorMessage = nil - communityDetails = try await communityRepository.loadDetails(for: community.id) + let communityDetails: GetCommunityResponse = try await communityRepository.loadDetails(for: community.communityId) + community = .init(from: communityDetails) } catch { - errorMessage = "We were unable to load this communities details, please try again." + errorMessage = "Unable to load community details, please try again." errorHandler.handle(error) } } @@ -60,16 +51,15 @@ struct CommunitySidebarView: View { return formatter.localizedString(for: date, relativeTo: Date.now) } - @ViewBuilder - private func view(for communityDetails: GetCommunityResponse) -> some View { + var view: some View { ScrollView { CommunitySidebarHeader( - title: communityDetails.communityView.community.name, - subtitle: "@\(communityDetails.communityView.community.name)@\(communityDetails.communityView.community.actorId.host()!)", - avatarSubtext: .constant("Created \(getRelativeTime(date: communityDetails.communityView.community.published))"), - bannerURL: shouldShowCommunityHeaders ? communityDetails.communityView.community.bannerUrl : nil, - avatarUrl: communityDetails.communityView.community.iconUrl, - label1: "\(communityDetails.communityView.counts.subscribers) Subscribers", + title: community.displayName, + subtitle: "@\(community.name)@\(community.communityUrl.host()!)", + avatarSubtext: .constant("Created \(getRelativeTime(date: community.creationDate))"), + bannerURL: shouldShowCommunityHeaders ? community.banner : nil, + avatarUrl: community.avatar, + label1: "\(community.subscriberCount ?? 0) Subscribers", avatarType: .community ) @@ -81,29 +71,28 @@ struct CommunitySidebarView: View { .padding(.horizontal) if selectionSection == 0 { - if let description = communityDetails - .communityView - .community - .description { + if let description = community.description { MarkdownView(text: description, isNsfw: false).padding() } } else if selectionSection == 1 { VStack { Divider() - ForEach(communityDetails.moderators) { moderatorView in - - NavigationLink(.apiPerson(moderatorView.moderator)) { - HStack { - UserLabelView( - person: moderatorView.moderator, - serverInstanceLocation: .bottom, - overrideShowAvatar: true, - communityContext: communityDetails - ) - Spacer() - }.padding() + if let moderators = community.moderators { + ForEach(moderators) { moderatorView in + + NavigationLink(.apiPerson(moderatorView.moderator)) { + HStack { + UserLabelView( + person: moderatorView.moderator, + serverInstanceLocation: .bottom, + overrideShowAvatar: true, + communityContext: community + ) + Spacer() + }.padding() + } + Divider() } - Divider() } }.padding(.top) } @@ -153,15 +142,14 @@ struct SidebarPreview: PreviewProvider { static let previewModerator = APICommunityModeratorView(community: previewCommunity, moderator: previewUser) static var previews: some View { - CommunitySidebarView( - community: previewCommunity, - communityDetails: .mock( - communityView: .mock( - community: previewCommunity, - subscribed: .subscribed - ), - moderators: .init(repeating: previewModerator, count: 11) - ) - ) + let model = CommunityModel(from: GetCommunityResponse.mock( + communityView: .mock( + community: previewCommunity, + subscribed: .subscribed + ), + moderators: .init(repeating: previewModerator, count: 11) + )) + + CommunitySidebarView(community: model) } } diff --git a/Mlem/Views/Tabs/Feeds/Feed View Logic.swift b/Mlem/Views/Tabs/Feeds/Feed View Logic.swift index 72316c07b..c3e03592f 100644 --- a/Mlem/Views/Tabs/Feeds/Feed View Logic.swift +++ b/Mlem/Views/Tabs/Feeds/Feed View Logic.swift @@ -26,7 +26,7 @@ extension FeedView { isLoading = true do { try await postTracker.loadNextPage( - communityId: community?.id, + communityId: community?.communityId, sort: postSortType, type: feedType, filtering: filter @@ -41,7 +41,7 @@ extension FeedView { // NOTE: refresh doesn't need to touch isLoading because that visual cue is handled by .refreshable do { try await postTracker.refresh( - communityId: community?.id, + communityId: community?.communityId, sort: postSortType, feedType: feedType, filtering: filter @@ -60,7 +60,7 @@ extension FeedView { isLoading = true do { try await postTracker.refresh( - communityId: community?.id, + communityId: community?.communityId, sort: postSortType, feedType: feedType, clearBeforeFetch: true, @@ -72,11 +72,11 @@ extension FeedView { } // MARK: Community loading - func fetchCommunityDetails() async { if let community { do { - communityDetails = try await communityRepository.loadDetails(for: community.id) + let communityDetails: GetCommunityResponse = try await communityRepository.loadDetails(for: community.communityId) + self.community = .init(from: communityDetails) } catch { errorHandler.handle( .init( @@ -148,7 +148,8 @@ extension FeedView { } // swiftlint:disable function_body_length - func genCommunitySpecificMenuFunctions(for community: APICommunity) -> [MenuFunction] { + func genCommunitySpecificMenuFunctions() -> [MenuFunction] { + guard let community else { return [] } var ret: [MenuFunction] = .init() // new post ret.append(MenuFunction.standardMenuFunction( @@ -164,9 +165,8 @@ extension FeedView { }) // subscribe/unsubscribe - if let communityDetails { - let isSubscribed: Bool = communityDetails.communityView.subscribed.rawValue == "Subscribed" - let (subscribeText, subscribeSymbol, subscribePrompt) = isSubscribed + if let subscribed = community.subscribed { + let (subscribeText, subscribeSymbol, subscribePrompt) = subscribed ? ("Unsubscribe", Icons.unsubscribe, "Really unsubscribe from \(community.name)?") : ("Subscribe", Icons.subscribe, nil) ret.append(MenuFunction.standardMenuFunction( @@ -176,20 +176,20 @@ extension FeedView { enabled: true ) { Task(priority: .userInitiated) { - await subscribe(communityId: community.id, shouldSubscribe: !isSubscribed) + await toggleSubscribe() } }) } // favorite/unfavorite - if favoriteCommunitiesTracker.isFavorited(community) { + if favoriteCommunitiesTracker.isFavorited(community.community) { ret.append(MenuFunction.standardMenuFunction( text: "Unfavorite", imageName: "star.slash", destructiveActionPrompt: "Really unfavorite \(community.name)?", enabled: true ) { - favoriteCommunitiesTracker.unfavorite(community) + favoriteCommunitiesTracker.unfavorite(community.community) Task { await notifier.add(.success("Unfavorited \(community.name)")) } @@ -201,7 +201,7 @@ extension FeedView { destructiveActionPrompt: nil, enabled: true ) { - favoriteCommunitiesTracker.favorite(community) + favoriteCommunitiesTracker.favorite(community.community) Task { await notifier.add(.success("Favorited \(community.name)")) } @@ -209,12 +209,12 @@ extension FeedView { } // share - ret.append(MenuFunction.shareMenuFunction(url: community.actorId)) + ret.append(MenuFunction.shareMenuFunction(url: community.communityUrl)) // block/unblock - if let communityDetails { + if let blocked = community.blocked { // block - let (blockText, blockSymbol, blockPrompt) = communityDetails.communityView.blocked + let (blockText, blockSymbol, blockPrompt) = blocked ? ("Unblock", Icons.show, nil) : ("Block", Icons.hide, "Really block \(community.name)?") ret.append(MenuFunction.standardMenuFunction( @@ -224,7 +224,7 @@ extension FeedView { enabled: true ) { Task(priority: .userInitiated) { - await block(communityId: community.id, shouldBlock: !communityDetails.communityView.blocked) + await block() } }) } @@ -292,53 +292,36 @@ extension FeedView { return nil } - private func subscribe(communityId: Int, shouldSubscribe: Bool) async { - hapticManager.play(haptic: .success, priority: .high) - do { - let response = try await communityRepository.updateSubscription(for: communityId, subscribed: shouldSubscribe) - // TODO: we receive the updated `APICommunityView` here to update our local state - // so there is no need to make a second call but we still need to address 'faking' - // the state while the call is in flight - communityDetails?.communityView = response - - let communityName = community?.name ?? "community" - if shouldSubscribe { - await notifier.add(.success("Subscibed to \(communityName)")) - } else { - await notifier.add(.success("Unsubscribed from \(communityName)")) + private func toggleSubscribe() async { + if var community = self.community { + hapticManager.play(haptic: .success, priority: .high) + do { + try await community.toggleSubscribe { + self.community = $0 + } + if community.subscribed ?? false { + await notifier.add(.success("Subscribed to \(community.name)")) + } else { + await notifier.add(.success("Unsubscribed from \(community.name)")) + } + } catch { + errorHandler.handle(error) } - } catch { - hapticManager.play(haptic: .failure, priority: .high) - let phrase = shouldSubscribe ? "subscribe to" : "unsubscribe from" - errorHandler.handle( - .init(title: "Failed to \(phrase) community", style: .toast, underlyingError: error) - ) } } - private func block(communityId: Int, shouldBlock: Bool) async { - do { + private func block() async { + if var community = self.community { hapticManager.play(haptic: .violentSuccess, priority: .high) - - let response: BlockCommunityResponse - let communityName = community?.name ?? "community" - if shouldBlock { - response = try await communityRepository.blockCommunity(id: communityId) - await notifier.add(.success("Blocked \(communityName)")) - } else { - response = try await communityRepository.unblockCommunity(id: communityId) - await notifier.add(.success("Unblocked \(communityName)")) + do { + try await community.toggleBlock { + self.community = $0 + } + // refresh the feed after blocking which will show/hide the posts + await hardRefreshFeed() + } catch { + errorHandler.handle(error) } - - communityDetails?.communityView = response.communityView - // refresh the feed after blocking which will show/hide the posts - await hardRefreshFeed() - } catch { - hapticManager.play(haptic: .failure, priority: .high) - let phrase = shouldBlock ? "block" : "unblock" - errorHandler.handle( - .init(title: "Failed to \(phrase) community", style: .toast, underlyingError: error) - ) } } } diff --git a/Mlem/Views/Tabs/Feeds/Feed View.swift b/Mlem/Views/Tabs/Feeds/Feed View.swift index 267db6865..c9e579e6b 100644 --- a/Mlem/Views/Tabs/Feeds/Feed View.swift +++ b/Mlem/Views/Tabs/Feeds/Feed View.swift @@ -30,14 +30,14 @@ struct FeedView: View { // MARK: Parameters and init - let community: APICommunity? + @State var community: CommunityModel? let showLoading: Bool @State var feedType: FeedType @State var errorDetails: ErrorDetails? init( - community: APICommunity?, + community: CommunityModel?, feedType: FeedType, sortType: PostSortType, showLoading: Bool = false @@ -46,9 +46,6 @@ struct FeedView: View { @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false - self.community = community - self.showLoading = showLoading - self._feedType = State(initialValue: feedType) self._postSortType = .init(initialValue: sortType) self._postTracker = StateObject(wrappedValue: .init( @@ -56,13 +53,15 @@ struct FeedView: View { internetSpeed: internetSpeed, upvoteOnSave: upvoteOnSave )) + + self.showLoading = showLoading + self._community = State(initialValue: community) } // MARK: State @StateObject var postTracker: PostTracker - @State var communityDetails: GetCommunityResponse? @State var postSortType: PostSortType @State var isLoading: Bool = true @State var shouldLoad: Bool = false @@ -195,18 +194,17 @@ struct FeedView: View { @ViewBuilder private var ellipsisMenu: some View { Menu { - if let community, let communityDetails { + if let community { // until we find a nice way to put nav stuff in MenuFunction, this'll have to do :( NavigationLink(.communitySidebarLinkWithContext( .init( - community: community, - communityDetails: communityDetails + community: community ) )) { Label("Sidebar", systemImage: "sidebar.right") } - ForEach(genCommunitySpecificMenuFunctions(for: community)) { menuFunction in + ForEach(genCommunitySpecificMenuFunctions()) { menuFunction in MenuButton(menuFunction: menuFunction, confirmDestructive: confirmDestructive) } } @@ -261,8 +259,7 @@ struct FeedView: View { private var toolbarHeader: some View { if let community { NavigationLink(.communitySidebarLinkWithContext(.init( - community: community, - communityDetails: communityDetails + community: community ))) { Text(community.name) .font(.headline) diff --git a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift index 751e3cdb4..8754cd3e3 100644 --- a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift +++ b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift @@ -19,12 +19,12 @@ struct CommunityResultView: View { var swipeActions: SwipeConfiguration? var subscribeSwipeAction: SwipeAction { - let (emptySymbolName, fullSymbolName) = community.subscribed + let (emptySymbolName, fullSymbolName) = (community.subscribed ?? false) ? (Icons.unsubscribePerson, Icons.unsubscribePersonFill) : (Icons.subscribePerson, Icons.subscribePersonFill) return SwipeAction( symbol: .init(emptyName: emptySymbolName, fillName: fullSymbolName), - color: community.subscribed ? .red : .green, + color: (community.subscribed ?? false) ? .red : .green, action: { Task { await subscribe() @@ -35,13 +35,13 @@ struct CommunityResultView: View { func subscribe() async { var community = community - await community.toggleSubscribe { + try? await community.toggleSubscribe { contentTracker.update(with: AnyContentModel($0)) } } var caption: String { - if let host = community.community.actorId.host { + if let host = community.communityUrl.host { if showTypeLabel { return "Community ∙ @\(host)" } else { @@ -52,15 +52,15 @@ struct CommunityResultView: View { } var body: some View { - NavigationLink(value: AppRoute.apiCommunity(community.community)) { + NavigationLink(value: AppRoute.community(community)) { HStack(spacing: 10) { - AvatarView(community: community.community, avatarSize: 48) + AvatarView(community: community, avatarSize: 48) VStack(alignment: .leading, spacing: 4) { - if community.community.nsfw { - Text("\(community.community.name) - NSFW") + if community.nsfw { + Text("\(community.name) - NSFW") .foregroundStyle(.red) } else { - Text(community.community.name) + Text(community.name) } Text(caption) .font(.footnote) @@ -69,11 +69,11 @@ struct CommunityResultView: View { } Spacer() HStack(spacing: 5) { - Text(abbreviateNumber(community.subscriberCount)) + Text(abbreviateNumber(community.subscriberCount ?? 0)) .monospacedDigit() - Image(systemName: community.subscribed ? Icons.subscribed : Icons.personFill) + Image(systemName: (community.subscribed ?? false) ? Icons.subscribed : Icons.personFill) } - .foregroundStyle(community.subscribed ? .green : .secondary) + .foregroundStyle((community.subscribed ?? false) ? .green : .secondary) Image(systemName: Icons.forward) .imageScale(.small) .foregroundStyle(.tertiary) @@ -84,22 +84,24 @@ struct CommunityResultView: View { .buttonStyle(.plain) .padding(.vertical, 8) .background(.background) - .draggable(community.community.actorId) { + .draggable(community.communityUrl) { HStack { - AvatarView(community: community.community, avatarSize: 24) - Text(community.community.name) + AvatarView(community: community, avatarSize: 24) + Text(community.name) } .padding(8) .background(.background) .clipShape(RoundedRectangle(cornerRadius: 8)) } .contextMenu { - Button(role: community.subscribed ? .destructive : nil) { - Task(priority: .userInitiated) { await subscribe() } - } label: { - Label( - community.subscribed ? "Unsubscribe" : "Subscribe", - systemImage: community.subscribed ? Icons.unsubscribe : Icons.subscribe) + if let subscribed = community.subscribed { + Button(role: subscribed ? .destructive : nil) { + Task(priority: .userInitiated) { await subscribe() } + } label: { + Label( + subscribed ? "Unsubscribe" : "Subscribe", + systemImage: subscribed ? Icons.unsubscribe : Icons.subscribe) + } } } .addSwipeyActions(swipeActions ?? .init(trailingActions: [subscribeSwipeAction])) diff --git a/Mlem/Window.swift b/Mlem/Window.swift index 6dc4988e1..c335a1cda 100644 --- a/Mlem/Window.swift +++ b/Mlem/Window.swift @@ -15,7 +15,6 @@ struct Window: View { @Dependency(\.hapticManager) var hapticManager @Dependency(\.siteInformation) var siteInformation - @StateObject var communitySearchResultsTracker: CommunitySearchResultsTracker = .init() @StateObject var easterFlagsTracker: EasterFlagsTracker = .init() @StateObject var filtersTracker: FiltersTracker = .init() @StateObject var recentSearchesTracker: RecentSearchesTracker = .init() @@ -67,7 +66,6 @@ struct Window: View { ContentView() .environmentObject(filtersTracker) .environmentObject(appState) - .environmentObject(communitySearchResultsTracker) .environmentObject(recentSearchesTracker) .environmentObject(easterFlagsTracker) .environmentObject(layoutWidgetTracker) diff --git a/MlemTests/Mocks/MockErrorHandler.swift b/MlemTests/Mocks/MockErrorHandler.swift index eb89edb8c..eb3eabc25 100644 --- a/MlemTests/Mocks/MockErrorHandler.swift +++ b/MlemTests/Mocks/MockErrorHandler.swift @@ -30,7 +30,8 @@ class MockErrorHandler: ErrorHandler { _ error: ContextualError?, file: StaticString = #fileID, function: StaticString = #function, - line: Int = #line + line: Int = #line, + showNoInternet: Bool = true ) { if let error { didReceiveError(error)