From 5cf1c2b689a037bdced18ac14f1c71a25bab007c Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 14 Jan 2024 18:36:12 +0000 Subject: [PATCH 01/40] Update --- Mlem.xcodeproj/project.pbxproj | 28 +++ Mlem/API/APIClient/APIClient.swift | 2 +- Mlem/Enums/AvatarType.swift | 6 +- .../View+HandleLemmyLinks.swift | 2 + .../CommunityModel+MenuFunctions.swift | 15 ++ .../InstanceModel+MenuFunctions.swift | 17 ++ .../Content/Instance/InstanceModel.swift | 63 ++++++ .../User/UserModel+MenuFunctions.swift | 9 + Mlem/Models/Content/User/UserModel.swift | 7 +- Mlem/Models/Menu Function.swift | 23 +++ Mlem/Navigation/Routes/AppRoutes.swift | 1 + Mlem/Repositories/PersonRepository.swift | 14 ++ .../Shared/Avatars/DefaultAvatarView.swift | 28 ++- .../Components/Components/Menu Button.swift | 4 + Mlem/Views/Shared/Error View.swift | 5 +- Mlem/Views/Shared/Instance/InstanceView.swift | 184 ++++++++++++++++++ Mlem/Views/Shared/Links/AvatarView.swift | 19 ++ .../Views/Tabs/Profile/AvatarBannerView.swift | 26 ++- Mlem/Views/Tabs/Profile/UserView+Logic.swift | 13 +- Mlem/Views/Tabs/Profile/UserView.swift | 2 +- 20 files changed, 447 insertions(+), 21 deletions(-) create mode 100644 Mlem/Models/Content/Instance/InstanceModel+MenuFunctions.swift create mode 100644 Mlem/Models/Content/Instance/InstanceModel.swift create mode 100644 Mlem/Views/Shared/Instance/InstanceView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 8b2aaff2a..b454abc71 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -68,6 +68,9 @@ 03A2767B2AFE560000C0D66B /* CommunityModel+SwipeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */; }; 03A2767D2AFE656700C0D66B /* UserModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */; }; 03A40DAD2AD5EA11005F019F /* NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */; }; + 03A54C322B5331F30064CCDE /* InstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A54C312B5331F30064CCDE /* InstanceView.swift */; }; + 03A54C352B533BC50064CCDE /* InstanceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A54C342B533BC50064CCDE /* InstanceModel.swift */; }; + 03A54C372B545A430064CCDE /* InstanceModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A54C362B545A430064CCDE /* InstanceModel+MenuFunctions.swift */; }; 03B643572A6864CD00F65700 /* TabBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B643562A6864CD00F65700 /* TabBarSettingsView.swift */; }; 03B7AAEF2ABCB9DC00068B23 /* ContentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */; }; 03B7AAF12ABE404300068B23 /* ContentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7AAF02ABE404300068B23 /* ContentModel.swift */; }; @@ -608,6 +611,9 @@ 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+SwipeActions.swift"; sourceTree = ""; }; 03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserModel+MenuFunctions.swift"; sourceTree = ""; }; 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoPostsView.swift; sourceTree = ""; }; + 03A54C312B5331F30064CCDE /* InstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceView.swift; sourceTree = ""; }; + 03A54C342B533BC50064CCDE /* InstanceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceModel.swift; sourceTree = ""; }; + 03A54C362B545A430064CCDE /* InstanceModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceModel+MenuFunctions.swift"; sourceTree = ""; }; 03B643562A6864CD00F65700 /* TabBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarSettingsView.swift; sourceTree = ""; }; 03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTracker.swift; sourceTree = ""; }; 03B7AAF02ABE404300068B23 /* ContentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentModel.swift; sourceTree = ""; }; @@ -1313,6 +1319,23 @@ path = Protocol; sourceTree = ""; }; + 03A54C302B5331D50064CCDE /* Instance */ = { + isa = PBXGroup; + children = ( + 03A54C312B5331F30064CCDE /* InstanceView.swift */, + ); + path = Instance; + sourceTree = ""; + }; + 03A54C332B533BBA0064CCDE /* Instance */ = { + isa = PBXGroup; + children = ( + 03A54C342B533BC50064CCDE /* InstanceModel.swift */, + 03A54C362B545A430064CCDE /* InstanceModel+MenuFunctions.swift */, + ); + path = Instance; + sourceTree = ""; + }; 03B643552A6863E800F65700 /* TabBar */ = { isa = PBXGroup; children = ( @@ -2022,6 +2045,7 @@ 6322A5CA27F77A4D00135D4F /* Loading View.swift */, 6386E03F2A045723006B3C1D /* Website Icon Complex.swift */, 63D24EDD2A169F2A005CCA81 /* Markdown View.swift */, + 03A54C302B5331D50064CCDE /* Instance */, 03BAA2392A57DC1400D48252 /* PublishedTimestampView.swift */, 6374570F2A18CB6600B69C03 /* Custom Text Field.swift */, B11D72822A49FAA7009DC22F /* Cached Image.swift */, @@ -2688,6 +2712,7 @@ CD4368C22AE2409D00BD8BD1 /* Inbox */, CDEBC3272A9A57F200518D9D /* Content Model Identifier.swift */, CDEBC3292A9A580B00518D9D /* Post Model.swift */, + 03A54C332B533BBA0064CCDE /* Instance */, 03FD64FD2AE538C600957AA9 /* Community */, 030D00862AD1BB0D00953B1D /* User */, 03B7AAF02ABE404300068B23 /* ContentModel.swift */, @@ -3037,6 +3062,7 @@ 03A18CBD2B1005A400BA69D2 /* SaveUserSettings.swift in Sources */, 03E0B9C82A61F0F400FED265 /* AdvancedSettingsView.swift in Sources */, 63344C622A08460D001BC616 /* View+Border.swift in Sources */, + 03A54C352B533BC50064CCDE /* InstanceModel.swift in Sources */, 637218702A3A2AAD008C4816 /* ResolveObject.swift in Sources */, CDE6A8162A490AE00062D161 /* InboxMessageBodyView.swift in Sources */, CD04D5DD2A361564008EF95B /* ReplyButtonView.swift in Sources */, @@ -3071,6 +3097,7 @@ B1955A1D2A606B950056CF99 /* Easter Rewards.swift in Sources */, CDB0117F2A6F70A000D043EB /* Editor Tracker.swift in Sources */, 030E86482AC6FD1D000283A6 /* _assignIfNotEqual.swift in Sources */, + 03A54C372B545A430064CCDE /* InstanceModel+MenuFunctions.swift in Sources */, 6354F30A2A2E20040074C08D /* View+Alert.swift in Sources */, 0315E9F52B41C3EB00E3BA88 /* CommunityView.swift in Sources */, 03EC92992AC0BF8A007BBE7E /* APIClient+Pictrs.swift in Sources */, @@ -3347,6 +3374,7 @@ 03EF1D0C2B434CB10056175C /* CommunityStatsView.swift in Sources */, 6363D5C727EE196700E34822 /* ContentView.swift in Sources */, 03F4DC9D2B193F4C00556C67 /* MatrixLinkView.swift in Sources */, + 03A54C322B5331F30064CCDE /* InstanceView.swift in Sources */, 6D8F08FF2A4029AE003EB4FD /* Community List View.swift in Sources */, 035EB0CA2A8687C200227859 /* JumpButtonView.swift in Sources */, 5016A2B12A67EB8600B257E8 /* UIViewController+TopMostViewController.swift in Sources */, diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index 1572ae4bd..00656f92a 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -58,7 +58,7 @@ class APIClient { let decoder: JSONDecoder let transport: (URLSession, URLRequest) async throws -> (Data, URLResponse) - private(set) var session: APISession = .undefined + var session: APISession = .undefined // MARK: - Initialisation diff --git a/Mlem/Enums/AvatarType.swift b/Mlem/Enums/AvatarType.swift index fa4e7d19c..d5f769757 100644 --- a/Mlem/Enums/AvatarType.swift +++ b/Mlem/Enums/AvatarType.swift @@ -9,7 +9,7 @@ import Foundation /// Enum of things that can have avatars enum AvatarType { - case user, community + case user, community, instance } extension AvatarType: AssociatedIcon { @@ -19,6 +19,8 @@ extension AvatarType: AssociatedIcon { return Icons.user case .community: return Icons.community + case .instance: + return Icons.instance } } @@ -28,6 +30,8 @@ extension AvatarType: AssociatedIcon { return Icons.userFill case .community: return Icons.communityFill + case .instance: + return Icons.instance } } } diff --git a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift index 39b2217be..4cb7b248a 100644 --- a/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift +++ b/Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift @@ -63,6 +63,8 @@ struct HandleLemmyLinksDisplay: ViewModifier { UserView(user: user, communityContext: communityContext) .environmentObject(appState) .environmentObject(quickLookState) + case let .instance(domainName, instance): + InstanceView(domainName: domainName, instance: instance) case let .postLinkWithContext(post): ExpandedPost(post: post.post, community: post.community, scrollTarget: post.scrollTarget) .environmentObject(post.postTracker) diff --git a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift index aee6f65d7..b6c405038 100644 --- a/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift +++ b/Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift @@ -98,6 +98,21 @@ extension CommunityModel { functions.append(.standard(function)) functions.append(.standard(favoriteMenuFunction(callback))) } + if let instanceHost = self.communityUrl.host() { + let instance: InstanceModel? + if let site { + instance = .init(from: site) + } else { + instance = nil + } + functions.append( + .navigationMenuFunction( + text: instanceHost, + imageName: Icons.instance, + destination: .instance(instanceHost, instance) + ) + ) + } functions.append( .standardMenuFunction( text: "Copy Name", diff --git a/Mlem/Models/Content/Instance/InstanceModel+MenuFunctions.swift b/Mlem/Models/Content/Instance/InstanceModel+MenuFunctions.swift new file mode 100644 index 000000000..7a0b21256 --- /dev/null +++ b/Mlem/Models/Content/Instance/InstanceModel+MenuFunctions.swift @@ -0,0 +1,17 @@ +// +// InstanceModel+MenuFunctions.swift +// Mlem +// +// Created by Sjmarf on 14/01/2024. +// + +import Foundation + +extension InstanceModel { + func menuFunctions() -> [MenuFunction] { + if let url { + return [.shareMenuFunction(url: url)] + } + return [] + } +} diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift new file mode 100644 index 000000000..bc9627b5f --- /dev/null +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -0,0 +1,63 @@ +// +// InstanceModel.swift +// Mlem +// +// Created by Sjmarf on 13/01/2024. +// + +import SwiftUI + +struct InstanceModel { + var instanceId: Int! + var name: String! + var description: String? + var avatar: URL? + var banner: URL? + var administrators: [UserModel]? + var url: URL! + + init(from response: SiteResponse) { + self.update(with: response) + } + + init(from site: APISite) { + self.update(with: site) + } + + mutating func update(with response: SiteResponse) { + self.administrators = response.admins.map { + var user = UserModel(from: $0, usesExternalData: true) + user.isAdmin = true + return user + } + self.update(with: response.siteView.site) + } + + mutating func update(with site: APISite) { + instanceId = site.id + name = site.name + description = site.sidebar + avatar = site.iconUrl + banner = site.bannerUrl + + if var components = URLComponents(string: site.inboxUrl) { + components.path = "" + url = components.url + } + } +} + +extension InstanceModel: Identifiable { + var id: Int { hashValue } +} + +extension InstanceModel: Hashable { + static func == (lhs: InstanceModel, rhs: InstanceModel) -> 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(instanceId) + } +} diff --git a/Mlem/Models/Content/User/UserModel+MenuFunctions.swift b/Mlem/Models/Content/User/UserModel+MenuFunctions.swift index 315bcecdc..1bb2eb7c1 100644 --- a/Mlem/Models/Content/User/UserModel+MenuFunctions.swift +++ b/Mlem/Models/Content/User/UserModel+MenuFunctions.swift @@ -25,6 +25,15 @@ extension UserModel { func menuFunctions(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> [MenuFunction] { var functions: [MenuFunction] = .init() + if let instanceHost = self.profileUrl.host() { + functions.append( + .navigationMenuFunction( + text: instanceHost, + imageName: Icons.instance, + destination: .instance(instanceHost) + ) + ) + } functions.append( .standardMenuFunction( text: "Copy Username", diff --git a/Mlem/Models/Content/User/UserModel.swift b/Mlem/Models/Content/User/UserModel.swift index 74bccc689..136eb791d 100644 --- a/Mlem/Models/Content/User/UserModel.swift +++ b/Mlem/Models/Content/User/UserModel.swift @@ -66,6 +66,9 @@ struct UserModel { "https://lemmy.ml/u/sjmarf" ] + /// Is True when the UserModel was created using data fetched from an instance other than the logged-in instance + var usesExternalData: Bool = false + /// Creates a UserModel from an GetPersonDetailsResponse /// - Parameter response: GetPersonDetailsResponse to create a UserModel representation of init(from response: GetPersonDetailsResponse) { @@ -75,11 +78,13 @@ struct UserModel { /// Creates a UserModel from an APIPersonView /// - Parameter apiPersonView: APIPersonView to create a UserModel representation of - init(from personView: APIPersonView) { + init(from personView: APIPersonView, usesExternalData: Bool = false) { self.init(from: personView.person) self.postCount = personView.counts.postCount self.commentCount = personView.counts.commentCount + + self.usesExternalData = usesExternalData // TODO: 0.18 Deprecation @Dependency(\.siteInformation) var siteInformation diff --git a/Mlem/Models/Menu Function.swift b/Mlem/Models/Menu Function.swift index 7aa1d10b1..ad87b203f 100644 --- a/Mlem/Models/Menu Function.swift +++ b/Mlem/Models/Menu Function.swift @@ -17,12 +17,15 @@ enum MenuFunction: Identifiable { return shareMenuFunction.id case let .shareImage(shareImageFunction): return shareImageFunction.id + case let .navigation(navigationMenuFunction): + return navigationMenuFunction.id } } case standard(StandardMenuFunction) case shareUrl(ShareMenuFunction) case shareImage(ShareImageFunction) + case navigation(NavigationMenuFunction) } // some convenience initializers because MenuFunction.standard(StandardMenuFunction...) is ugly @@ -43,6 +46,18 @@ extension MenuFunction { )) } + static func navigationMenuFunction( + text: String, + imageName: String, + destination: AppRoute + ) -> MenuFunction { + MenuFunction.navigation(NavigationMenuFunction( + text: text, + imageName: imageName, + destination: destination + )) + } + static func shareMenuFunction(url: URL) -> MenuFunction { MenuFunction.shareUrl(ShareMenuFunction(url: url)) } @@ -79,3 +94,11 @@ struct StandardMenuFunction: Identifiable { let enabled: Bool let callback: () -> Void } + +struct NavigationMenuFunction: Identifiable { + var id: String { text } + + let text: String + let imageName: String + let destination: AppRoute +} diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index 23543b59d..d227021f8 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -18,6 +18,7 @@ enum AppRoute: Routable { case apiPost(APIPost) case community(CommunityModel) + case instance(String, InstanceModel? = nil) @available(*, deprecated, message: "Use .userProfile instead.") case apiPerson(APIPerson) diff --git a/Mlem/Repositories/PersonRepository.swift b/Mlem/Repositories/PersonRepository.swift index 597ead0ab..fdf1e0bdd 100644 --- a/Mlem/Repositories/PersonRepository.swift +++ b/Mlem/Repositories/PersonRepository.swift @@ -8,6 +8,10 @@ import Dependencies import Foundation +enum PersonRequestError: Error { + case notFound +} + class PersonRepository { @Dependency(\.apiClient) private var apiClient @@ -47,6 +51,16 @@ class PersonRepository { try await apiClient.getPersonDetails(for: id, limit: limit, savedOnly: savedOnly) } + func loadUserDetails(for url: URL, limit: Int, savedOnly: Bool = false) async throws -> GetPersonDetailsResponse { + let result = try await apiClient.resolve(query: url.absoluteString) + switch result { + case .person(let person): + return try await loadUserDetails(for: person.person.id, limit: limit, savedOnly: savedOnly) + default: + throw PersonRequestError.notFound + } + } + func getUnreadCounts() async throws -> APIPersonUnreadCounts { do { return try await apiClient.getUnreadCount() diff --git a/Mlem/Views/Shared/Avatars/DefaultAvatarView.swift b/Mlem/Views/Shared/Avatars/DefaultAvatarView.swift index 0e55ed07a..e3595b7a0 100644 --- a/Mlem/Views/Shared/Avatars/DefaultAvatarView.swift +++ b/Mlem/Views/Shared/Avatars/DefaultAvatarView.swift @@ -12,11 +12,27 @@ struct DefaultAvatarView: View { let avatarType: AvatarType var body: some View { - Image(systemName: avatarType.iconNameFill) - .resizable() - .scaledToFill() - .symbolRenderingMode(.multicolor) - .foregroundStyle(Color.gray.gradient, .white) + switch avatarType { + case .instance: + ZStack { + Circle() + .fill(Color.gray.gradient) + GeometryReader { frame in + Image(systemName: avatarType.iconNameFill) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(.horizontal, frame.size.width * 0.2) + .foregroundStyle(.white) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + default: + Image(systemName: avatarType.iconNameFill) + .resizable() + .scaledToFill() + .symbolRenderingMode(.multicolor) + .foregroundStyle(Color.gray.gradient, .white) + } } } @@ -26,6 +42,8 @@ struct DefaultAvatarView: View { .frame(width: 100, height: 100) DefaultAvatarView(avatarType: .community) .frame(width: 100, height: 100) + DefaultAvatarView(avatarType: .instance) + .frame(width: 100, height: 100) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.black) diff --git a/Mlem/Views/Shared/Components/Components/Menu Button.swift b/Mlem/Views/Shared/Components/Components/Menu Button.swift index f42d33134..ec3d2f59b 100644 --- a/Mlem/Views/Shared/Components/Components/Menu Button.swift +++ b/Mlem/Views/Shared/Components/Components/Menu Button.swift @@ -30,6 +30,10 @@ struct MenuButton: View { Label(standardMenuFunction.text, systemImage: standardMenuFunction.imageName) } .disabled(!standardMenuFunction.enabled) + case let .navigation(navigationMenuFunction): + NavigationLink(navigationMenuFunction.destination) { + Label(navigationMenuFunction.text, systemImage: navigationMenuFunction.imageName) + } } } } diff --git a/Mlem/Views/Shared/Error View.swift b/Mlem/Views/Shared/Error View.swift index 3ded07b5b..9634e8f1c 100644 --- a/Mlem/Views/Shared/Error View.swift +++ b/Mlem/Views/Shared/Error View.swift @@ -54,7 +54,10 @@ struct ErrorView: View { .font(.title3.bold()) .foregroundStyle(.primary) - if let body = errorDetails.body { Text(body) } + if let body = errorDetails.body { + Text(body) + .multilineTextAlignment(.center) + } if !errorDetails.autoRefresh, let refresh = errorDetails.refresh { Button { diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift new file mode 100644 index 000000000..38ceab550 --- /dev/null +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -0,0 +1,184 @@ +// +// InstanceView.swift +// Mlem +// +// Created by Sjmarf on 13/01/2024. +// + +import SwiftUI +import Charts +import Dependencies + +enum InstanceViewTab: String, Identifiable, CaseIterable { + case about, administrators, statistics, uptime, safety + + var id: Self { self } + + var label: String { + switch self { + case .safety: + return "Trust & Safety" + default: + return rawValue.capitalized + } + } +} + +struct InstanceView: View { + @Dependency(\.errorHandler) var errorHandler + + @Environment(\.navigationPathWithRoutes) private var navigationPath + @Environment(\.scrollViewProxy) private var scrollViewProxy + + @State var domainName: String + @State var instance: InstanceModel? + @State var errorDetails: ErrorDetails? + + @Namespace var scrollToTop + @State private var scrollToTopAppeared = false + + @State var selectedTab: InstanceViewTab = .about + + init(domainName: String, instance: InstanceModel? = nil) { + _domainName = State(wrappedValue: domainName) + _instance = State(wrappedValue: instance) + } + + var body: some View { + ScrollView { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + VStack(spacing: AppConstants.postAndCommentSpacing) { + AvatarBannerView(instance: instance) + .padding(.horizontal, AppConstants.postAndCommentSpacing) + .padding(.top, 10) + VStack(spacing: 5) { + if errorDetails == nil { + if let instance { + Text(instance.name) + .font(.title) + .fontWeight(.semibold) + .lineLimit(1) + .minimumScaleFactor(0.01) + Text(domainName) + .font(.footnote) + .foregroundStyle(.secondary) + .padding(.bottom, 5) + } + } else { + Text(domainName) + .font(.title) + .fontWeight(.semibold) + .lineLimit(1) + .minimumScaleFactor(0.01) + .padding(.bottom, 5) + Divider() + + } + } + if let instance { + VStack(spacing: 0) { + VStack(spacing: 4) { + Divider() + BubblePicker([.about, .administrators], selected: $selectedTab) { tab in + Text(tab.label) + } + Divider() + } + switch self.selectedTab { + case .about: + if let description = instance.description { + MarkdownView(text: description, isNsfw: false) + .padding(.horizontal, AppConstants.postAndCommentSpacing) + .padding(.top) + } else { + Text("No Description") + .foregroundStyle(.secondary) + } + case .administrators: + if let administrators = instance.administrators { + Divider() + ForEach(administrators, id: \.self) { user in + UserResultView(user) + Divider() + } + } else { + ProgressView() + } + default: + EmptyView() + } + Spacer() + .frame(height: 100) + } + + } else if let errorDetails { + ErrorView(errorDetails) + } else { + LoadingView(whatIsLoading: .instanceDetails) + } + } + } + .toolbar { + if let instance { + ToolbarItem(placement: .topBarTrailing) { + Link(destination: instance.url) { + Label("Open in Browser", systemImage: Icons.browser) + } + } + } + } + .task { + if instance?.administrators == nil { + do { + let client = APIClient(transport: { urlSession, urlRequest in try await urlSession.data(for: urlRequest) }) + let url = try await getCorrectURLtoEndpoint(baseInstanceAddress: domainName) + client.session = .unauthenticated(url) + let info = try await client.loadSiteInformation() + DispatchQueue.main.async { + withAnimation(.easeOut(duration: 0.2)) { + if var instance { + instance.update(with: info) + self.instance = instance + } else { + self.instance = InstanceModel(from: info) + } + } + } + } catch EndpointDiscoveryError.couldNotFindAnyCorrectEndpoints { + withAnimation(.easeOut(duration: 0.2)) { + errorDetails = ErrorDetails( + title: "Cannot Connect to Instance", + body: "Maybe this is a KBin instance? Mlem can't yet display KBin instance details.", + icon: "point.3.filled.connected.trianglepath.dotted" + ) + } + } catch { + withAnimation(.easeOut(duration: 0.2)) { + errorDetails = ErrorDetails(error: error) + } + } + } + } + .fancyTabScrollCompatible() + .hoistNavigation { + if navigationPath.isEmpty { + withAnimation { + scrollViewProxy?.scrollTo(scrollToTop) + } + return true + } else { + if scrollToTopAppeared { + return false + } else { + withAnimation { + scrollViewProxy?.scrollTo(scrollToTop) + } + return true + } + } + } + .navigationTitle(instance?.name ?? domainName) + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/Mlem/Views/Shared/Links/AvatarView.swift b/Mlem/Views/Shared/Links/AvatarView.swift index d8a48ae6d..fb32e2444 100644 --- a/Mlem/Views/Shared/Links/AvatarView.swift +++ b/Mlem/Views/Shared/Links/AvatarView.swift @@ -86,6 +86,25 @@ struct AvatarView: View { ) } + init( + instance: InstanceModel, + avatarSize: CGFloat, + blurAvatar: Bool = false, + lineColor: Color? = nil, + lineWidth: CGFloat = 1, + iconResolution: AvatarIconResolution? = nil + ) { + self.init( + url: instance.avatar, + type: .instance, + avatarSize: avatarSize, + blurAvatar: blurAvatar, + lineColor: lineColor, + lineWidth: lineWidth, + iconResolution: iconResolution + ) + } + static func shouldShowCommunityAvatarOutline(url: URL?) -> Bool { guard let hostString = url?.host else { return true diff --git a/Mlem/Views/Tabs/Profile/AvatarBannerView.swift b/Mlem/Views/Tabs/Profile/AvatarBannerView.swift index 4a72be6a5..3a960b453 100644 --- a/Mlem/Views/Tabs/Profile/AvatarBannerView.swift +++ b/Mlem/Views/Tabs/Profile/AvatarBannerView.swift @@ -15,29 +15,37 @@ struct AvatarBannerView: View { let avatar: URL? let banner: URL? - let showBanner: Bool - let showAvatar: Bool + var showEmptyBanner: Bool = false + var showBanner: Bool = true + var showAvatar: Bool = true - init(user: UserModel) { + init(user: UserModel?) { self.type = .user - self.avatar = user.avatar - self.banner = user.banner + self.avatar = user?.avatar + self.banner = user?.banner @AppStorage("shouldShowUserHeaders") var shouldShowUserHeaders: Bool = true self.showBanner = shouldShowUserHeaders @AppStorage("shouldShowUserAvatars") var shouldShowUserAvatars: Bool = true self.showAvatar = shouldShowUserAvatars } - init(community: CommunityModel) { + init(community: CommunityModel?) { self.type = .community - self.avatar = community.avatar - self.banner = community.banner + self.avatar = community?.avatar + self.banner = community?.banner @AppStorage("shouldShowCommunityHeaders") var shouldShowCommunityHeaders: Bool = true self.showBanner = shouldShowCommunityHeaders @AppStorage("shouldShowCommunityIcons") var shouldShowCommunityIcons: Bool = true self.showAvatar = shouldShowCommunityIcons } + init(instance: InstanceModel?) { + self.type = .instance + self.avatar = instance?.avatar + self.banner = instance?.banner + self.showEmptyBanner = true + } + static let bannerHeight: CGFloat = 170 static let avatarOverdraw: CGFloat = 40 static let avatarSize: CGFloat = 108 @@ -55,7 +63,7 @@ struct AvatarBannerView: View { var body: some View { Group { - if let banner, showBanner { + if (banner != nil || showEmptyBanner) && showBanner { ZStack(alignment: .bottom) { VStack { LazyImage(url: banner) { state in diff --git a/Mlem/Views/Tabs/Profile/UserView+Logic.swift b/Mlem/Views/Tabs/Profile/UserView+Logic.swift index 21b6adc09..0ef668e3d 100644 --- a/Mlem/Views/Tabs/Profile/UserView+Logic.swift +++ b/Mlem/Views/Tabs/Profile/UserView+Logic.swift @@ -41,8 +41,17 @@ extension UserView { func tryReloadUser() async { do { - let authoredContent = try await personRepository.loadUserDetails(for: user.userId, limit: internetSpeed.pageSize) - self.user = UserModel(from: authoredContent) + let authoredContent: GetPersonDetailsResponse + if user.usesExternalData { + authoredContent = try await personRepository.loadUserDetails(for: user.profileUrl, limit: internetSpeed.pageSize) + } else { + authoredContent = try await personRepository.loadUserDetails(for: user.userId, limit: internetSpeed.pageSize) + } + + var newUser = UserModel(from: authoredContent) + newUser.isAdmin = user.isAdmin + self.user = newUser + self.communityTracker.replaceAll(with: user.moderatedCommunities ?? []) var savedContentData: GetPersonDetailsResponse? diff --git a/Mlem/Views/Tabs/Profile/UserView.swift b/Mlem/Views/Tabs/Profile/UserView.swift index 2af52885b..d265423bf 100644 --- a/Mlem/Views/Tabs/Profile/UserView.swift +++ b/Mlem/Views/Tabs/Profile/UserView.swift @@ -250,7 +250,7 @@ struct UserView: View { flairBackground(color: flair.color) { HStack { Image(systemName: Icons.adminFlair) - let host = try? apiClient.session.instanceUrl.host() + let host = user.profileUrl.host() Text("\(host ?? "Instance") Administrator") } } From fe8c799ebb324b629a02e63f0f7a380a5929c3eb Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 14 Jan 2024 19:28:59 +0000 Subject: [PATCH 02/40] Show instance version --- Mlem/Models/Content/Instance/InstanceModel.swift | 2 ++ Mlem/Views/Shared/Instance/InstanceView.swift | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index bc9627b5f..10ca52888 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -15,6 +15,7 @@ struct InstanceModel { var banner: URL? var administrators: [UserModel]? var url: URL! + var version: SiteVersion? init(from response: SiteResponse) { self.update(with: response) @@ -30,6 +31,7 @@ struct InstanceModel { user.isAdmin = true return user } + self.version = SiteVersion(response.version) self.update(with: response.siteView.site) } diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 38ceab550..a2a562ef6 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -44,6 +44,14 @@ struct InstanceView: View { _instance = State(wrappedValue: instance) } + var subtitleText: String { + if let version = instance?.version { + "\(domainName) • \(String(describing: version))" + } else { + domainName + } + } + var body: some View { ScrollView { ScrollToView(appeared: $scrollToTopAppeared) @@ -60,10 +68,10 @@ struct InstanceView: View { .fontWeight(.semibold) .lineLimit(1) .minimumScaleFactor(0.01) - Text(domainName) + + Text(subtitleText) .font(.footnote) .foregroundStyle(.secondary) - .padding(.bottom, 5) } } else { Text(domainName) @@ -73,9 +81,9 @@ struct InstanceView: View { .minimumScaleFactor(0.01) .padding(.bottom, 5) Divider() - } } + .padding(.bottom, 5) if let instance { VStack(spacing: 0) { VStack(spacing: 4) { From 84c9e38637efea36c1751ae8a4803c3d8f412d64 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 14 Jan 2024 19:57:55 +0000 Subject: [PATCH 03/40] Rewrite CommunityResultView and UserResultView --- Mlem/Views/Shared/Instance/InstanceView.swift | 2 +- Mlem/Views/Tabs/Feeds/CommunityView.swift | 2 +- Mlem/Views/Tabs/Profile/UserFeedView.swift | 2 +- .../Tabs/Search/RecentSearchesView.swift | 4 +- .../Search/Results/CommunityResultView.swift | 35 ++++-- .../Tabs/Search/Results/UserResultView.swift | 116 +++++++++++------- .../Tabs/Search/SearchResultListView.swift | 20 ++- 7 files changed, 110 insertions(+), 71 deletions(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index a2a562ef6..5633e3f28 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -107,7 +107,7 @@ struct InstanceView: View { if let administrators = instance.administrators { Divider() ForEach(administrators, id: \.self) { user in - UserResultView(user) + UserResultView(user, complications: [.instance, .date]) Divider() } } else { diff --git a/Mlem/Views/Tabs/Feeds/CommunityView.swift b/Mlem/Views/Tabs/Feeds/CommunityView.swift index 5d2a1a436..8f703394f 100644 --- a/Mlem/Views/Tabs/Feeds/CommunityView.swift +++ b/Mlem/Views/Tabs/Feeds/CommunityView.swift @@ -118,7 +118,7 @@ struct CommunityView: View { .padding(.top, 15) .background(Color.secondarySystemBackground) ForEach(moderators, id: \.id) { user in - UserResultView(user, communityContext: community) + UserResultView(user, complications: [.instance, .date], communityContext: community) Divider() } Color.secondarySystemBackground diff --git a/Mlem/Views/Tabs/Profile/UserFeedView.swift b/Mlem/Views/Tabs/Profile/UserFeedView.swift index 1bf7303c3..9d748dda4 100644 --- a/Mlem/Views/Tabs/Profile/UserFeedView.swift +++ b/Mlem/Views/Tabs/Profile/UserFeedView.swift @@ -50,7 +50,7 @@ struct UserFeedView: View { .padding(.vertical, 4) Divider() ForEach(communityTracker.items, id: \.uid) { community in - CommunityResultView(community, showTypeLabel: false, trackerCallback: { + CommunityResultView(community, complications: .instanceOnly, trackerCallback: { communityTracker.update(with: $0) }) diff --git a/Mlem/Views/Tabs/Search/RecentSearchesView.swift b/Mlem/Views/Tabs/Search/RecentSearchesView.swift index 4ed143a83..ceb236642 100644 --- a/Mlem/Views/Tabs/Search/RecentSearchesView.swift +++ b/Mlem/Views/Tabs/Search/RecentSearchesView.swift @@ -74,7 +74,7 @@ struct RecentSearchesView: View { if let community = contentModel.wrappedValue as? CommunityModel { CommunityResultView( community, - showTypeLabel: true, + complications: .withTypeLabel, swipeActions: .init(trailingActions: [deleteSwipeAction(contentModel)]), trackerCallback: { contentTracker.update(with: AnyContentModel($0)) @@ -83,7 +83,7 @@ struct RecentSearchesView: View { } else if let user = contentModel.wrappedValue as? UserModel { UserResultView( user, - showTypeLabel: true, + complications: [.type, .instance, .comments], swipeActions: .init(trailingActions: [deleteSwipeAction(contentModel)]), trackerCallback: { contentTracker.update(with: AnyContentModel($0)) diff --git a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift index 6891f59e3..b51d2a69e 100644 --- a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift +++ b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift @@ -8,14 +8,24 @@ import SwiftUI import Dependencies +enum CommunityComplication: CaseIterable { + case type, instance, subscribers +} + +extension Array where Element == CommunityComplication { + static let withTypeLabel: [CommunityComplication] = [.type, .instance, .subscribers] + static let withoutTypeLabel: [CommunityComplication] = [.instance, .subscribers] + static let instanceOnly: [CommunityComplication] = [.instance] +} + struct CommunityResultView: View { @Dependency(\.apiClient) private var apiClient @Dependency(\.hapticManager) var hapticManager let community: CommunityModel - let showTypeLabel: Bool let trackerCallback: (_ item: CommunityModel) -> Void let swipeActions: SwipeConfiguration? + let complications: [CommunityComplication] @State private var isPresentingConfirmDestructive: Bool = false @State private var confirmationMenuFunction: StandardMenuFunction? @@ -24,12 +34,12 @@ struct CommunityResultView: View { init( _ community: CommunityModel, - showTypeLabel: Bool = false, + complications: [CommunityComplication] = .withoutTypeLabel, swipeActions: SwipeConfiguration? = nil, trackerCallback: @escaping (_ item: CommunityModel) -> Void = { _ in } ) { self.community = community - self.showTypeLabel = showTypeLabel + self.complications = complications self.swipeActions = swipeActions self.trackerCallback = trackerCallback } @@ -50,14 +60,14 @@ struct CommunityResultView: View { } var caption: String { - if let host = community.communityUrl.host { - if showTypeLabel { - return "Community ∙ @\(host)" - } else { - return "@\(host)" - } + var parts: [String] = [] + if complications.contains(.type) { + parts.append("Community") + } + if complications.contains(.instance), let host = community.communityUrl.host { + parts.append("@\(host)") } - return "Unknown instance" + return parts.joined(separator: " ∙ ") } var subscriberCountColor: Color { @@ -103,7 +113,7 @@ struct CommunityResultView: View { .lineLimit(1) } Spacer() - if let subscriberCount = community.subscriberCount { + if complications.contains(.subscribers), let subscriberCount = community.subscriberCount { HStack(spacing: 5) { Text(abbreviateNumber(subscriberCount)) .monospacedDigit() @@ -151,7 +161,6 @@ struct CommunityResultView: View { #Preview { CommunityResultView( - .init(from: .mock()), - showTypeLabel: true + .init(from: .mock()) ) } diff --git a/Mlem/Views/Tabs/Search/Results/UserResultView.swift b/Mlem/Views/Tabs/Search/Results/UserResultView.swift index b19a17292..cdbd0b05d 100644 --- a/Mlem/Views/Tabs/Search/Results/UserResultView.swift +++ b/Mlem/Views/Tabs/Search/Results/UserResultView.swift @@ -8,31 +8,39 @@ import Dependencies import SwiftUI +enum UserComplication: CaseIterable { + case type, instance, date, posts, comments +} + +extension Array where Element == UserComplication { + static let withTypeLabel: [UserComplication] = [.type, .instance, .comments] + static let withoutTypeLabel: [UserComplication] = [.instance, .date, .posts, .comments] + static let instanceOnly: [UserComplication] = [.instance] +} + struct UserResultView: View { @Dependency(\.apiClient) private var apiClient @Dependency(\.hapticManager) var hapticManager - @EnvironmentObject var contentTracker: ContentTracker - let user: UserModel let communityContext: CommunityModel? - let showTypeLabel: Bool let trackerCallback: (_ item: UserModel) -> Void let swipeActions: SwipeConfiguration? + let complications: [UserComplication] @State private var isPresentingConfirmDestructive: Bool = false @State private var confirmationMenuFunction: StandardMenuFunction? init( _ user: UserModel, + complications: [UserComplication] = .withoutTypeLabel, communityContext: CommunityModel? = nil, - showTypeLabel: Bool = false, swipeActions: SwipeConfiguration? = nil, trackerCallback: @escaping (_ item: UserModel) -> Void = { _ in } ) { self.user = user + self.complications = complications self.communityContext = communityContext - self.showTypeLabel = showTypeLabel self.swipeActions = swipeActions self.trackerCallback = trackerCallback } @@ -51,16 +59,19 @@ struct UserResultView: View { } var caption: String { - if let host = user.profileUrl.host { - if showTypeLabel { - return "User ∙ @\(host)" - } else { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy" - return "@\(host) ∙ \(dateFormatter.string(from: user.creationDate))" - } + var parts: [String] = [] + if complications.contains(.type) { + parts.append("User") } - return "Unknown instance" + if complications.contains(.instance), let host = user.profileUrl.host { + parts.append("@\(host)") + } + if complications.contains(.date) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy" + parts.append(dateFormatter.string(from: user.creationDate)) + } + return parts.joined(separator: " ∙ ") } var body: some View { @@ -92,37 +103,7 @@ struct UserResultView: View { .lineLimit(1) } Spacer() - if showTypeLabel { - HStack(spacing: 5) { - if let commentCount = user.commentCount { - Text(abbreviateNumber(commentCount)) - .monospacedDigit() - Image(systemName: Icons.replies) - } - } - .foregroundStyle(.secondary) - } else { - if let commentCount = user.commentCount, let postCount = user.postCount { - HStack(spacing: 5) { - VStack(alignment: .trailing, spacing: 6) { - Text(abbreviateNumber(postCount)) - .font(.subheadline) - .monospacedDigit() - Text(abbreviateNumber(commentCount)) - .font(.subheadline) - .monospacedDigit() - } - .foregroundStyle(.secondary) - VStack(spacing: 10) { - Image(systemName: Icons.posts) - .imageScale(.small) - Image(systemName: Icons.replies) - .imageScale(.small) - } - } - .foregroundStyle(.secondary) - } - } + trailingInfo Image(systemName: Icons.forward) .imageScale(.small) .foregroundStyle(.tertiary) @@ -154,11 +135,52 @@ struct UserResultView: View { } } } + + @ViewBuilder + var trailingInfo: some View { + Group { + if complications.contains(.posts), let postCount = user.postCount { + if complications.contains(.comments), let commentCount = user.commentCount { + HStack(spacing: 5) { + VStack(alignment: .trailing, spacing: 6) { + Text(abbreviateNumber(postCount)) + .font(.subheadline) + .monospacedDigit() + Text(abbreviateNumber(commentCount)) + .font(.subheadline) + .monospacedDigit() + } + .foregroundStyle(.secondary) + VStack(spacing: 10) { + Image(systemName: Icons.posts) + .imageScale(.small) + Image(systemName: Icons.replies) + .imageScale(.small) + } + } + .foregroundStyle(.secondary) + } else { + HStack(spacing: 5) { + Text(abbreviateNumber(postCount)) + .monospacedDigit() + Image(systemName: Icons.posts) + } + .foregroundStyle(.secondary) + } + } else if complications.contains(.comments), let commentCount = user.commentCount { + HStack(spacing: 5) { + Text(abbreviateNumber(commentCount)) + .monospacedDigit() + Image(systemName: Icons.replies) + } + .foregroundStyle(.secondary) + } + } + } } #Preview { UserResultView( - .init(from: .mock()), - showTypeLabel: true + .init(from: .mock()) ) } diff --git a/Mlem/Views/Tabs/Search/SearchResultListView.swift b/Mlem/Views/Tabs/Search/SearchResultListView.swift index 97c46ccd7..5722c27b5 100644 --- a/Mlem/Views/Tabs/Search/SearchResultListView.swift +++ b/Mlem/Views/Tabs/Search/SearchResultListView.swift @@ -21,13 +21,21 @@ struct SearchResultListView: View { ForEach(contentTracker.items, id: \.uid) { contentModel in Group { if let community = contentModel.wrappedValue as? CommunityModel { - CommunityResultView(community, showTypeLabel: showTypeLabel, trackerCallback: { - contentTracker.update(with: AnyContentModel($0)) - }) + CommunityResultView( + community, + complications: showTypeLabel ? .withTypeLabel : .withoutTypeLabel, + trackerCallback: { + contentTracker.update(with: AnyContentModel($0)) + } + ) } else if let user = contentModel.wrappedValue as? UserModel { - UserResultView(user, showTypeLabel: showTypeLabel, trackerCallback: { - contentTracker.update(with: AnyContentModel($0)) - }) + UserResultView( + user, + complications: showTypeLabel ? .withTypeLabel : .withoutTypeLabel, + trackerCallback: { + contentTracker.update(with: AnyContentModel($0)) + } + ) } } .simultaneousGesture(TapGesture().onEnded { From e298d1d6b7b8d4aa999ff7132c3fcfe81ed59dff Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 14 Jan 2024 19:59:58 +0000 Subject: [PATCH 04/40] Remove extra divider --- Mlem/Views/Shared/Instance/InstanceView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 5633e3f28..92944ab96 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -105,7 +105,6 @@ struct InstanceView: View { } case .administrators: if let administrators = instance.administrators { - Divider() ForEach(administrators, id: \.self) { user in UserResultView(user, complications: [.instance, .date]) Divider() From 95a467163c367941ae41127976fce860c4fd7536 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:02:22 +0000 Subject: [PATCH 05/40] Don't show instance in admin list --- Mlem/Views/Shared/Instance/InstanceView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 92944ab96..8183cf31e 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -106,7 +106,7 @@ struct InstanceView: View { case .administrators: if let administrators = instance.administrators { ForEach(administrators, id: \.self) { user in - UserResultView(user, complications: [.instance, .date]) + UserResultView(user, complications: [.date]) Divider() } } else { From 4a81fc3f9e486cc12a20950b827c82fa5869711a Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Wed, 17 Jan 2024 20:50:39 +0000 Subject: [PATCH 06/40] Use existing `GetSiteRequest(instanceURL: URL)` system --- Mlem/API/APIClient/APIClient.swift | 5 ++++ Mlem/Models/Content/Post Model.swift | 1 + Mlem/Views/Shared/Instance/InstanceView.swift | 25 +++++++++++-------- Mlem/Views/Tabs/Profile/UserView.swift | 1 + 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index e3a8c9239..5e68f9d22 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -384,6 +384,11 @@ extension APIClient { return try await perform(request: request) } + func loadSiteInformation(instanceURL: URL) async throws -> SiteResponse { + let request = GetSiteRequest(instanceURL: instanceURL.appendingPathComponent("api/v3")) + return try await perform(request: request) + } + // swiftlint:disable function_parameter_count func performSearch( query: String, diff --git a/Mlem/Models/Content/Post Model.swift b/Mlem/Models/Content/Post Model.swift index 49081467b..0ee3e71b3 100644 --- a/Mlem/Models/Content/Post Model.swift +++ b/Mlem/Models/Content/Post Model.swift @@ -119,5 +119,6 @@ extension PostModel: Hashable { hasher.combine(saved) hasher.combine(read) hasher.combine(post.updated) + hasher.combine(unreadCommentCount) } } diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 8183cf31e..608fb0b26 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -25,6 +25,7 @@ enum InstanceViewTab: String, Identifiable, CaseIterable { } struct InstanceView: View { + @Dependency(\.apiClient) var apiClient: APIClient @Dependency(\.errorHandler) var errorHandler @Environment(\.navigationPathWithRoutes) private var navigationPath @@ -138,19 +139,20 @@ struct InstanceView: View { .task { if instance?.administrators == nil { do { - let client = APIClient(transport: { urlSession, urlRequest in try await urlSession.data(for: urlRequest) }) - let url = try await getCorrectURLtoEndpoint(baseInstanceAddress: domainName) - client.session = .unauthenticated(url) - let info = try await client.loadSiteInformation() - DispatchQueue.main.async { - withAnimation(.easeOut(duration: 0.2)) { - if var instance { - instance.update(with: info) - self.instance = instance - } else { - self.instance = InstanceModel(from: info) + if let url = URL(string: "https://\(domainName)") { + let info = try await apiClient.loadSiteInformation(instanceURL: url) + DispatchQueue.main.async { + withAnimation(.easeOut(duration: 0.2)) { + if var instance { + instance.update(with: info) + self.instance = instance + } else { + self.instance = InstanceModel(from: info) + } } } + } else { + errorDetails = ErrorDetails(title: "\"\(domainName)\" is an invalid URL.") } } catch EndpointDiscoveryError.couldNotFindAnyCorrectEndpoints { withAnimation(.easeOut(duration: 0.2)) { @@ -185,6 +187,7 @@ struct InstanceView: View { } } } + .navigationBarColor() .navigationTitle(instance?.name ?? domainName) .navigationBarTitleDisplayMode(.inline) } diff --git a/Mlem/Views/Tabs/Profile/UserView.swift b/Mlem/Views/Tabs/Profile/UserView.swift index cc00b4a50..f1a1ee8f2 100644 --- a/Mlem/Views/Tabs/Profile/UserView.swift +++ b/Mlem/Views/Tabs/Profile/UserView.swift @@ -206,6 +206,7 @@ struct UserView: View { } } .fancyTabScrollCompatible() + .navigationBarColor() .navigationTitle(user.displayName) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $isPresentingAccountSwitcher) { From 672a9ba3bed5cfe03215594a027a17aa573f64a9 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Fri, 19 Jan 2024 22:11:06 +0000 Subject: [PATCH 07/40] Bug fix --- Mlem/API/APIClient/APIClient.swift | 2 +- Mlem/Views/Shared/Instance/InstanceView.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index 5e68f9d22..1e7bb764a 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -385,7 +385,7 @@ extension APIClient { } func loadSiteInformation(instanceURL: URL) async throws -> SiteResponse { - let request = GetSiteRequest(instanceURL: instanceURL.appendingPathComponent("api/v3")) + let request = GetSiteRequest(instanceURL: instanceURL.appendingPathComponent("api/v3/")) return try await perform(request: request) } diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 608fb0b26..338675f32 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -85,7 +85,9 @@ struct InstanceView: View { } } .padding(.bottom, 5) - if let instance { + if let errorDetails { + ErrorView(errorDetails) + } else if let instance { VStack(spacing: 0) { VStack(spacing: 4) { Divider() @@ -120,8 +122,6 @@ struct InstanceView: View { .frame(height: 100) } - } else if let errorDetails { - ErrorView(errorDetails) } else { LoadingView(whatIsLoading: .instanceDetails) } From 85e7d4442681887cd543c3df4678b0b71748db81 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Fri, 19 Jan 2024 22:35:04 +0000 Subject: [PATCH 08/40] Fix --- Mlem/API/APIClient/APIClient.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index 1e7bb764a..82c861c03 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -161,8 +161,6 @@ class APIClient { if let overrideToken { urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") - } else if case let .authenticated(_, token) = session { - urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } if defintion as? any APIGetRequest != nil { From 9129886dad002c49f519c8070b91c5bf58b248b1 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 18:33:59 +0000 Subject: [PATCH 09/40] KBin fix --- Mlem/API/APIClient/APIClient.swift | 14 +++++++++----- Mlem/Views/Shared/Instance/InstanceView.swift | 17 +++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index 82c861c03..f5f140b43 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -156,13 +156,17 @@ class APIClient { private func urlRequest(from defintion: any APIRequest, overrideToken: String?) throws -> URLRequest { var urlRequest = URLRequest(url: defintion.endpoint) defintion.headers.forEach { header in - urlRequest.setValue(header.value, forHTTPHeaderField: header.key) + if header.key == "Authorization" { + if let overrideToken { + urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") + } else if case let .authenticated(_, token) = session { + urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + } else { + urlRequest.setValue(header.value, forHTTPHeaderField: header.key) + } } - if let overrideToken { - urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") - } - if defintion as? any APIGetRequest != nil { urlRequest.httpMethod = "GET" } else if let postDefinition = defintion as? any APIPostRequest { diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 338675f32..6c2fea31a 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -154,13 +154,18 @@ struct InstanceView: View { } else { errorDetails = ErrorDetails(title: "\"\(domainName)\" is an invalid URL.") } - } catch EndpointDiscoveryError.couldNotFindAnyCorrectEndpoints { + } catch APIClientError.decoding(let data, let error) { withAnimation(.easeOut(duration: 0.2)) { - errorDetails = ErrorDetails( - title: "Cannot Connect to Instance", - body: "Maybe this is a KBin instance? Mlem can't yet display KBin instance details.", - icon: "point.3.filled.connected.trianglepath.dotted" - ) + if let content = String(data: data, encoding: .utf8), + content.contains("Error 404 - \(domainName)<title/>" ) { + errorDetails = ErrorDetails( + title: "Cannot Connect to Instance", + body: "Maybe this is a KBin instance? Mlem can't yet display KBin instance details.", + icon: "point.3.filled.connected.trianglepath.dotted" + ) + } else { + errorDetails = ErrorDetails(error: APIClientError.decoding(data, error)) + } } } catch { withAnimation(.easeOut(duration: 0.2)) { From cf282ddb73cb8d34d4db8d216e3b8ef8ddea3c53 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 18:35:08 +0000 Subject: [PATCH 10/40] Update InstanceView.swift --- Mlem/Views/Shared/Instance/InstanceView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 6c2fea31a..17767f0cb 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -157,7 +157,7 @@ struct InstanceView: View { } catch APIClientError.decoding(let data, let error) { withAnimation(.easeOut(duration: 0.2)) { if let content = String(data: data, encoding: .utf8), - content.contains("<title>Error 404 - \(domainName)<title/>" ) { + content.contains("<title>Error 404 - \(domainName)" ) { errorDetails = ErrorDetails( title: "Cannot Connect to Instance", body: "Maybe this is a KBin instance? Mlem can't yet display KBin instance details.", From 31acf9cfc43a4e4de6dac9f0e6164a323eb1c8cf Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 18:36:57 +0000 Subject: [PATCH 11/40] Update InstanceView.swift --- Mlem/Views/Shared/Instance/InstanceView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 17767f0cb..769267844 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -159,8 +159,8 @@ struct InstanceView: View { if let content = String(data: data, encoding: .utf8), content.contains("Error 404 - \(domainName)" ) { errorDetails = ErrorDetails( - title: "Cannot Connect to Instance", - body: "Maybe this is a KBin instance? Mlem can't yet display KBin instance details.", + title: "KBin Instance", + body: "We can't yet display KBin instance details.", icon: "point.3.filled.connected.trianglepath.dotted" ) } else { From fa2f0f97e3cd9d97b0bafa20592643bba46551e5 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 18:37:44 +0000 Subject: [PATCH 12/40] Update InstanceView.swift --- Mlem/Views/Shared/Instance/InstanceView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 769267844..9a4ed5209 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -160,7 +160,7 @@ struct InstanceView: View { content.contains("Error 404 - \(domainName)" ) { errorDetails = ErrorDetails( title: "KBin Instance", - body: "We can't yet display KBin instance details.", + body: "We can't yet display KBin details.", icon: "point.3.filled.connected.trianglepath.dotted" ) } else { From d416622716035318b906fe392627a9b9abb02425 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:26:38 +0000 Subject: [PATCH 13/40] Faster loading for own instance page --- Mlem/Models/Trackers/SiteInformationTracker.swift | 3 ++- Mlem/Views/Shared/Instance/InstanceView.swift | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Mlem/Models/Trackers/SiteInformationTracker.swift b/Mlem/Models/Trackers/SiteInformationTracker.swift index 0fd58be25..b15e523af 100644 --- a/Mlem/Models/Trackers/SiteInformationTracker.swift +++ b/Mlem/Models/Trackers/SiteInformationTracker.swift @@ -14,6 +14,7 @@ class SiteInformationTracker: ObservableObject { @Dependency(\.errorHandler) var errorHandler @Dependency(\.accountsTracker) var accountsTracker + @Published private(set) var instance: InstanceModel? @Published private(set) var enableDownvotes = true @Published var version: SiteVersion? @Published private(set) var allLanguages: [APILanguage] = .init() @@ -23,8 +24,8 @@ class SiteInformationTracker: ObservableObject { version = account.siteVersion Task { do { - let response = try await apiClient.loadSiteInformation() + instance = .init(from: response) enableDownvotes = response.siteView.localSite.enableDownvotes version = SiteVersion(response.version) if version != account.siteVersion { diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 9a4ed5209..5380644c6 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -27,6 +27,7 @@ enum InstanceViewTab: String, Identifiable, CaseIterable { struct InstanceView: View { @Dependency(\.apiClient) var apiClient: APIClient @Dependency(\.errorHandler) var errorHandler + @Dependency(\.siteInformation) var siteInformation @Environment(\.navigationPathWithRoutes) private var navigationPath @Environment(\.scrollViewProxy) private var scrollViewProxy @@ -42,6 +43,10 @@ struct InstanceView: View { init(domainName: String, instance: InstanceModel? = nil) { _domainName = State(wrappedValue: domainName) + var instance = instance + if domainName == siteInformation.instance?.url.host() { + instance = siteInformation.instance ?? instance + } _instance = State(wrappedValue: instance) } From a385c1df2aeba77c4cf84c412ffaf2b077c7219a Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 23:31:41 +0000 Subject: [PATCH 14/40] Update --- Mlem/API/APIClient/APIClient.swift | 2 +- Mlem/API/Models/Site/APILocalSite.swift | 16 +- Mlem/Icons.swift | 3 + .../Content/Community/CommunityModel.swift | 14 +- .../Content/Instance/InstanceModel.swift | 54 ++++- .../Notifications/NotificationDisplayer.swift | 13 -- .../Shared/Instance/InstanceStatsView.swift | 191 ++++++++++++++++++ Mlem/Views/Shared/Instance/InstanceView.swift | 15 +- .../Views/Tabs/Feeds/CommunityStatsView.swift | 34 ++-- 9 files changed, 290 insertions(+), 52 deletions(-) create mode 100644 Mlem/Views/Shared/Instance/InstanceStatsView.swift diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index f5f140b43..70de116f6 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -156,7 +156,7 @@ class APIClient { private func urlRequest(from defintion: any APIRequest, overrideToken: String?) throws -> URLRequest { var urlRequest = URLRequest(url: defintion.endpoint) defintion.headers.forEach { header in - if header.key == "Authorization" { + if header.key == "auth" { if let overrideToken { urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") } else if case let .authenticated(_, token) = session { diff --git a/Mlem/API/Models/Site/APILocalSite.swift b/Mlem/API/Models/Site/APILocalSite.swift index d48a42ef5..eae6abacc 100644 --- a/Mlem/API/Models/Site/APILocalSite.swift +++ b/Mlem/API/Models/Site/APILocalSite.swift @@ -7,15 +7,17 @@ import Foundation +enum APICaptchaDifficulty: String, Codable { case easy, medium, hard } + // lemmy_db_schema::source::local_site::LocalSite struct APILocalSite: Decodable { // let id: Int // let siteId: Int // let siteSetup: Bool let enableDownvotes: Bool -// let enableNsfw: Bool -// let communityCreationAdminOnly: Bool -// let requireEmailVerification: Bool + let enableNsfw: Bool + let communityCreationAdminOnly: Bool + let requireEmailVerification: Bool // let applicationQuestion: String? // let privateInstance: Bool // let defaultTheme: String @@ -23,13 +25,13 @@ struct APILocalSite: Decodable { // let legalInformation: String? // let hideModlogModNames: Bool // let applicationEmailAdmins: Bool -// let slurFilterRegex: String? + let slurFilterRegex: String? // let actorNameMaxLength: Int -// let federationEnabled: Bool + let federationEnabled: Bool // let federationDebug: Bool // let federationWorkerCount: Int -// let captchaEnabled: Bool -// let captchaDifficulty: String + let captchaEnabled: Bool + let captchaDifficulty: APICaptchaDifficulty // let registrationMode: APIRegistrationMode // let reportsEmailAdmins: Bool // let published: Date diff --git a/Mlem/Icons.swift b/Mlem/Icons.swift index 180cf36c3..d494c2838 100644 --- a/Mlem/Icons.swift +++ b/Mlem/Icons.swift @@ -108,6 +108,7 @@ struct Icons { static let bannedFlair: String = "multiply.circle" // entities/general Lemmy concepts + static let federation: String = "point.3.filled.connected.trianglepath.dotted" static let instance: String = "server.rack" static let user: String = "person.crop.circle" static let userFill: String = "person.crop.circle.fill" @@ -189,6 +190,8 @@ struct Icons { static let swipeUpGestureSetting: String = "arrow.up.to.line.alt" // misc + static let email: String = "envelope" + static let photo: String = "photo" static let switchUser: String = "person.crop.circle.badge.plus" static let missing: String = "questionmark.square.dashed" static let connection: String = "antenna.radiowaves.left.and.right" diff --git a/Mlem/Models/Content/Community/CommunityModel.swift b/Mlem/Models/Content/Community/CommunityModel.swift index 8066a62d9..2a5518083 100644 --- a/Mlem/Models/Content/Community/CommunityModel.swift +++ b/Mlem/Models/Content/Community/CommunityModel.swift @@ -8,6 +8,13 @@ import Dependencies import SwiftUI +struct ActiveUserCount { + let sixMonths: Int + let month: Int + let week: Int + let day: Int +} + struct CommunityModel { @Dependency(\.apiClient) private var apiClient @Dependency(\.errorHandler) var errorHandler @@ -20,13 +27,6 @@ struct CommunityModel { case noData } - struct ActiveUserCount { - let sixMonths: Int - let month: Int - let week: Int - let day: Int - } - @available(*, deprecated, message: "Use attributes of the CommunityModel directly instead.") var community: APICommunity! diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index 10ca52888..5afb77241 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -17,10 +17,30 @@ struct InstanceModel { var url: URL! var version: SiteVersion? + // From APISiteView + var userCount: Int? + var communityCount: Int? + var postCount: Int? + var commentCount: Int? + var activeUserCount: ActiveUserCount? + + // From APILocalSite (only accessible via SiteResponse) + var federates: Bool? + var allowsDownvotes: Bool? + var allowsNSFW: Bool? + var allowsCommunityCreation: Bool? + var requiresEmailVerification: Bool? + var slurFilterRegex: Regex? + var captchaDifficulty: APICaptchaDifficulty? + init(from response: SiteResponse) { self.update(with: response) } + init(from siteView: APISiteView) { + self.update(with: siteView) + } + init(from site: APISite) { self.update(with: site) } @@ -32,7 +52,39 @@ struct InstanceModel { return user } self.version = SiteVersion(response.version) - self.update(with: response.siteView.site) + + let localSite = response.siteView.localSite + self.allowsDownvotes = localSite.enableDownvotes + self.allowsNSFW = localSite.enableNsfw + self.allowsCommunityCreation = !localSite.communityCreationAdminOnly + self.requiresEmailVerification = localSite.requireEmailVerification + self.captchaDifficulty = localSite.captchaEnabled ? localSite.captchaDifficulty : nil + self.federates = localSite.federationEnabled + do { + if let regex = localSite.slurFilterRegex { + self.slurFilterRegex = try .init(regex) + } + } catch { + print("Invalid slur filter regex") + } + + self.update(with: response.siteView) + } + + mutating func update(with siteView: APISiteView) { + userCount = siteView.counts.users + communityCount = siteView.counts.communities + postCount = siteView.counts.posts + commentCount = siteView.counts.comments + + self.activeUserCount = .init( + sixMonths: siteView.counts.usersActiveHalfYear, + month: siteView.counts.usersActiveMonth, + week: siteView.counts.usersActiveWeek, + day: siteView.counts.usersActiveDay + ) + + self.update(with: siteView.site) } mutating func update(with site: APISite) { diff --git a/Mlem/Notifications/NotificationDisplayer.swift b/Mlem/Notifications/NotificationDisplayer.swift index 64595b604..46ce63f4e 100644 --- a/Mlem/Notifications/NotificationDisplayer.swift +++ b/Mlem/Notifications/NotificationDisplayer.swift @@ -31,19 +31,6 @@ enum NotificationDisplayer { } } - /// A method to present the user with the token refresh view - /// - Parameters: - /// - account: The current `SavedAccount` for the active session - /// - refreshedAccount: A closure which will receive the updated version of the account with a refreshed access token - static func presentTokenRefreshFlow( - for account: SavedAccount, - refreshedAccount: @escaping (SavedAccount) -> Void - ) { - let tokenRefreshView = TokenRefreshView(account: account, refreshedAccount: refreshedAccount) - let view = UIHostingController(rootView: tokenRefreshView) - present(view) - } - // MARK: - Private methods private static func display(contextualError: ContextualError) async { diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceStatsView.swift new file mode 100644 index 000000000..d7c0fc5a2 --- /dev/null +++ b/Mlem/Views/Shared/Instance/InstanceStatsView.swift @@ -0,0 +1,191 @@ +// +// InstanceStatsView.swift +// Mlem +// +// Created by Sjmarf on 20/01/2024. +// + +import SwiftUI + +struct InstanceStatsView: View { + let instance: InstanceModel + + var body: some View { + VStack(spacing: 16) { + HStack(spacing: 16) { + box { + HStack { + Text("Users") + .foregroundStyle(.secondary) + } + HStack { + Text("\(abbreviateNumber(instance.userCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + } + } + + box { + HStack { + Text("Communities") + .foregroundStyle(.secondary) + } + HStack { + Text("\(abbreviateNumber(instance.communityCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.green) + } + } + } + .frame(maxWidth: .infinity) + + HStack(spacing: 16) { + box { + HStack { + Text("Posts") + .foregroundStyle(.secondary) + } + HStack { + Text("\(abbreviateNumber(instance.postCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.pink) + } + } + + box { + HStack { + Text("Comments") + .foregroundStyle(.secondary) + } + HStack { + Text("\(abbreviateNumber(instance.commentCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.orange) + } + } + } + .frame(maxWidth: .infinity) + + if let activeUserCount = instance.activeUserCount { + box(spacing: 8) { + Text("Active Users") + .foregroundStyle(.secondary) + HStack(spacing: 16) { + activeUserBox("6mo", value: activeUserCount.sixMonths) + activeUserBox("1mo", value: activeUserCount.month) + activeUserBox("1w", value: activeUserCount.week) + activeUserBox("1d", value: activeUserCount.day) + } + } + } + VStack(alignment: .leading, spacing: 0) { + settingRow( + "Federates", + systemImage: Icons.federation, + value: instance.federates ?? false + ) + Divider() + settingRow( + "Allows NSFW", + systemImage: Icons.blurNsfw, + value: instance.allowsNSFW ?? false + ) + Divider() + settingRow( + "Has Downvotes", + systemImage: Icons.downvote, + value: instance.allowsDownvotes ?? false + ) + Divider() + settingRow( + "Allows Community Creation", + systemImage: Icons.community, + value: instance.allowsCommunityCreation ?? false + ) + Divider() + settingRow( + "Has a Slur Filter", + systemImage: Icons.filterFill, + value: instance.slurFilterRegex != nil + ) + Divider() + settingRow( + "Requires Email Verification", + systemImage: Icons.email, + value: instance.requiresEmailVerification ?? false + ) + Divider() + + settingRow( + "Requires Captcha", + systemImage: Icons.photo, + value: captchaLabel, + color: instance.captchaDifficulty == nil ? .red : .green + ) + } + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) + } + .padding(.horizontal, 16) + } + + var captchaLabel: String { + if let diff = instance.captchaDifficulty { + return "Yes (\(diff.rawValue.capitalized))" + } + return "No" + } + + @ViewBuilder func box(spacing: CGFloat = 5, @ViewBuilder content: () -> some View) -> some View { + VStack(spacing: spacing) { + content() + } + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) + } + + @ViewBuilder func settingRow(_ label: String, systemImage: String, value: String, color: Color) -> some View { + HStack { + Image(systemName: systemImage) + .foregroundStyle(.secondary) + .frame(width: 30) + Text(label) + Spacer() + Text(value) + .foregroundStyle(color) + } + .padding(12) + } + + @ViewBuilder func settingRow(_ label: String, systemImage: String, value: Bool) -> some View { + HStack { + Image(systemName: systemImage) + .foregroundStyle(.secondary) + .frame(width: 30) + Text(label) + Spacer() + Text(value ? "Yes" : "No") + .foregroundStyle(value ? .green : .red) + } + .padding(12) + } + + @ViewBuilder + func activeUserBox(_ label: String, value: Int) -> some View { + VStack { + Text(abbreviateNumber(value)) + .font(.title3) + .fontWeight(.semibold) + Text(label) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + + } +} diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 5380644c6..6717c9583 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -96,7 +96,7 @@ struct InstanceView: View { VStack(spacing: 0) { VStack(spacing: 4) { Divider() - BubblePicker([.about, .administrators], selected: $selectedTab) { tab in + BubblePicker([.about, .administrators, .statistics], selected: $selectedTab) { tab in Text(tab.label) } Divider() @@ -119,6 +119,15 @@ struct InstanceView: View { } } else { ProgressView() + .padding(.top) + } + case .statistics: + if instance.userCount != nil { + InstanceStatsView(instance: instance) + .padding(.top, 16) + } else { + ProgressView() + .padding(.top) } default: EmptyView() @@ -162,11 +171,11 @@ struct InstanceView: View { } catch APIClientError.decoding(let data, let error) { withAnimation(.easeOut(duration: 0.2)) { if let content = String(data: data, encoding: .utf8), - content.contains("Error 404 - \(domainName)" ) { + content.contains("
" ) { errorDetails = ErrorDetails( title: "KBin Instance", body: "We can't yet display KBin details.", - icon: "point.3.filled.connected.trianglepath.dotted" + icon: Icons.federation ) } else { errorDetails = ErrorDetails(error: APIClientError.decoding(data, error)) diff --git a/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift b/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift index d52def8b2..9be10cac3 100644 --- a/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift +++ b/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift @@ -12,7 +12,7 @@ struct CommunityStatsView: View { var body: some View { VStack(spacing: 16) { - VStack(spacing: 5) { + box { Text("Subscribers") .foregroundStyle(.secondary) Text("\(community.subscriberCount ?? 0)") @@ -20,13 +20,9 @@ struct CommunityStatsView: View { .font(.title) } - .padding(.vertical) - .frame(maxWidth: .infinity) - .background(Color(uiColor: .secondarySystemGroupedBackground)) - .cornerRadius(AppConstants.largeItemCornerRadius) HStack(spacing: 16) { - VStack(spacing: 5) { + box { HStack { Text("Posts") .foregroundStyle(.secondary) @@ -38,12 +34,8 @@ struct CommunityStatsView: View { .foregroundStyle(.pink) } } - .padding(10) - .frame(maxWidth: .infinity) - .background(Color(uiColor: .secondarySystemGroupedBackground)) - .cornerRadius(AppConstants.largeItemCornerRadius) - VStack(spacing: 5) { + box { HStack { Text("Comments") .foregroundStyle(.secondary) @@ -55,15 +47,11 @@ struct CommunityStatsView: View { .foregroundStyle(.orange) } } - .padding(10) - .frame(maxWidth: .infinity) - .background(Color(uiColor: .secondarySystemGroupedBackground)) - .cornerRadius(AppConstants.largeItemCornerRadius) } .frame(maxWidth: .infinity) if let activeUserCount = community.activeUserCount { - VStack(spacing: 8) { + box(spacing: 8) { Text("Active Users") .foregroundStyle(.secondary) HStack(spacing: 16) { @@ -73,15 +61,21 @@ struct CommunityStatsView: View { activeUserBox("1d", value: activeUserCount.day) } } - .padding(.vertical, 10) - .frame(maxWidth: .infinity) - .background(Color(uiColor: .secondarySystemGroupedBackground)) - .cornerRadius(AppConstants.largeItemCornerRadius) } } .padding(.horizontal, 16) } + @ViewBuilder func box(spacing: CGFloat = 5, @ViewBuilder content: () -> some View) -> some View { + VStack(spacing: spacing) { + content() + } + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) + } + @ViewBuilder func activeUserBox(_ label: String, value: Int) -> some View { VStack { From 30186f75c57aeb0566daf4db0d5f757b1c41fd86 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 23:31:47 +0000 Subject: [PATCH 15/40] Update project.pbxproj --- Mlem.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index b454abc71..5366c531c 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 031A93D62AC847DA0077030C /* UploadConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A93D52AC847DA0077030C /* UploadConfirmationView.swift */; }; 031BF9532AB24BAF00F4517F /* SiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031BF9522AB24BAF00F4517F /* SiteVersion.swift */; }; 031BF9552AB25AFB00F4517F /* SiteVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031BF9542AB25AFB00F4517F /* SiteVersionTests.swift */; }; + 031F95572B5C7FF20069C244 /* InstanceStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F95562B5C7FF20069C244 /* InstanceStatsView.swift */; }; 032109472AA7C3FC00912DFC /* CommunityLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032109462AA7C3FC00912DFC /* CommunityLabelView.swift */; }; 032109492AA7C41800912DFC /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032109482AA7C41800912DFC /* AvatarView.swift */; }; 032DD2FD2AC3594B00F1B33D /* LinkAttatchmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DD2FC2AC3594B00F1B33D /* LinkAttatchmentView.swift */; }; @@ -581,6 +582,7 @@ 031A93D52AC847DA0077030C /* UploadConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadConfirmationView.swift; sourceTree = ""; }; 031BF9522AB24BAF00F4517F /* SiteVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVersion.swift; sourceTree = ""; }; 031BF9542AB25AFB00F4517F /* SiteVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVersionTests.swift; sourceTree = ""; }; + 031F95562B5C7FF20069C244 /* InstanceStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceStatsView.swift; sourceTree = ""; }; 032109462AA7C3FC00912DFC /* CommunityLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLabelView.swift; sourceTree = ""; }; 032109482AA7C41800912DFC /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; 032DD2FC2AC3594B00F1B33D /* LinkAttatchmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAttatchmentView.swift; sourceTree = ""; }; @@ -1323,6 +1325,7 @@ isa = PBXGroup; children = ( 03A54C312B5331F30064CCDE /* InstanceView.swift */, + 031F95562B5C7FF20069C244 /* InstanceStatsView.swift */, ); path = Instance; sourceTree = ""; @@ -3317,6 +3320,7 @@ CDC1C93C2A7AA76000072E3D /* InternetSpeed.swift in Sources */, 50EC39B22A346DDC00E014C2 /* URLHandler.swift in Sources */, 63F0C7BF2A058EDE00A18C5D /* Get Correct URL to Endpoint.swift in Sources */, + 031F95572B5C7FF20069C244 /* InstanceStatsView.swift in Sources */, 632E8EE827EE63DB007E8D75 /* DownvoteButtonView.swift in Sources */, 50D61E5B2AA32B9400A926EC /* APISession.swift in Sources */, CDCBD72B2A8EC0A800387A2C /* Instance Summary.swift in Sources */, From c2f59e01b8aedb6594c5a1aa70cdc23852e9b233 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 23:44:50 +0000 Subject: [PATCH 16/40] Fix again --- Mlem/API/APIClient/APIClient.swift | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index f5f140b43..646494121 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -153,26 +153,26 @@ class APIClient { } } - private func urlRequest(from defintion: any APIRequest, overrideToken: String?) throws -> URLRequest { - var urlRequest = URLRequest(url: defintion.endpoint) - defintion.headers.forEach { header in - if header.key == "Authorization" { - if let overrideToken { - urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") - } else if case let .authenticated(_, token) = session { - urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - } - } else { - urlRequest.setValue(header.value, forHTTPHeaderField: header.key) + private func urlRequest(from definition: any APIRequest, overrideToken: String?) throws -> URLRequest { + var urlRequest = URLRequest(url: definition.endpoint) + definition.headers.forEach { header in + urlRequest.setValue(header.value, forHTTPHeaderField: header.key) + } + + if !definition.headers.keys.contains("Authorization") { + if let overrideToken { + urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") + } else if case let .authenticated(_, token) = session, try session.instanceUrl == definition.instanceURL { + urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } } - if defintion as? any APIGetRequest != nil { + if definition as? any APIGetRequest != nil { urlRequest.httpMethod = "GET" - } else if let postDefinition = defintion as? any APIPostRequest { + } else if let postDefinition = definition as? any APIPostRequest { urlRequest.httpMethod = "POST" urlRequest.httpBody = try createBodyData(for: postDefinition) - } else if let putDefinition = defintion as? any APIPutRequest { + } else if let putDefinition = definition as? any APIPutRequest { urlRequest.httpMethod = "PUT" urlRequest.httpBody = try createBodyData(for: putDefinition) } From 401add5304950d0d1318294045dbf7c54a97bf44 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 20 Jan 2024 23:47:34 +0000 Subject: [PATCH 17/40] Remove unnecessary code --- Mlem/API/APIClient/APIClient.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index 646494121..d9144a035 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -158,13 +158,11 @@ class APIClient { definition.headers.forEach { header in urlRequest.setValue(header.value, forHTTPHeaderField: header.key) } - - if !definition.headers.keys.contains("Authorization") { - if let overrideToken { - urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") - } else if case let .authenticated(_, token) = session, try session.instanceUrl == definition.instanceURL { - urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - } + + if let overrideToken { + urlRequest.setValue("Bearer \(overrideToken)", forHTTPHeaderField: "Authorization") + } else if case let .authenticated(_, token) = session, try session.instanceUrl == definition.instanceURL { + urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } if definition as? any APIGetRequest != nil { From 0840ead09b706e866004ad4b68254a350f071dbc Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 21 Jan 2024 09:56:22 +0000 Subject: [PATCH 18/40] Update --- Mlem/API/Models/Site/APILocalSite.swift | 31 ++++++-- Mlem/Icons.swift | 2 + .../Content/Instance/InstanceModel.swift | 7 ++ .../Shared/Instance/InstanceStatsView.swift | 78 ++++++++++++++----- 4 files changed, 93 insertions(+), 25 deletions(-) diff --git a/Mlem/API/Models/Site/APILocalSite.swift b/Mlem/API/Models/Site/APILocalSite.swift index eae6abacc..46975db4c 100644 --- a/Mlem/API/Models/Site/APILocalSite.swift +++ b/Mlem/API/Models/Site/APILocalSite.swift @@ -5,7 +5,7 @@ // Created by Jonathan de Jong on 12/06/2023. // -import Foundation +import SwiftUI enum APICaptchaDifficulty: String, Codable { case easy, medium, hard } @@ -19,7 +19,7 @@ struct APILocalSite: Decodable { let communityCreationAdminOnly: Bool let requireEmailVerification: Bool // let applicationQuestion: String? -// let privateInstance: Bool + let privateInstance: Bool // let defaultTheme: String // let defaultPostListingType: String // let legalInformation: String? @@ -28,13 +28,12 @@ struct APILocalSite: Decodable { let slurFilterRegex: String? // let actorNameMaxLength: Int let federationEnabled: Bool -// let federationDebug: Bool -// let federationWorkerCount: Int + let federationSignedFetch: Bool let captchaEnabled: Bool let captchaDifficulty: APICaptchaDifficulty -// let registrationMode: APIRegistrationMode + let registrationMode: APIRegistrationMode // let reportsEmailAdmins: Bool -// let published: Date + let published: Date // let updated: Date? } @@ -43,4 +42,24 @@ enum APIRegistrationMode: String, Codable { case closed = "Closed" case requireApplication = "RequireApplication" case open = "Open" + + var label: String { + switch self { + case .requireApplication: + return "Requires Application" + default: + return rawValue + } + } + + var color: Color { + switch self { + case .closed: + return .red + case .requireApplication: + return .yellow + case .open: + return .green + } + } } diff --git a/Mlem/Icons.swift b/Mlem/Icons.swift index d494c2838..89b6ac7f3 100644 --- a/Mlem/Icons.swift +++ b/Mlem/Icons.swift @@ -144,6 +144,7 @@ struct Icons { static let favoriteFill: String = "star.fill" static let unfavorite: String = "star.slash" static let unfavoriteFill: String = "star.slash.fill" + static let person: String = "person" static let personFill: String = "person.fill" static let close: String = "multiply" static let cakeDay: String = "birthday.cake" @@ -190,6 +191,7 @@ struct Icons { static let swipeUpGestureSetting: String = "arrow.up.to.line.alt" // misc + static let `private`: String = "lock" static let email: String = "envelope" static let photo: String = "photo" static let switchUser: String = "person.crop.circle.badge.plus" diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index 5afb77241..b2a015bd4 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -25,13 +25,16 @@ struct InstanceModel { var activeUserCount: ActiveUserCount? // From APILocalSite (only accessible via SiteResponse) + var `private`: Bool? var federates: Bool? + var federationSignedFetch: Bool? var allowsDownvotes: Bool? var allowsNSFW: Bool? var allowsCommunityCreation: Bool? var requiresEmailVerification: Bool? var slurFilterRegex: Regex? var captchaDifficulty: APICaptchaDifficulty? + var registrationMode: APIRegistrationMode? init(from response: SiteResponse) { self.update(with: response) @@ -59,7 +62,11 @@ struct InstanceModel { self.allowsCommunityCreation = !localSite.communityCreationAdminOnly self.requiresEmailVerification = localSite.requireEmailVerification self.captchaDifficulty = localSite.captchaEnabled ? localSite.captchaDifficulty : nil + self.private = localSite.privateInstance self.federates = localSite.federationEnabled + self.federationSignedFetch = localSite.federationSignedFetch + + self.registrationMode = localSite.registrationMode do { if let regex = localSite.slurFilterRegex { self.slurFilterRegex = try .init(regex) diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceStatsView.swift index d7c0fc5a2..32f3123b1 100644 --- a/Mlem/Views/Shared/Instance/InstanceStatsView.swift +++ b/Mlem/Views/Shared/Instance/InstanceStatsView.swift @@ -8,6 +8,8 @@ import SwiftUI struct InstanceStatsView: View { + @AppStorage("developerMode") var developerMode: Bool = false + let instance: InstanceModel var body: some View { @@ -82,53 +84,86 @@ struct InstanceStatsView: View { } } VStack(alignment: .leading, spacing: 0) { + settingRow( + "Private", + systemImage: Icons.private, + value: instance.private ?? false + ) + Divider() settingRow( "Federates", systemImage: Icons.federation, value: instance.federates ?? false ) - Divider() + if developerMode, let signedFetch = instance.federationSignedFetch { + Divider() + settingRow( + "Federation Signed Fetch", + systemImage: Icons.developerMode, + value: signedFetch + ) + } + } + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) + + VStack(alignment: .leading, spacing: 0) { settingRow( - "Allows NSFW", + "NSFW Content", systemImage: Icons.blurNsfw, value: instance.allowsNSFW ?? false ) Divider() settingRow( - "Has Downvotes", + "Downvotes", systemImage: Icons.downvote, value: instance.allowsDownvotes ?? false ) Divider() settingRow( - "Allows Community Creation", - systemImage: Icons.community, + "Community Creation", + systemImage: "house", value: instance.allowsCommunityCreation ?? false ) Divider() settingRow( - "Has a Slur Filter", + "Slur Filter", systemImage: Icons.filterFill, value: instance.slurFilterRegex != nil ) - Divider() - settingRow( - "Requires Email Verification", - systemImage: Icons.email, - value: instance.requiresEmailVerification ?? false - ) - Divider() - + } + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) + + VStack(alignment: .leading, spacing: 0) { settingRow( - "Requires Captcha", - systemImage: Icons.photo, - value: captchaLabel, - color: instance.captchaDifficulty == nil ? .red : .green + "Registration", + systemImage: Icons.person, + value: instance.registrationMode?.label ?? "Closed", + color: instance.registrationMode?.color ?? .red ) + if instance.registrationMode != .closed { + Divider() + settingRow( + "Email Verification", + systemImage: Icons.email, + value: instance.requiresEmailVerification ?? false + ) + Divider() + settingRow( + "Captcha", + systemImage: Icons.photo, + value: captchaLabel, + color: instance.captchaDifficulty == nil ? .red : .green + ) + } } .frame(maxWidth: .infinity) .background(Color(uiColor: .secondarySystemGroupedBackground)) .cornerRadius(AppConstants.largeItemCornerRadius) + } .padding(.horizontal, 16) } @@ -150,7 +185,12 @@ struct InstanceStatsView: View { .cornerRadius(AppConstants.largeItemCornerRadius) } - @ViewBuilder func settingRow(_ label: String, systemImage: String, value: String, color: Color) -> some View { + @ViewBuilder func settingRow( + _ label: String, + systemImage: String, + value: String, + color: Color = .primary + ) -> some View { HStack { Image(systemName: systemImage) .foregroundStyle(.secondary) From 563e340b6ac14e043779b26aa6529ecfb7d92aa0 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:16:21 +0000 Subject: [PATCH 19/40] Update --- .../Community/APICommunityAggregates.swift | 1 + Mlem/API/Models/Site/APILocalSite.swift | 29 ++-- Mlem/Extensions/Date/Date+Formatter.swift | 25 +++ Mlem/Extensions/Date/Date+RelativeTime.swift | 18 --- .../Mocks/APICommunityAggregates+Mock.swift | 2 + .../Content/Community/CommunityModel.swift | 2 + .../Content/Instance/InstanceModel.swift | 12 ++ Mlem/Models/UserFlair.swift | 17 ++ .../Shared/Instance/InstanceStatsView.swift | 153 +++++++++++------- .../Views/Tabs/Feeds/CommunityStatsView.swift | 43 +++-- Mlem/Views/Tabs/Profile/UserView+Logic.swift | 6 - Mlem/Views/Tabs/Profile/UserView.swift | 43 ++--- .../Views/Advanced/AdvancedSettingsView.swift | 2 + 13 files changed, 209 insertions(+), 144 deletions(-) create mode 100644 Mlem/Extensions/Date/Date+Formatter.swift delete mode 100644 Mlem/Extensions/Date/Date+RelativeTime.swift diff --git a/Mlem/API/Models/Community/APICommunityAggregates.swift b/Mlem/API/Models/Community/APICommunityAggregates.swift index a529a61c2..2c2f4c5a0 100644 --- a/Mlem/API/Models/Community/APICommunityAggregates.swift +++ b/Mlem/API/Models/Community/APICommunityAggregates.swift @@ -12,6 +12,7 @@ struct APICommunityAggregates: Decodable { let id: Int? // TODO: 0.18 Deprecation remove this field let communityId: Int let subscribers: Int + let subscribersLocal: Int? let posts: Int let comments: Int let published: Date diff --git a/Mlem/API/Models/Site/APILocalSite.swift b/Mlem/API/Models/Site/APILocalSite.swift index 46975db4c..e40cd9659 100644 --- a/Mlem/API/Models/Site/APILocalSite.swift +++ b/Mlem/API/Models/Site/APILocalSite.swift @@ -21,34 +21,45 @@ struct APILocalSite: Decodable { // let applicationQuestion: String? let privateInstance: Bool // let defaultTheme: String -// let defaultPostListingType: String + let defaultPostListingType: FeedType // let legalInformation: String? -// let hideModlogModNames: Bool -// let applicationEmailAdmins: Bool + let hideModlogModNames: Bool + let applicationEmailAdmins: Bool let slurFilterRegex: String? // let actorNameMaxLength: Int let federationEnabled: Bool - let federationSignedFetch: Bool + let federationSignedFetch: Bool? let captchaEnabled: Bool let captchaDifficulty: APICaptchaDifficulty let registrationMode: APIRegistrationMode -// let reportsEmailAdmins: Bool + let reportsEmailAdmins: Bool let published: Date // let updated: Date? } // lemmy_db_schema::source::local_site::RegistrationMode enum APIRegistrationMode: String, Codable { - case closed = "Closed" - case requireApplication = "RequireApplication" - case open = "Open" + case closed = "closed" + case requireApplication = "requireapplication" + case open = "open" + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let stringValue = try? container.decode(String.self) { + if let item = APIRegistrationMode(rawValue: stringValue.lowercased()) { + self = item + return + } + } + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid APIRegistrationMode value") + } var label: String { switch self { case .requireApplication: return "Requires Application" default: - return rawValue + return rawValue.capitalized } } diff --git a/Mlem/Extensions/Date/Date+Formatter.swift b/Mlem/Extensions/Date/Date+Formatter.swift new file mode 100644 index 000000000..2aa5784a3 --- /dev/null +++ b/Mlem/Extensions/Date/Date+Formatter.swift @@ -0,0 +1,25 @@ +// +// Date+RelativeTime.swift +// Mlem +// +// Created by Jake Shirley on 6/22/23. +// + +import SwiftUI + +extension Date { + // Returns strings like "3 seconds ago" and "10 days ago" + func getRelativeTime(date: Date = .now, unitsStyle: RelativeDateTimeFormatter.UnitsStyle = .full) -> String { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = unitsStyle + + return formatter.localizedString(for: self, relativeTo: date) + } + + // Returns strings like "5/10/2023" + var dateString: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "ddMMYY", options: 0, locale: Locale.current) + return dateFormatter.string(from: self) + } +} diff --git a/Mlem/Extensions/Date/Date+RelativeTime.swift b/Mlem/Extensions/Date/Date+RelativeTime.swift deleted file mode 100644 index 3561d0765..000000000 --- a/Mlem/Extensions/Date/Date+RelativeTime.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Date+RelativeTime.swift -// Mlem -// -// Created by Jake Shirley on 6/22/23. -// - -import SwiftUI - -extension Date { - // Returns strings like "3 seconds ago" and "10 days ago" - func getRelativeTime(date: Date, unitsStyle: RelativeDateTimeFormatter.UnitsStyle = .full) -> String { - let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = unitsStyle - - return formatter.localizedString(for: self, relativeTo: date) - } -} diff --git a/Mlem/Extensions/Mocks/APICommunityAggregates+Mock.swift b/Mlem/Extensions/Mocks/APICommunityAggregates+Mock.swift index 7be6d9f64..70e1ac209 100644 --- a/Mlem/Extensions/Mocks/APICommunityAggregates+Mock.swift +++ b/Mlem/Extensions/Mocks/APICommunityAggregates+Mock.swift @@ -13,6 +13,7 @@ extension APICommunityAggregates { id: Int = 0, communityId: Int = 0, subscribers: Int = 42349, + subscribersLocal: Int = 2043, posts: Int = 300, comments: Int = 5000, published: Date = .mock, @@ -25,6 +26,7 @@ extension APICommunityAggregates { id: id, communityId: communityId, subscribers: subscribers, + subscribersLocal: subscribersLocal, posts: posts, comments: comments, published: published, diff --git a/Mlem/Models/Content/Community/CommunityModel.swift b/Mlem/Models/Content/Community/CommunityModel.swift index 2a5518083..659d2c882 100644 --- a/Mlem/Models/Content/Community/CommunityModel.swift +++ b/Mlem/Models/Content/Community/CommunityModel.swift @@ -63,6 +63,7 @@ struct CommunityModel { var blocked: Bool? var subscribed: Bool? var subscriberCount: Int? + var localSubscriberCount: Int? var postCount: Int? var commentCount: Int? var activeUserCount: ActiveUserCount? @@ -110,6 +111,7 @@ struct CommunityModel { self.blocked = communityView.blocked self.subscriberCount = communityView.counts.subscribers + self.localSubscriberCount = communityView.counts.subscribersLocal self.postCount = communityView.counts.posts self.commentCount = communityView.counts.comments self.activeUserCount = .init( diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index b2a015bd4..38782a5c0 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -16,6 +16,7 @@ struct InstanceModel { var administrators: [UserModel]? var url: URL! var version: SiteVersion? + var creationDate: Date! // From APISiteView var userCount: Int? @@ -33,8 +34,13 @@ struct InstanceModel { var allowsCommunityCreation: Bool? var requiresEmailVerification: Bool? var slurFilterRegex: Regex? + var slurFilterString: String? var captchaDifficulty: APICaptchaDifficulty? var registrationMode: APIRegistrationMode? + var defaultFeedType: FeedType? + var hideModlogModNames: Bool? + var applicationsEmailAdmins: Bool? + var reportsEmailAdmins: Bool? init(from response: SiteResponse) { self.update(with: response) @@ -65,10 +71,15 @@ struct InstanceModel { self.private = localSite.privateInstance self.federates = localSite.federationEnabled self.federationSignedFetch = localSite.federationSignedFetch + self.defaultFeedType = localSite.defaultPostListingType + self.hideModlogModNames = localSite.hideModlogModNames + self.applicationsEmailAdmins = localSite.applicationEmailAdmins + self.reportsEmailAdmins = localSite.reportsEmailAdmins self.registrationMode = localSite.registrationMode do { if let regex = localSite.slurFilterRegex { + self.slurFilterString = regex self.slurFilterRegex = try .init(regex) } } catch { @@ -100,6 +111,7 @@ struct InstanceModel { description = site.sidebar avatar = site.iconUrl banner = site.bannerUrl + creationDate = site.published if var components = URLComponents(string: site.inboxUrl) { components.path = "" diff --git a/Mlem/Models/UserFlair.swift b/Mlem/Models/UserFlair.swift index fc9a921e1..b000df65e 100644 --- a/Mlem/Models/UserFlair.swift +++ b/Mlem/Models/UserFlair.swift @@ -48,4 +48,21 @@ enum UserFlair { return Icons.developerFlair } } + + var label: String { + switch self { + case .admin: + return "Administrator" + case .bot: + return "Bot Account" + case .banned: + return "Banned" + case .moderator: + return "Moderator" + case .developer: + return "Mlem Developer" + case .op: + return "Original Poster" + } + } } diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceStatsView.swift index 32f3123b1..e22bfa35d 100644 --- a/Mlem/Views/Shared/Instance/InstanceStatsView.swift +++ b/Mlem/Views/Shared/Instance/InstanceStatsView.swift @@ -10,63 +10,58 @@ import SwiftUI struct InstanceStatsView: View { @AppStorage("developerMode") var developerMode: Bool = false + @State var showingSlurRegex: Bool = false + let instance: InstanceModel var body: some View { VStack(spacing: 16) { + box { + HStack { + Label(instance.creationDate.dateString, systemImage: Icons.cakeDay) + Text("•") + Label(instance.creationDate.getRelativeTime(unitsStyle: .abbreviated), systemImage: Icons.time) + } + .foregroundStyle(.secondary) + .font(.footnote) + } HStack(spacing: 16) { box { - HStack { - Text("Users") - .foregroundStyle(.secondary) - } - HStack { - Text("\(abbreviateNumber(instance.userCount ?? 0))") - .font(.title) - .fontWeight(.semibold) - } + Text("Users") + .foregroundStyle(.secondary) + Text("\(abbreviateNumber(instance.userCount ?? 0))") + .font(.title) + .fontWeight(.semibold) } box { - HStack { - Text("Communities") - .foregroundStyle(.secondary) - } - HStack { - Text("\(abbreviateNumber(instance.communityCount ?? 0))") - .font(.title) - .fontWeight(.semibold) - .foregroundStyle(.green) - } + Text("Communities") + .foregroundStyle(.secondary) + Text("\(abbreviateNumber(instance.communityCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.green) } } .frame(maxWidth: .infinity) HStack(spacing: 16) { box { - HStack { - Text("Posts") - .foregroundStyle(.secondary) - } - HStack { - Text("\(abbreviateNumber(instance.postCount ?? 0))") - .font(.title) - .fontWeight(.semibold) - .foregroundStyle(.pink) - } + Text("Posts") + .foregroundStyle(.secondary) + Text("\(abbreviateNumber(instance.postCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.pink) } box { - HStack { - Text("Comments") - .foregroundStyle(.secondary) - } - HStack { - Text("\(abbreviateNumber(instance.commentCount ?? 0))") - .font(.title) - .fontWeight(.semibold) - .foregroundStyle(.orange) - } + Text("Comments") + .foregroundStyle(.secondary) + Text("\(abbreviateNumber(instance.commentCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.orange) } } .frame(maxWidth: .infinity) @@ -108,6 +103,33 @@ struct InstanceStatsView: View { .background(Color(uiColor: .secondarySystemGroupedBackground)) .cornerRadius(AppConstants.largeItemCornerRadius) + VStack(alignment: .leading, spacing: 0) { + settingRow( + "Registration", + systemImage: Icons.person, + value: instance.registrationMode?.label ?? "Closed", + color: instance.registrationMode?.color ?? .red + ) + if instance.registrationMode != .closed { + Divider() + settingRow( + "Email Verification", + systemImage: Icons.email, + value: instance.requiresEmailVerification ?? false + ) + Divider() + settingRow( + "Captcha", + systemImage: Icons.photo, + value: captchaLabel, + color: instance.captchaDifficulty == nil ? .red : .green + ) + } + } + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) + VStack(alignment: .leading, spacing: 0) { settingRow( "NSFW Content", @@ -132,37 +154,54 @@ struct InstanceStatsView: View { systemImage: Icons.filterFill, value: instance.slurFilterRegex != nil ) + if developerMode, let regex = instance.slurFilterString { + Divider() + Text(showingSlurRegex ? regex : "Tap to show slur filter regex") + .textSelection(.enabled) + .font(.footnote) + .foregroundStyle(.secondary) + .padding(12) + .onTapGesture { + showingSlurRegex.toggle() + } + } + if developerMode, let feedType = instance.defaultFeedType { + Divider() + settingRow( + "Default Feed Type", + systemImage: Icons.feeds, + value: feedType.label + ) + } } .frame(maxWidth: .infinity) .background(Color(uiColor: .secondarySystemGroupedBackground)) .cornerRadius(AppConstants.largeItemCornerRadius) - VStack(alignment: .leading, spacing: 0) { - settingRow( - "Registration", - systemImage: Icons.person, - value: instance.registrationMode?.label ?? "Closed", - color: instance.registrationMode?.color ?? .red - ) - if instance.registrationMode != .closed { + if developerMode { + VStack(alignment: .leading, spacing: 0) { + settingRow( + "Show Mod Names in Modlog", + systemImage: Icons.moderation, + value: !(instance.hideModlogModNames ?? true) + ) Divider() settingRow( - "Email Verification", - systemImage: Icons.email, - value: instance.requiresEmailVerification ?? false + "Applications Email Admins", + systemImage: Icons.person, + value: instance.applicationsEmailAdmins ?? false ) Divider() settingRow( - "Captcha", - systemImage: Icons.photo, - value: captchaLabel, - color: instance.captchaDifficulty == nil ? .red : .green + "Reports Email Admins", + systemImage: Icons.moderationReport, + value: instance.reportsEmailAdmins ?? false ) } + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) } - .frame(maxWidth: .infinity) - .background(Color(uiColor: .secondarySystemGroupedBackground)) - .cornerRadius(AppConstants.largeItemCornerRadius) } .padding(.horizontal, 16) diff --git a/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift b/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift index 9be10cac3..bead4120e 100644 --- a/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift +++ b/Mlem/Views/Tabs/Feeds/CommunityStatsView.swift @@ -12,40 +12,39 @@ struct CommunityStatsView: View { var body: some View { VStack(spacing: 16) { + box { + HStack { + Label(community.creationDate.dateString, systemImage: Icons.cakeDay) + Text("•") + Label(community.creationDate.getRelativeTime(date: Date.now, unitsStyle: .abbreviated), systemImage: Icons.time) + } + .foregroundStyle(.secondary) + .font(.footnote) + } box { Text("Subscribers") .foregroundStyle(.secondary) Text("\(community.subscriberCount ?? 0)") .fontWeight(.semibold) .font(.title) - } HStack(spacing: 16) { - box { - HStack { - Text("Posts") - .foregroundStyle(.secondary) - } - HStack { - Text("\(abbreviateNumber(community.postCount ?? 0))") - .font(.title) - .fontWeight(.semibold) - .foregroundStyle(.pink) - } + Text("Posts") + .foregroundStyle(.secondary) + Text("\(abbreviateNumber(community.postCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.pink) } box { - HStack { - Text("Comments") - .foregroundStyle(.secondary) - } - HStack { - Text("\(abbreviateNumber(community.commentCount ?? 0))") - .font(.title) - .fontWeight(.semibold) - .foregroundStyle(.orange) - } + Text("Comments") + .foregroundStyle(.secondary) + Text("\(abbreviateNumber(community.commentCount ?? 0))") + .font(.title) + .fontWeight(.semibold) + .foregroundStyle(.orange) } } .frame(maxWidth: .infinity) diff --git a/Mlem/Views/Tabs/Profile/UserView+Logic.swift b/Mlem/Views/Tabs/Profile/UserView+Logic.swift index 0ef668e3d..301eb1575 100644 --- a/Mlem/Views/Tabs/Profile/UserView+Logic.swift +++ b/Mlem/Views/Tabs/Profile/UserView+Logic.swift @@ -21,12 +21,6 @@ extension UserView { return tabs } - var cakeDayFormatter: DateFormatter { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "ddMMYY", options: 0, locale: Locale.current) - return dateFormatter - } - var bioAlignment: TextAlignment { if let bio = user.bio { if bio.rangeOfCharacter(from: CharacterSet.newlines) != nil { diff --git a/Mlem/Views/Tabs/Profile/UserView.swift b/Mlem/Views/Tabs/Profile/UserView.swift index f1a1ee8f2..3a95bdebf 100644 --- a/Mlem/Views/Tabs/Profile/UserView.swift +++ b/Mlem/Views/Tabs/Profile/UserView.swift @@ -92,7 +92,7 @@ struct UserView: View { MarkdownView(text: bio, isNsfw: false, alignment: bioAlignment).padding(AppConstants.postAndCommentSpacing) } HStack { - Label(cakeDayFormatter.string(from: user.creationDate), systemImage: Icons.cakeDay) + Label(user.creationDate.dateString, systemImage: Icons.cakeDay) Text("•") Label(user.creationDate.getRelativeTime(date: Date.now, unitsStyle: .abbreviated), systemImage: Icons.time) if bioAlignment == .leading { @@ -222,49 +222,28 @@ struct UserView: View { if !flairs.isEmpty { VStack(spacing: AppConstants.postAndCommentSpacing) { ForEach(flairs, id: \.self) { flair in - switch flair { - case .developer: - flairBackground(color: flair.color) { - HStack { - Image(systemName: Icons.developerFlair) - Text("Mlem Developer") - } - } - case .banned: - flairBackground(color: flair.color) { - HStack { + flairBackground(color: flair.color) { + HStack { + switch flair { + case .banned: Image(systemName: Icons.bannedFlair) if let expirationDate = user.banExpirationDate { - Text("Banned Until \(cakeDayFormatter.string(from: expirationDate))") + Text("Banned Until \(expirationDate.dateString)") } else { Text("Permanently Banned") } - } - } - case .bot: - flairBackground(color: flair.color) { - HStack { - Image(systemName: Icons.botFlair) - Text("Bot Account") - } - } - case .admin: - flairBackground(color: flair.color) { - HStack { + case .admin: Image(systemName: Icons.adminFlair) let host = user.profileUrl.host() Text("\(host ?? "Instance") Administrator") - } - } - case .moderator: - flairBackground(color: flair.color) { - HStack { + case .moderator: Image(systemName: Icons.moderationFill) Text("\(communityContext?.displayName ?? "Community") Moderator") + default: + Image(systemName: flair.icon) + Text(flair.label) } } - default: - EmptyView() } } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift index 7f47e6ab8..5586f578a 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift @@ -21,6 +21,8 @@ struct AdvancedSettingsView: View { settingName: "Developer Mode", isTicked: $developerMode ) + } footer: { + Text("Shows additional technical information.") } Section { From 7f792fa1030133d0134f4acde45752da3804fa90 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:16:31 +0000 Subject: [PATCH 20/40] Update project.pbxproj --- Mlem.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 5366c531c..92efaf31e 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -275,7 +275,7 @@ 63F0C7B92A0533C700A18C5D /* Add Account View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0C7B82A0533C700A18C5D /* Add Account View.swift */; }; 63F0C7BD2A058CD200A18C5D /* Check if Endpoint Exists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0C7BC2A058CD200A18C5D /* Check if Endpoint Exists.swift */; }; 63F0C7BF2A058EDE00A18C5D /* Get Correct URL to Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0C7BE2A058EDE00A18C5D /* Get Correct URL to Endpoint.swift */; }; - 6D15D74C2A44DC240061B5CB /* Date+RelativeTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D15D74B2A44DC240061B5CB /* Date+RelativeTime.swift */; }; + 6D15D74C2A44DC240061B5CB /* Date+Formatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D15D74B2A44DC240061B5CB /* Date+Formatter.swift */; }; 6D405AFF2A43E66600C65F9C /* UserLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D405AFE2A43E66600C65F9C /* UserLabelView.swift */; }; 6D693A3E2A5113DF009E2D76 /* CreatePostReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D693A3D2A5113DF009E2D76 /* CreatePostReport.swift */; }; 6D693A402A51147E009E2D76 /* APIPostReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D693A3F2A51147E009E2D76 /* APIPostReportView.swift */; }; @@ -820,7 +820,7 @@ 63F0C7B82A0533C700A18C5D /* Add Account View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Add Account View.swift"; sourceTree = ""; }; 63F0C7BC2A058CD200A18C5D /* Check if Endpoint Exists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Check if Endpoint Exists.swift"; sourceTree = ""; }; 63F0C7BE2A058EDE00A18C5D /* Get Correct URL to Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Get Correct URL to Endpoint.swift"; sourceTree = ""; }; - 6D15D74B2A44DC240061B5CB /* Date+RelativeTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+RelativeTime.swift"; sourceTree = ""; }; + 6D15D74B2A44DC240061B5CB /* Date+Formatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Formatter.swift"; sourceTree = ""; }; 6D405AFE2A43E66600C65F9C /* UserLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLabelView.swift; sourceTree = ""; }; 6D693A3D2A5113DF009E2D76 /* CreatePostReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePostReport.swift; sourceTree = ""; }; 6D693A3F2A51147E009E2D76 /* APIPostReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPostReportView.swift; sourceTree = ""; }; @@ -2255,7 +2255,7 @@ CD29ED2E2B2E8307006937CE /* Date */ = { isa = PBXGroup; children = ( - 6D15D74B2A44DC240061B5CB /* Date+RelativeTime.swift */, + 6D15D74B2A44DC240061B5CB /* Date+Formatter.swift */, ); path = Date; sourceTree = ""; @@ -3480,7 +3480,7 @@ E49E01F42ABD99D300E42BB3 /* Routable.swift in Sources */, 03F4DC9F2B1A8AD500556C67 /* SignInAndSecuritySettingsView.swift in Sources */, 03F76FA62B2F5F4700E2B54A /* LinkUploadOptionsView.swift in Sources */, - 6D15D74C2A44DC240061B5CB /* Date+RelativeTime.swift in Sources */, + 6D15D74C2A44DC240061B5CB /* Date+Formatter.swift in Sources */, CDA217E62A63016A00BDA173 /* ReportMessage.swift in Sources */, CD9DD8832A622A6C0044EA8E /* ReportCommentReply.swift in Sources */, CD3FBCE12A4A836000B2063F /* AllItemsFeedView.swift in Sources */, From b97ea619edf97b78ceba837c40320c7e18d5b5a6 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 21 Jan 2024 16:14:04 +0000 Subject: [PATCH 21/40] Rename "Statistics" tab to "Details" --- Mlem/Views/Shared/Instance/InstanceView.swift | 6 +++--- Mlem/Views/Tabs/Profile/UserView.swift | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 6717c9583..f3e194b27 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -10,7 +10,7 @@ import Charts import Dependencies enum InstanceViewTab: String, Identifiable, CaseIterable { - case about, administrators, statistics, uptime, safety + case about, administrators, details, uptime, safety var id: Self { self } @@ -96,7 +96,7 @@ struct InstanceView: View { VStack(spacing: 0) { VStack(spacing: 4) { Divider() - BubblePicker([.about, .administrators, .statistics], selected: $selectedTab) { tab in + BubblePicker([.about, .administrators, .details], selected: $selectedTab) { tab in Text(tab.label) } Divider() @@ -121,7 +121,7 @@ struct InstanceView: View { ProgressView() .padding(.top) } - case .statistics: + case .details: if instance.userCount != nil { InstanceStatsView(instance: instance) .padding(.top, 16) diff --git a/Mlem/Views/Tabs/Profile/UserView.swift b/Mlem/Views/Tabs/Profile/UserView.swift index 3a95bdebf..2eb0792be 100644 --- a/Mlem/Views/Tabs/Profile/UserView.swift +++ b/Mlem/Views/Tabs/Profile/UserView.swift @@ -8,7 +8,6 @@ import Dependencies import SwiftUI -// swiftlint:disable type_body_length struct UserView: View { @Dependency(\.apiClient) var apiClient @Dependency(\.errorHandler) var errorHandler @@ -264,5 +263,3 @@ struct UserView: View { .padding(.horizontal, AppConstants.postAndCommentSpacing) } } - -// swiftlint:enable type_body_length From 597a24b017cf532d18c6fd84cf2667767fa3a8aa Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:01:08 +0000 Subject: [PATCH 22/40] Bug fix --- Mlem/Models/Content/Community/CommunityModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mlem/Models/Content/Community/CommunityModel.swift b/Mlem/Models/Content/Community/CommunityModel.swift index 8066a62d9..4c4197c67 100644 --- a/Mlem/Models/Content/Community/CommunityModel.swift +++ b/Mlem/Models/Content/Community/CommunityModel.swift @@ -197,6 +197,10 @@ struct CommunityModel { } callback(community) } + } else { + RunLoop.main.perform { [new] in + callback(new) + } } } else { favoriteCommunitiesTracker.unfavorite(community) From d8a990120bc2e4e50f90c1bdddaddb93116aeac0 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:52:45 +0000 Subject: [PATCH 23/40] Update --- Mlem.xcodeproj/project.pbxproj | 12 +++ Mlem/API/APIClient/APIClient.swift | 10 ++ Mlem/Enums/Content Type.swift | 2 +- Mlem/Enums/SearchTab.swift | 4 +- .../Instance/InstanceModel+ContentModel.swift | 19 ++++ .../Content/Instance/InstanceModel.swift | 27 ++++-- .../Content/Instance/InstanceStub.swift | 16 ++++ .../Trackers/RecentSearchesTracker.swift | 8 +- Mlem/Navigation/Routes/AppRoutes.swift | 2 +- .../Shared/Instance/InstanceStatsView.swift | 16 ++-- Mlem/Views/Shared/Instance/InstanceView.swift | 12 ++- .../Tabs/Search/RecentSearchesView.swift | 5 + .../Search/Results/InstanceResultView.swift | 92 +++++++++++++++++++ Mlem/Views/Tabs/Search/SearchModel.swift | 51 +++++++++- .../Tabs/Search/SearchResultListView.swift | 5 + .../Views/Tabs/Search/SearchResultsView.swift | 16 ++-- Mlem/Views/Tabs/Search/SearchView.swift | 31 +++++-- 17 files changed, 287 insertions(+), 41 deletions(-) create mode 100644 Mlem/Models/Content/Instance/InstanceModel+ContentModel.swift create mode 100644 Mlem/Models/Content/Instance/InstanceStub.swift create mode 100644 Mlem/Views/Tabs/Search/Results/InstanceResultView.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 92efaf31e..d7cf30852 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -43,6 +43,9 @@ 032109492AA7C41800912DFC /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032109482AA7C41800912DFC /* AvatarView.swift */; }; 032DD2FD2AC3594B00F1B33D /* LinkAttatchmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DD2FC2AC3594B00F1B33D /* LinkAttatchmentView.swift */; }; 034C724F2A82B61200B8A4B8 /* LayoutWidgetTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C724E2A82B61200B8A4B8 /* LayoutWidgetTracker.swift */; }; + 0355DA4D2B5EB51900CDF5A5 /* InstanceModel+ContentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0355DA4C2B5EB51900CDF5A5 /* InstanceModel+ContentModel.swift */; }; + 0355DA4F2B5EB63600CDF5A5 /* InstanceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0355DA4E2B5EB63600CDF5A5 /* InstanceStub.swift */; }; + 0355DA512B5EB87700CDF5A5 /* InstanceResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0355DA502B5EB87700CDF5A5 /* InstanceResultView.swift */; }; 035EB0CA2A8687C200227859 /* JumpButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035EB0C92A8687C200227859 /* JumpButtonView.swift */; }; 036ED3BC2ABF1058009664BC /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED3BB2ABF1058009664BC /* SearchModel.swift */; }; 038A16DF2A75172C0087987E /* LayoutWidgetEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A16DE2A75172C0087987E /* LayoutWidgetEditView.swift */; }; @@ -587,6 +590,9 @@ 032109482AA7C41800912DFC /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; 032DD2FC2AC3594B00F1B33D /* LinkAttatchmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAttatchmentView.swift; sourceTree = ""; }; 034C724E2A82B61200B8A4B8 /* LayoutWidgetTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutWidgetTracker.swift; sourceTree = ""; }; + 0355DA4C2B5EB51900CDF5A5 /* InstanceModel+ContentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceModel+ContentModel.swift"; sourceTree = ""; }; + 0355DA4E2B5EB63600CDF5A5 /* InstanceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceStub.swift; sourceTree = ""; }; + 0355DA502B5EB87700CDF5A5 /* InstanceResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceResultView.swift; sourceTree = ""; }; 035EB0C92A8687C200227859 /* JumpButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpButtonView.swift; sourceTree = ""; }; 036ED3BB2ABF1058009664BC /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; 038A16DE2A75172C0087987E /* LayoutWidgetEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutWidgetEditView.swift; sourceTree = ""; }; @@ -1227,6 +1233,7 @@ isa = PBXGroup; children = ( 03EEEAF22AB8DCDF0087F8D8 /* CommunityResultView.swift */, + 0355DA502B5EB87700CDF5A5 /* InstanceResultView.swift */, 03B7AAF42ABEFA7A00068B23 /* UserResultView.swift */, ); path = Results; @@ -1334,6 +1341,8 @@ isa = PBXGroup; children = ( 03A54C342B533BC50064CCDE /* InstanceModel.swift */, + 0355DA4E2B5EB63600CDF5A5 /* InstanceStub.swift */, + 0355DA4C2B5EB51900CDF5A5 /* InstanceModel+ContentModel.swift */, 03A54C362B545A430064CCDE /* InstanceModel+MenuFunctions.swift */, ); path = Instance; @@ -3045,6 +3054,7 @@ 039439932A99098900463032 /* InternetConnectionManager.swift in Sources */, CD82A2592A71775E00111034 /* UnreadTracker.swift in Sources */, CDEBC32E2A9A583900518D9D /* Post Tracker.swift in Sources */, + 0355DA4F2B5EB63600CDF5A5 /* InstanceStub.swift in Sources */, CD4368CA2AE2428C00BD8BD1 /* ContentIdentifiable.swift in Sources */, CD3FBCE32A4A844800B2063F /* Replies Feed View.swift in Sources */, 637218652A3A2AAD008C4816 /* GetPosts.swift in Sources */, @@ -3393,6 +3403,7 @@ CD6A2A792B1A553500003E23 /* SuccessResponse.swift in Sources */, 031A61802B1CEA7300ABF23B /* ChangePassword.swift in Sources */, CD4368B42AE23F3500BD8BD1 /* ChildTrackerProtocol.swift in Sources */, + 0355DA512B5EB87700CDF5A5 /* InstanceResultView.swift in Sources */, CD4368D92AE2478300BD8BD1 /* MentionModel+InboxItem.swift in Sources */, CDB45C5A2AF0AEFE00A1FF08 /* AlternativeIconLabel.swift in Sources */, 50811B302A92049B006BA3F2 /* APICommunityView+Mock.swift in Sources */, @@ -3424,6 +3435,7 @@ CD4DBC032A6F803C001A1E61 /* ReplyToPost.swift in Sources */, CD6483302A38D31C00EE6CA3 /* UpvoteCounterView.swift in Sources */, 032DD2FD2AC3594B00F1B33D /* LinkAttatchmentView.swift in Sources */, + 0355DA4D2B5EB51900CDF5A5 /* InstanceModel+ContentModel.swift in Sources */, 88B165B82A8643F4007C9115 /* View+NavigationBarColor.swift in Sources */, 030AC0522A64666C00037155 /* UserSettingsView.swift in Sources */, CDA2C5262A705D6000649D5A /* PostEditor.swift in Sources */, diff --git a/Mlem/API/APIClient/APIClient.swift b/Mlem/API/APIClient/APIClient.swift index d9144a035..3dad35a46 100644 --- a/Mlem/API/APIClient/APIClient.swift +++ b/Mlem/API/APIClient/APIClient.swift @@ -341,6 +341,16 @@ extension APIClient { ) return try await perform(request: request) } + + @discardableResult + func fetchInstanceList() async throws -> [InstanceStub] { + if let url = URL(string: "https://raw.githubusercontent.com/mlemgroup/mlem-stats/master/output/instances_by_score.json") { + if let data = try? await urlSession.data(from: url).0 { + return try decode([InstanceStub].self, from: data) + } + } + return [] + } } // MARK: - Object Resolving methods diff --git a/Mlem/Enums/Content Type.swift b/Mlem/Enums/Content Type.swift index c483aa9c9..670f7351a 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, comment, community, user, message, mention, reply + case post, comment, community, user, message, mention, reply, instance } diff --git a/Mlem/Enums/SearchTab.swift b/Mlem/Enums/SearchTab.swift index 3cb9678d6..a20bd8309 100644 --- a/Mlem/Enums/SearchTab.swift +++ b/Mlem/Enums/SearchTab.swift @@ -6,7 +6,7 @@ // enum SearchTab: String, CaseIterable, Identifiable { - case topResults, communities, users + case topResults, communities, users, instances var id: Self { self } @@ -19,5 +19,5 @@ enum SearchTab: String, CaseIterable, Identifiable { } } - static var homePageCases: [SearchTab] = [.communities, .users] + static var homePageCases: [SearchTab] = [.communities, .users, .instances] } diff --git a/Mlem/Models/Content/Instance/InstanceModel+ContentModel.swift b/Mlem/Models/Content/Instance/InstanceModel+ContentModel.swift new file mode 100644 index 000000000..ca0a17e1c --- /dev/null +++ b/Mlem/Models/Content/Instance/InstanceModel+ContentModel.swift @@ -0,0 +1,19 @@ +// +// InstanceModel+ContentModel.swift +// Mlem +// +// Created by Sjmarf on 22/01/2024. +// + +import Foundation + +extension InstanceModel: ContentModel { + var uid: ContentModelIdentifier { .init(contentType: .instance, contentId: name.hash) } + var imageUrls: [URL] { + if let url = avatar { + return [url.withIconSize(128)] + } + return [] + } + var searchResultScore: Int { self.userCount ?? 0 } +} diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index 38782a5c0..5182c0f45 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -8,15 +8,14 @@ import SwiftUI struct InstanceModel { - var instanceId: Int! - var name: String! + var displayName: String! var description: String? var avatar: URL? var banner: URL? var administrators: [UserModel]? var url: URL! var version: SiteVersion? - var creationDate: Date! + var creationDate: Date? // From APISiteView var userCount: Int? @@ -54,6 +53,12 @@ struct InstanceModel { self.update(with: site) } + init(from stub: InstanceStub) { + self.update(with: stub) + } + + var name: String { url.host() ?? displayName } + mutating func update(with response: SiteResponse) { self.administrators = response.admins.map { var user = UserModel(from: $0, usesExternalData: true) @@ -106,8 +111,7 @@ struct InstanceModel { } mutating func update(with site: APISite) { - instanceId = site.id - name = site.name + displayName = site.name description = site.sidebar avatar = site.iconUrl banner = site.bannerUrl @@ -118,6 +122,16 @@ struct InstanceModel { url = components.url } } + + mutating func update(with stub: InstanceStub) { + displayName = stub.name + url = URL(string: "https://\(stub.host)") + version = stub.version + userCount = stub.userCount + if let avatar = stub.avatar { + self.avatar = URL(string: avatar) + } + } } extension InstanceModel: Identifiable { @@ -131,6 +145,7 @@ extension InstanceModel: Hashable { /// Hashes all fields for which state changes should trigger view updates. func hash(into hasher: inout Hasher) { - hasher.combine(instanceId) + hasher.combine(url) + hasher.combine(creationDate) } } diff --git a/Mlem/Models/Content/Instance/InstanceStub.swift b/Mlem/Models/Content/Instance/InstanceStub.swift new file mode 100644 index 000000000..96393dd1c --- /dev/null +++ b/Mlem/Models/Content/Instance/InstanceStub.swift @@ -0,0 +1,16 @@ +// +// InstanceStub.swift +// Mlem +// +// Created by Sjmarf on 22/01/2024. +// + +import Foundation + +struct InstanceStub: Codable { + let name: String + let host: String + let avatar: String? + let version: SiteVersion + let userCount: Int +} diff --git a/Mlem/Models/Trackers/RecentSearchesTracker.swift b/Mlem/Models/Trackers/RecentSearchesTracker.swift index 2f72678f4..2a074eb8a 100644 --- a/Mlem/Models/Trackers/RecentSearchesTracker.swift +++ b/Mlem/Models/Trackers/RecentSearchesTracker.swift @@ -19,7 +19,7 @@ class RecentSearchesTracker: ObservableObject { @Published var recentSearches: [AnyContentModel] = .init() /// clears recentSearches and loads new values based on the current account - func reloadRecentSearches(accountId: String?) async throws { + func reloadRecentSearches(accountId: String?, instanceStubs: [InstanceStub]) async throws { defer { hasLoaded = true } if let accountId { @@ -36,6 +36,12 @@ class RecentSearchesTracker: ObservableObject { case .user: let user = try await personRepository.loadUser(for: id.contentId) newSearches.append(AnyContentModel(user)) + case .instance: + if let stub = instanceStubs.first(where: { $0.host.hash == id.contentId }) { + newSearches.append(AnyContentModel(InstanceModel(from: stub))) + } else { + print("Recent search error: cannot find instance sub") + } default: assertionFailure("Received unexpected content type in recent searches \(id.contentType)") return diff --git a/Mlem/Navigation/Routes/AppRoutes.swift b/Mlem/Navigation/Routes/AppRoutes.swift index d227021f8..a81137fbc 100644 --- a/Mlem/Navigation/Routes/AppRoutes.swift +++ b/Mlem/Navigation/Routes/AppRoutes.swift @@ -18,7 +18,7 @@ enum AppRoute: Routable { case apiPost(APIPost) case community(CommunityModel) - case instance(String, InstanceModel? = nil) + case instance(String? = nil, InstanceModel? = nil) @available(*, deprecated, message: "Use .userProfile instead.") case apiPerson(APIPerson) diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceStatsView.swift index e22bfa35d..023c69ede 100644 --- a/Mlem/Views/Shared/Instance/InstanceStatsView.swift +++ b/Mlem/Views/Shared/Instance/InstanceStatsView.swift @@ -16,14 +16,16 @@ struct InstanceStatsView: View { var body: some View { VStack(spacing: 16) { - box { - HStack { - Label(instance.creationDate.dateString, systemImage: Icons.cakeDay) - Text("•") - Label(instance.creationDate.getRelativeTime(unitsStyle: .abbreviated), systemImage: Icons.time) + if let date = instance.creationDate { + box { + HStack { + Label(date.dateString, systemImage: Icons.cakeDay) + Text("•") + Label(date.getRelativeTime(unitsStyle: .abbreviated), systemImage: Icons.time) + } + .foregroundStyle(.secondary) + .font(.footnote) } - .foregroundStyle(.secondary) - .font(.footnote) } HStack(spacing: 16) { box { diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index f3e194b27..ed065ede5 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -41,13 +41,14 @@ struct InstanceView: View { @State var selectedTab: InstanceViewTab = .about - init(domainName: String, instance: InstanceModel? = nil) { - _domainName = State(wrappedValue: domainName) + init(domainName: String? = nil, instance: InstanceModel? = nil) { + _domainName = State(wrappedValue: domainName ?? instance?.name ?? "") var instance = instance if domainName == siteInformation.instance?.url.host() { instance = siteInformation.instance ?? instance } _instance = State(wrappedValue: instance) + print("INSTANCE", domainName) } var subtitleText: String { @@ -69,7 +70,7 @@ struct InstanceView: View { VStack(spacing: 5) { if errorDetails == nil { if let instance { - Text(instance.name) + Text(instance.displayName) .font(.title) .fontWeight(.semibold) .lineLimit(1) @@ -92,7 +93,7 @@ struct InstanceView: View { .padding(.bottom, 5) if let errorDetails { ErrorView(errorDetails) - } else if let instance { + } else if let instance, instance.creationDate != nil { VStack(spacing: 0) { VStack(spacing: 4) { Divider() @@ -110,6 +111,7 @@ struct InstanceView: View { } else { Text("No Description") .foregroundStyle(.secondary) + .padding(.top) } case .administrators: if let administrators = instance.administrators { @@ -207,7 +209,7 @@ struct InstanceView: View { } } .navigationBarColor() - .navigationTitle(instance?.name ?? domainName) + .navigationTitle(instance?.displayName ?? domainName) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Mlem/Views/Tabs/Search/RecentSearchesView.swift b/Mlem/Views/Tabs/Search/RecentSearchesView.swift index ceb236642..aa283f710 100644 --- a/Mlem/Views/Tabs/Search/RecentSearchesView.swift +++ b/Mlem/Views/Tabs/Search/RecentSearchesView.swift @@ -89,6 +89,11 @@ struct RecentSearchesView: View { contentTracker.update(with: AnyContentModel($0)) } ) + } else if let instance = contentModel.wrappedValue as? InstanceModel { + InstanceResultView( + instance, + complications: .withTypeLabel + ) } } .simultaneousGesture(TapGesture().onEnded { diff --git a/Mlem/Views/Tabs/Search/Results/InstanceResultView.swift b/Mlem/Views/Tabs/Search/Results/InstanceResultView.swift new file mode 100644 index 000000000..a05800c1b --- /dev/null +++ b/Mlem/Views/Tabs/Search/Results/InstanceResultView.swift @@ -0,0 +1,92 @@ +// +// InstanceResultView.swift +// Mlem +// +// Created by Sjmarf on 22/01/2024. +// + +import SwiftUI +import Dependencies + +enum InstanceComplication: CaseIterable { + case type, version, users +} + +extension Array where Element == InstanceComplication { + static let withTypeLabel: [InstanceComplication] = [.type, .version, .users] + static let withoutTypeLabel: [InstanceComplication] = [.version, .users] +} + +struct InstanceResultView: View { + @Dependency(\.apiClient) private var apiClient + @Dependency(\.hapticManager) var hapticManager + + let instance: InstanceModel + let swipeActions: SwipeConfiguration? + let complications: [InstanceComplication] + + init( + _ instance: InstanceModel, + complications: [InstanceComplication] = .withoutTypeLabel, + swipeActions: SwipeConfiguration? = nil + ) { + self.instance = instance + self.complications = complications + self.swipeActions = swipeActions + } + + var caption: String { + var parts: [String] = [] + if complications.contains(.type) { + parts.append("Instance") + } + if complications.contains(.version), let version = instance.version { + parts.append(String(describing: version)) + } + return parts.joined(separator: " ∙ ") + } + + var body: some View { + NavigationLink(value: AppRoute.instance(instance.url.host(), instance)) { + HStack(spacing: 10) { + AvatarView(instance: instance, avatarSize: 48, iconResolution: .fixed(128)) + + VStack(alignment: .leading, spacing: 4) { + Text(instance.name) + .lineLimit(1) + Text(caption) + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) + } + Spacer() + if complications.contains(.users), let userCount = instance.userCount { + HStack(spacing: 5) { + Text(abbreviateNumber(userCount)) + .monospacedDigit() + Image(systemName: Icons.personFill) + } + .foregroundStyle(.secondary) + } + Image(systemName: Icons.forward) + .imageScale(.small) + .foregroundStyle(.tertiary) + } + .padding(.horizontal) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.vertical, 8) + .background(.background) + .draggable(instance.url) { + HStack { + AvatarView(instance: instance, avatarSize: 24) + Text(instance.name) + } + .padding(8) + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .addSwipeyActions(swipeActions ?? .init()) + } +} diff --git a/Mlem/Views/Tabs/Search/SearchModel.swift b/Mlem/Views/Tabs/Search/SearchModel.swift index 42f4cd8cf..2f0282742 100644 --- a/Mlem/Views/Tabs/Search/SearchModel.swift +++ b/Mlem/Views/Tabs/Search/SearchModel.swift @@ -25,6 +25,8 @@ class SearchModel: ObservableObject { var firstPageCommunities: [AnyContentModel]? var firstPageUsers: [AnyContentModel]? + var instanceStubs: [InstanceStub] = [] + init(searchTab: SearchTab = .topResults, internetSpeed: InternetSpeed = .fast) { self.searchTab = searchTab self.internetSpeed = internetSpeed @@ -34,7 +36,12 @@ class SearchModel: ObservableObject { switch searchTab { case .topResults: if let communities = firstPageCommunities, let users = firstPageUsers { - contentTracker.replaceAll(with: combineResults(communities: communities, users: users)) + contentTracker.replaceAll( + with: combineResults( + communities: communities, + users: users, + instances: searchInstances(page: 1)) + ) return } case .communities: @@ -47,6 +54,8 @@ class SearchModel: ObservableObject { contentTracker.replaceAll(with: users) return } + case .instances: + contentTracker.replaceAll(with: searchInstances(page: 1)) } contentTracker.refresh(using: performSearch) } @@ -57,7 +66,8 @@ class SearchModel: ObservableObject { case .topResults: async let communities = try await searchCommunities(page: page) async let users = try await searchUsers(page: page) - return try await combineResults(communities: communities, users: users) + async let instances = searchInstances(page: page) + return try await combineResults(communities: communities, users: users, instances: instances) case .communities: if searchText != previousSearchText { firstPageUsers = nil @@ -68,6 +78,9 @@ class SearchModel: ObservableObject { firstPageCommunities = nil } return try await searchUsers(page: page) + case .instances: + print("SHORTCUT", instanceStubs.count) + return searchInstances(page: page) } } @@ -97,10 +110,42 @@ class SearchModel: ObservableObject { return users } - func combineResults(communities: [AnyContentModel], users: [AnyContentModel]) -> [AnyContentModel] { + @discardableResult + func searchInstances(page: Int) -> [AnyContentModel] { + print("SEARCH INSTANCES", page) + + if searchText.isEmpty { + if page != 1 { + return [] + } + return instanceStubs.map { AnyContentModel(InstanceModel(from: $0)) } + } + + let query = searchText.lowercased() + var results: [InstanceStub] = [] + for stub in instanceStubs { + if stub.name.lowercased().contains(query) || stub.host.contains(query) { + results.append(stub) + } + if results.count == internetSpeed.pageSize { + break + } + } + let instances = results + .dropFirst((page-1) * internetSpeed.pageSize) + .map { AnyContentModel(InstanceModel(from: $0)) } + return instances + } + + func combineResults( + communities: [AnyContentModel], + users: [AnyContentModel], + instances: [AnyContentModel] + ) -> [AnyContentModel] { var results: [AnyContentModel] = .init() results.append(contentsOf: communities) results.append(contentsOf: users) + results.append(contentsOf: instances) return results.sorted { $0.searchResultScore > $1.searchResultScore } } diff --git a/Mlem/Views/Tabs/Search/SearchResultListView.swift b/Mlem/Views/Tabs/Search/SearchResultListView.swift index 5722c27b5..15ea482f6 100644 --- a/Mlem/Views/Tabs/Search/SearchResultListView.swift +++ b/Mlem/Views/Tabs/Search/SearchResultListView.swift @@ -36,6 +36,11 @@ struct SearchResultListView: View { contentTracker.update(with: AnyContentModel($0)) } ) + } else if let instance = contentModel.wrappedValue as? InstanceModel { + InstanceResultView( + instance, + complications: showTypeLabel ? .withTypeLabel : .withoutTypeLabel + ) } } .simultaneousGesture(TapGesture().onEnded { diff --git a/Mlem/Views/Tabs/Search/SearchResultsView.swift b/Mlem/Views/Tabs/Search/SearchResultsView.swift index 3d818c01c..71b186522 100644 --- a/Mlem/Views/Tabs/Search/SearchResultsView.swift +++ b/Mlem/Views/Tabs/Search/SearchResultsView.swift @@ -44,14 +44,14 @@ struct SearchResultsView: View { BubblePicker(SearchTab.allCases, selected: $searchModel.searchTab) { Text($0.label) } - Group { - if contentTracker.isLoading && contentTracker.page == 1 && !shouldLoad { - ProgressView() - .padding(.trailing) - .transition(.opacity) - } - } - .animation(.default, value: contentTracker.isLoading) +// Group { +// if contentTracker.isLoading && contentTracker.page == 1 && !shouldLoad { +// ProgressView() +// .padding(.trailing) +// .transition(.opacity) +// } +// } +// .animation(.default, value: contentTracker.isLoading) } } } diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index 0dffe8f7a..cc483223d 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -19,6 +19,7 @@ private struct ViewOffsetKey: PreferenceKey { } struct SearchView: View { + @Dependency(\.apiClient) var apiClient @Dependency(\.errorHandler) var errorHandler enum Page { @@ -31,9 +32,10 @@ struct SearchView: View { @Environment(\.tabSelectionHashValue) var tabSelectionHashValue @EnvironmentObject var appState: AppState @EnvironmentObject private var recentSearchesTracker: RecentSearchesTracker - @StateObject var searchModel: SearchModel + @StateObject var searchModel: SearchModel @StateObject var homeSearchModel: SearchModel + @StateObject var homeContentTracker: ContentTracker = .init() @State var searchBarFocused: Bool = false @@ -75,12 +77,27 @@ struct SearchView: View { .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .onAppear { - Task(priority: .background) { - do { - try await recentSearchesTracker.reloadRecentSearches(accountId: appState.currentActiveAccount?.stableIdString) - } catch { - print("Error while loading recent searches: \(error.localizedDescription)") - errorHandler.handle(error) + if searchModel.instanceStubs.isEmpty { + Task(priority: .background) { + var instanceStubs: [InstanceStub] = [] + do { + instanceStubs = try await apiClient.fetchInstanceList() + DispatchQueue.main.async { + searchModel.instanceStubs = instanceStubs + homeSearchModel.instanceStubs = instanceStubs + } + } catch { + print("Error while loading instance stubs: \(error.localizedDescription)") + errorHandler.handle(error) + } + do { + try await recentSearchesTracker.reloadRecentSearches( + accountId: appState.currentActiveAccount?.stableIdString, + instanceStubs: instanceStubs + ) + } catch { + print("Error while loading recent searches: \(error.localizedDescription)") + } } } } From 06837fda684b9e355c837521af6e585512c9c2b6 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:57:39 +0000 Subject: [PATCH 24/40] Fix background on light mode --- Mlem/Views/Shared/Instance/InstanceView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index f3e194b27..d7ab665b3 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -123,8 +123,12 @@ struct InstanceView: View { } case .details: if instance.userCount != nil { - InstanceStatsView(instance: instance) - .padding(.top, 16) + VStack(spacing: 0) { + InstanceStatsView(instance: instance) + .padding(.vertical, 16) + .background(Color(uiColor: .systemGroupedBackground)) + Divider() + } } else { ProgressView() .padding(.top) From b5dd5bd20a496d4b8a547a6fc9aaf9626b2d65fe Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:05:59 +0000 Subject: [PATCH 25/40] Update --- .../Trackers/RecentSearchesTracker.swift | 6 +++--- Mlem/Views/Tabs/Search/SearchModel.swift | 18 ++++++++---------- Mlem/Views/Tabs/Search/SearchView.swift | 11 +++++------ 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Mlem/Models/Trackers/RecentSearchesTracker.swift b/Mlem/Models/Trackers/RecentSearchesTracker.swift index 2a074eb8a..d79ea7709 100644 --- a/Mlem/Models/Trackers/RecentSearchesTracker.swift +++ b/Mlem/Models/Trackers/RecentSearchesTracker.swift @@ -19,7 +19,7 @@ class RecentSearchesTracker: ObservableObject { @Published var recentSearches: [AnyContentModel] = .init() /// clears recentSearches and loads new values based on the current account - func reloadRecentSearches(accountId: String?, instanceStubs: [InstanceStub]) async throws { + func reloadRecentSearches(accountId: String?, instances: [InstanceModel]) async throws { defer { hasLoaded = true } if let accountId { @@ -37,8 +37,8 @@ class RecentSearchesTracker: ObservableObject { let user = try await personRepository.loadUser(for: id.contentId) newSearches.append(AnyContentModel(user)) case .instance: - if let stub = instanceStubs.first(where: { $0.host.hash == id.contentId }) { - newSearches.append(AnyContentModel(InstanceModel(from: stub))) + if let instance = instances.first(where: { $0.name.hash == id.contentId }) { + newSearches.append(AnyContentModel(instance)) } else { print("Recent search error: cannot find instance sub") } diff --git a/Mlem/Views/Tabs/Search/SearchModel.swift b/Mlem/Views/Tabs/Search/SearchModel.swift index 2f0282742..9fe05f367 100644 --- a/Mlem/Views/Tabs/Search/SearchModel.swift +++ b/Mlem/Views/Tabs/Search/SearchModel.swift @@ -25,7 +25,7 @@ class SearchModel: ObservableObject { var firstPageCommunities: [AnyContentModel]? var firstPageUsers: [AnyContentModel]? - var instanceStubs: [InstanceStub] = [] + static var allInstances: [InstanceModel] = [] init(searchTab: SearchTab = .topResults, internetSpeed: InternetSpeed = .fast) { self.searchTab = searchTab @@ -79,7 +79,7 @@ class SearchModel: ObservableObject { } return try await searchUsers(page: page) case .instances: - print("SHORTCUT", instanceStubs.count) + print("SHORTCUT", SearchModel.allInstances.count) return searchInstances(page: page) } } @@ -112,20 +112,18 @@ class SearchModel: ObservableObject { @discardableResult func searchInstances(page: Int) -> [AnyContentModel] { - print("SEARCH INSTANCES", page) - if searchText.isEmpty { if page != 1 { return [] } - return instanceStubs.map { AnyContentModel(InstanceModel(from: $0)) } + return SearchModel.allInstances.map { AnyContentModel($0) } } let query = searchText.lowercased() - var results: [InstanceStub] = [] - for stub in instanceStubs { - if stub.name.lowercased().contains(query) || stub.host.contains(query) { - results.append(stub) + var results: [InstanceModel] = [] + for instance in SearchModel.allInstances { + if instance.displayName.lowercased().contains(query) || instance.name.contains(query) { + results.append(instance) } if results.count == internetSpeed.pageSize { break @@ -133,7 +131,7 @@ class SearchModel: ObservableObject { } let instances = results .dropFirst((page-1) * internetSpeed.pageSize) - .map { AnyContentModel(InstanceModel(from: $0)) } + .map { AnyContentModel($0) } return instances } diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index cc483223d..8f4c00fa3 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -77,14 +77,13 @@ struct SearchView: View { .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .onAppear { - if searchModel.instanceStubs.isEmpty { + if SearchModel.allInstances.isEmpty { Task(priority: .background) { - var instanceStubs: [InstanceStub] = [] + var instances: [InstanceModel] = [] do { - instanceStubs = try await apiClient.fetchInstanceList() + instances = try await apiClient.fetchInstanceList().map { InstanceModel(from: $0) } DispatchQueue.main.async { - searchModel.instanceStubs = instanceStubs - homeSearchModel.instanceStubs = instanceStubs + SearchModel.allInstances = instances } } catch { print("Error while loading instance stubs: \(error.localizedDescription)") @@ -93,7 +92,7 @@ struct SearchView: View { do { try await recentSearchesTracker.reloadRecentSearches( accountId: appState.currentActiveAccount?.stableIdString, - instanceStubs: instanceStubs + instances: instances ) } catch { print("Error while loading recent searches: \(error.localizedDescription)") From 2b654ca0382605af822fe6992d37701f1ba33b23 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:14:24 +0000 Subject: [PATCH 26/40] Update RecentSearchesView.swift --- Mlem/Views/Tabs/Search/RecentSearchesView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mlem/Views/Tabs/Search/RecentSearchesView.swift b/Mlem/Views/Tabs/Search/RecentSearchesView.swift index aa283f710..180c8cd68 100644 --- a/Mlem/Views/Tabs/Search/RecentSearchesView.swift +++ b/Mlem/Views/Tabs/Search/RecentSearchesView.swift @@ -92,7 +92,8 @@ struct RecentSearchesView: View { } else if let instance = contentModel.wrappedValue as? InstanceModel { InstanceResultView( instance, - complications: .withTypeLabel + complications: .withTypeLabel, + swipeActions: .init(trailingActions: [deleteSwipeAction(contentModel)]) ) } } From 19f7e4c50c011ae356fe59b6a25637b6c5052c2a Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Wed, 24 Jan 2024 20:49:30 +0000 Subject: [PATCH 27/40] Add pre-emptive support for `GetPersonDetailsResponse.site` --- Mlem/API/Requests/Person/GetPersonDetails.swift | 6 ++++++ Mlem/Models/Content/User/UserModel+MenuFunctions.swift | 8 +++++++- Mlem/Models/Content/User/UserModel.swift | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Mlem/API/Requests/Person/GetPersonDetails.swift b/Mlem/API/Requests/Person/GetPersonDetails.swift index 64072843f..e01062f17 100644 --- a/Mlem/API/Requests/Person/GetPersonDetails.swift +++ b/Mlem/API/Requests/Person/GetPersonDetails.swift @@ -73,6 +73,12 @@ struct GetPersonDetailsRequest: APIGetRequest { struct GetPersonDetailsResponse: Decodable { let personView: APIPersonView let comments: [APICommentView] + + // At the time of writing this has been merged into + // Lemmy but isn't in a live Lemmy version. Probably will be in 0.19.3. + // - Sjmarf 2024-01-24 + let site: APISite? + let posts: [APIPostView] let moderates: [APICommunityModeratorView] } diff --git a/Mlem/Models/Content/User/UserModel+MenuFunctions.swift b/Mlem/Models/Content/User/UserModel+MenuFunctions.swift index 1bb2eb7c1..7b6c40b10 100644 --- a/Mlem/Models/Content/User/UserModel+MenuFunctions.swift +++ b/Mlem/Models/Content/User/UserModel+MenuFunctions.swift @@ -26,11 +26,17 @@ extension UserModel { func menuFunctions(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> [MenuFunction] { var functions: [MenuFunction] = .init() if let instanceHost = self.profileUrl.host() { + let instance: InstanceModel? + if let site { + instance = .init(from: site) + } else { + instance = nil + } functions.append( .navigationMenuFunction( text: instanceHost, imageName: Icons.instance, - destination: .instance(instanceHost) + destination: .instance(instanceHost, instance) ) ) } diff --git a/Mlem/Models/Content/User/UserModel.swift b/Mlem/Models/Content/User/UserModel.swift index 136eb791d..3227664e7 100644 --- a/Mlem/Models/Content/User/UserModel.swift +++ b/Mlem/Models/Content/User/UserModel.swift @@ -55,6 +55,7 @@ struct UserModel { var commentCount: Int? // From GetPersonDetailsResponse + var site: APISite? var moderatedCommunities: [CommunityModel]? static let developerNames = [ @@ -73,6 +74,7 @@ struct UserModel { /// - Parameter response: GetPersonDetailsResponse to create a UserModel representation of init(from response: GetPersonDetailsResponse) { self.init(from: response.personView) + self.site = response.site self.moderatedCommunities = response.moderates.map { CommunityModel(from: $0.community) } } From b210e58dd0f0cd3ec736cf741d3fd2ed16d84870 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:26:44 +0000 Subject: [PATCH 28/40] Design tweak --- Mlem/Views/Shared/Instance/InstanceView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 290fc96f2..214ff2a87 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -29,6 +29,8 @@ struct InstanceView: View { @Dependency(\.errorHandler) var errorHandler @Dependency(\.siteInformation) var siteInformation + @Environment(\.colorScheme) var colorScheme + @Environment(\.navigationPathWithRoutes) private var navigationPath @Environment(\.scrollViewProxy) private var scrollViewProxy @@ -129,7 +131,9 @@ struct InstanceView: View { InstanceStatsView(instance: instance) .padding(.vertical, 16) .background(Color(uiColor: .systemGroupedBackground)) - Divider() + if colorScheme == .light { + Divider() + } } } else { ProgressView() From 51e7868b995e79f7ce657fa020137c880ad9fcfc Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:26:44 +0000 Subject: [PATCH 29/40] Design tweak --- Mlem/Views/Shared/Instance/InstanceView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index d7ab665b3..3531906e5 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -29,6 +29,8 @@ struct InstanceView: View { @Dependency(\.errorHandler) var errorHandler @Dependency(\.siteInformation) var siteInformation + @Environment(\.colorScheme) var colorScheme + @Environment(\.navigationPathWithRoutes) private var navigationPath @Environment(\.scrollViewProxy) private var scrollViewProxy @@ -127,7 +129,9 @@ struct InstanceView: View { InstanceStatsView(instance: instance) .padding(.vertical, 16) .background(Color(uiColor: .systemGroupedBackground)) - Divider() + if colorScheme == .light { + Divider() + } } } else { ProgressView() From 50ccbf395e768036e49ec12af697bbf9288176b7 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:31:28 +0000 Subject: [PATCH 30/40] Remove debug print --- Mlem/Views/Shared/Instance/InstanceView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index 214ff2a87..fa6035473 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -50,7 +50,6 @@ struct InstanceView: View { instance = siteInformation.instance ?? instance } _instance = State(wrappedValue: instance) - print("INSTANCE", domainName) } var subtitleText: String { From a8d31b8cbfc53799660fcd9bcd570961c05c51f8 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:27:29 +0000 Subject: [PATCH 31/40] Update project.pbxproj --- Mlem.xcodeproj/project.pbxproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 8ae1e8270..e637e3663 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -61,7 +61,6 @@ 03A276792AFD903600C0D66B /* CommunityModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */; }; 03A2767B2AFE560000C0D66B /* CommunityModel+SwipeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */; }; 03A2767D2AFE656700C0D66B /* UserModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */; }; - 03A40DAD2AD5EA11005F019F /* NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */; }; 03A54C322B5331F30064CCDE /* InstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A54C312B5331F30064CCDE /* InstanceView.swift */; }; 03A54C352B533BC50064CCDE /* InstanceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A54C342B533BC50064CCDE /* InstanceModel.swift */; }; 03A54C372B545A430064CCDE /* InstanceModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A54C362B545A430064CCDE /* InstanceModel+MenuFunctions.swift */; }; @@ -608,7 +607,6 @@ 03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+MenuFunctions.swift"; sourceTree = ""; }; 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+SwipeActions.swift"; sourceTree = ""; }; 03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserModel+MenuFunctions.swift"; sourceTree = ""; }; - 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoPostsView.swift; sourceTree = ""; }; 03A54C312B5331F30064CCDE /* InstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceView.swift; sourceTree = ""; }; 03A54C342B533BC50064CCDE /* InstanceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceModel.swift; sourceTree = ""; }; 03A54C362B545A430064CCDE /* InstanceModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InstanceModel+MenuFunctions.swift"; sourceTree = ""; }; @@ -3407,7 +3405,7 @@ 6363D5C727EE196700E34822 /* ContentView.swift in Sources */, 03F4DC9D2B193F4C00556C67 /* MatrixLinkView.swift in Sources */, 03A54C322B5331F30064CCDE /* InstanceView.swift in Sources */, - 6D8F08FF2A4029AE003EB4FD /* Community List View.swift in Sources */, + 6D8F08FF2A4029AE003EB4FD /* CommunityListSection.swift in Sources */, 6D8F08FF2A4029AE003EB4FD /* CommunityListSection.swift in Sources */, 6D8F08FF2A4029AE003EB4FD /* CommunityListSection.swift in Sources */, 035EB0CA2A8687C200227859 /* JumpButtonView.swift in Sources */, From 513f3a2cd05123715bd9c0225e3252a63e222670 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:32:30 +0000 Subject: [PATCH 32/40] Bug fixes --- Mlem/API/Models/Site/APILocalSite.swift | 2 +- Mlem/Models/Content/Community/CommunityModel.swift | 5 ----- Mlem/Models/Content/Instance/InstanceModel.swift | 2 +- Mlem/Views/Shared/Instance/InstanceStatsView.swift | 2 +- .../Tabs/Search/Results/CommunityResultView.swift | 10 ---------- 5 files changed, 3 insertions(+), 18 deletions(-) diff --git a/Mlem/API/Models/Site/APILocalSite.swift b/Mlem/API/Models/Site/APILocalSite.swift index e40cd9659..7ea8f7c7f 100644 --- a/Mlem/API/Models/Site/APILocalSite.swift +++ b/Mlem/API/Models/Site/APILocalSite.swift @@ -21,7 +21,7 @@ struct APILocalSite: Decodable { // let applicationQuestion: String? let privateInstance: Bool // let defaultTheme: String - let defaultPostListingType: FeedType + let defaultPostListingType: APIListingType // let legalInformation: String? let hideModlogModNames: Bool let applicationEmailAdmins: Bool diff --git a/Mlem/Models/Content/Community/CommunityModel.swift b/Mlem/Models/Content/Community/CommunityModel.swift index 9cc909e61..970d82f46 100644 --- a/Mlem/Models/Content/Community/CommunityModel.swift +++ b/Mlem/Models/Content/Community/CommunityModel.swift @@ -113,11 +113,6 @@ struct CommunityModel { localSubscriberCount = communityView.counts.subscribersLocal postCount = communityView.counts.posts commentCount = communityView.counts.comments - activeUserCount = .init( - subscriberCount = communityView.counts.subscribers - localSubscriberCount = communityView.counts.subscribersLocal - postCount = communityView.counts.posts - commentCount = communityView.counts.comments activeUserCount = .init( sixMonths: communityView.counts.usersActiveHalfYear, month: communityView.counts.usersActiveMonth, diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index 38782a5c0..3de9f57a8 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -37,7 +37,7 @@ struct InstanceModel { var slurFilterString: String? var captchaDifficulty: APICaptchaDifficulty? var registrationMode: APIRegistrationMode? - var defaultFeedType: FeedType? + var defaultFeedType: APIListingType? var hideModlogModNames: Bool? var applicationsEmailAdmins: Bool? var reportsEmailAdmins: Bool? diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceStatsView.swift index e22bfa35d..0a6d952d0 100644 --- a/Mlem/Views/Shared/Instance/InstanceStatsView.swift +++ b/Mlem/Views/Shared/Instance/InstanceStatsView.swift @@ -170,7 +170,7 @@ struct InstanceStatsView: View { settingRow( "Default Feed Type", systemImage: Icons.feeds, - value: feedType.label + value: feedType.rawValue ) } } diff --git a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift index 2d1843f21..861f5bbf3 100644 --- a/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift +++ b/Mlem/Views/Tabs/Search/Results/CommunityResultView.swift @@ -18,16 +18,6 @@ extension Array where Element == CommunityComplication { static let instanceOnly: [CommunityComplication] = [.instance] } -enum CommunityComplication: CaseIterable { - case type, instance, subscribers -} - -extension Array where Element == CommunityComplication { - static let withTypeLabel: [CommunityComplication] = [.type, .instance, .subscribers] - static let withoutTypeLabel: [CommunityComplication] = [.instance, .subscribers] - static let instanceOnly: [CommunityComplication] = [.instance] -} - struct CommunityResultView: View { @Dependency(\.apiClient) private var apiClient @Dependency(\.hapticManager) var hapticManager From 51a1c8ae9a41dae78dfd1e1da1a356a5c773af39 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:33:28 +0000 Subject: [PATCH 33/40] Tweak --- Mlem/API/Models/Site/APILocalSite.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mlem/API/Models/Site/APILocalSite.swift b/Mlem/API/Models/Site/APILocalSite.swift index 7ea8f7c7f..9d1b4ccb0 100644 --- a/Mlem/API/Models/Site/APILocalSite.swift +++ b/Mlem/API/Models/Site/APILocalSite.swift @@ -68,7 +68,7 @@ enum APIRegistrationMode: String, Codable { case .closed: return .red case .requireApplication: - return .yellow + return .orange case .open: return .green } From 6ee52e071eeef8809914d41cf7b2f2c44a1c5ce8 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:38:35 +0000 Subject: [PATCH 34/40] Add context menu to search results --- Mlem/Views/Tabs/Search/Results/InstanceResultView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Mlem/Views/Tabs/Search/Results/InstanceResultView.swift b/Mlem/Views/Tabs/Search/Results/InstanceResultView.swift index a05800c1b..0086c510b 100644 --- a/Mlem/Views/Tabs/Search/Results/InstanceResultView.swift +++ b/Mlem/Views/Tabs/Search/Results/InstanceResultView.swift @@ -88,5 +88,10 @@ struct InstanceResultView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } .addSwipeyActions(swipeActions ?? .init()) + .contextMenu { + ForEach(instance.menuFunctions()) { item in + MenuButton(menuFunction: item, confirmDestructive: nil) + } + } } } From e85f37b33a9726a6b3e2d39f5021b8ba638e1a8c Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 28 Jan 2024 11:35:21 +0000 Subject: [PATCH 35/40] Show/hide slur filter with animation --- Mlem/Views/Shared/Instance/InstanceStatsView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceStatsView.swift index 0a6d952d0..b139b1415 100644 --- a/Mlem/Views/Shared/Instance/InstanceStatsView.swift +++ b/Mlem/Views/Shared/Instance/InstanceStatsView.swift @@ -162,7 +162,9 @@ struct InstanceStatsView: View { .foregroundStyle(.secondary) .padding(12) .onTapGesture { - showingSlurRegex.toggle() + withAnimation { + showingSlurRegex.toggle() + } } } if developerMode, let feedType = instance.defaultFeedType { From 37d06c2256fa7c54075c67a36c58c0693a49fe48 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 28 Jan 2024 11:35:37 +0000 Subject: [PATCH 36/40] Make some settings always show --- .../Shared/Instance/InstanceStatsView.swift | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceStatsView.swift index b139b1415..e03ddb24f 100644 --- a/Mlem/Views/Shared/Instance/InstanceStatsView.swift +++ b/Mlem/Views/Shared/Instance/InstanceStatsView.swift @@ -180,31 +180,28 @@ struct InstanceStatsView: View { .background(Color(uiColor: .secondarySystemGroupedBackground)) .cornerRadius(AppConstants.largeItemCornerRadius) - if developerMode { - VStack(alignment: .leading, spacing: 0) { - settingRow( - "Show Mod Names in Modlog", - systemImage: Icons.moderation, - value: !(instance.hideModlogModNames ?? true) - ) - Divider() - settingRow( - "Applications Email Admins", - systemImage: Icons.person, - value: instance.applicationsEmailAdmins ?? false - ) - Divider() - settingRow( - "Reports Email Admins", - systemImage: Icons.moderationReport, - value: instance.reportsEmailAdmins ?? false - ) - } - .frame(maxWidth: .infinity) - .background(Color(uiColor: .secondarySystemGroupedBackground)) - .cornerRadius(AppConstants.largeItemCornerRadius) + VStack(alignment: .leading, spacing: 0) { + settingRow( + "Show Mod Names in Modlog", + systemImage: Icons.moderation, + value: !(instance.hideModlogModNames ?? true) + ) + Divider() + settingRow( + "Applications Email Admins", + systemImage: Icons.person, + value: instance.applicationsEmailAdmins ?? false + ) + Divider() + settingRow( + "Reports Email Admins", + systemImage: Icons.moderationReport, + value: instance.reportsEmailAdmins ?? false + ) } - + .frame(maxWidth: .infinity) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(AppConstants.largeItemCornerRadius) } .padding(.horizontal, 16) } From e73c7a1494f544d4e586f66d7dc6d640f74b48f0 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:57:36 +0000 Subject: [PATCH 37/40] Update InstanceModel.swift --- Mlem/Models/Content/Instance/InstanceModel.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mlem/Models/Content/Instance/InstanceModel.swift b/Mlem/Models/Content/Instance/InstanceModel.swift index dc382533f..9f76ebb8a 100644 --- a/Mlem/Models/Content/Instance/InstanceModel.swift +++ b/Mlem/Models/Content/Instance/InstanceModel.swift @@ -42,8 +42,6 @@ struct InstanceModel { var applicationsEmailAdmins: Bool? var reportsEmailAdmins: Bool? - var slurFilterRegex: Regex? - init(from response: SiteResponse) { self.update(with: response) } From 64b6428e726e2ebed197977e6d7a27c9d4f38490 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:43:33 +0000 Subject: [PATCH 38/40] Update --- Mlem.xcodeproj/project.pbxproj | 8 +-- ...tsView.swift => InstanceDetailsView.swift} | 49 ++++++++++--------- Mlem/Views/Shared/Instance/InstanceView.swift | 2 +- 3 files changed, 31 insertions(+), 28 deletions(-) rename Mlem/Views/Shared/Instance/{InstanceStatsView.swift => InstanceDetailsView.swift} (87%) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 098fd8ff5..0cc538cd2 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -34,7 +34,7 @@ 031A93D62AC847DA0077030C /* UploadConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A93D52AC847DA0077030C /* UploadConfirmationView.swift */; }; 031BF9532AB24BAF00F4517F /* SiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031BF9522AB24BAF00F4517F /* SiteVersion.swift */; }; 031BF9552AB25AFB00F4517F /* SiteVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031BF9542AB25AFB00F4517F /* SiteVersionTests.swift */; }; - 031F95572B5C7FF20069C244 /* InstanceStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F95562B5C7FF20069C244 /* InstanceStatsView.swift */; }; + 031F95572B5C7FF20069C244 /* InstanceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F95562B5C7FF20069C244 /* InstanceDetailsView.swift */; }; 032109472AA7C3FC00912DFC /* CommunityLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032109462AA7C3FC00912DFC /* CommunityLabelView.swift */; }; 032109492AA7C41800912DFC /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032109482AA7C41800912DFC /* AvatarView.swift */; }; 032DD2FD2AC3594B00F1B33D /* LinkAttatchmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DD2FC2AC3594B00F1B33D /* LinkAttatchmentView.swift */; }; @@ -587,7 +587,7 @@ 031A93D52AC847DA0077030C /* UploadConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadConfirmationView.swift; sourceTree = ""; }; 031BF9522AB24BAF00F4517F /* SiteVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVersion.swift; sourceTree = ""; }; 031BF9542AB25AFB00F4517F /* SiteVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVersionTests.swift; sourceTree = ""; }; - 031F95562B5C7FF20069C244 /* InstanceStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceStatsView.swift; sourceTree = ""; }; + 031F95562B5C7FF20069C244 /* InstanceDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceDetailsView.swift; sourceTree = ""; }; 032109462AA7C3FC00912DFC /* CommunityLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLabelView.swift; sourceTree = ""; }; 032109482AA7C41800912DFC /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; 032DD2FC2AC3594B00F1B33D /* LinkAttatchmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAttatchmentView.swift; sourceTree = ""; }; @@ -1344,7 +1344,7 @@ isa = PBXGroup; children = ( 03A54C312B5331F30064CCDE /* InstanceView.swift */, - 031F95562B5C7FF20069C244 /* InstanceStatsView.swift */, + 031F95562B5C7FF20069C244 /* InstanceDetailsView.swift */, ); path = Instance; sourceTree = ""; @@ -3379,7 +3379,7 @@ CDC1C93C2A7AA76000072E3D /* InternetSpeed.swift in Sources */, 50EC39B22A346DDC00E014C2 /* URLHandler.swift in Sources */, 63F0C7BF2A058EDE00A18C5D /* Get Correct URL to Endpoint.swift in Sources */, - 031F95572B5C7FF20069C244 /* InstanceStatsView.swift in Sources */, + 031F95572B5C7FF20069C244 /* InstanceDetailsView.swift in Sources */, 632E8EE827EE63DB007E8D75 /* DownvoteButtonView.swift in Sources */, 50D61E5B2AA32B9400A926EC /* APISession.swift in Sources */, CDCBD72B2A8EC0A800387A2C /* Instance Summary.swift in Sources */, diff --git a/Mlem/Views/Shared/Instance/InstanceStatsView.swift b/Mlem/Views/Shared/Instance/InstanceDetailsView.swift similarity index 87% rename from Mlem/Views/Shared/Instance/InstanceStatsView.swift rename to Mlem/Views/Shared/Instance/InstanceDetailsView.swift index e03ddb24f..c9e850e1f 100644 --- a/Mlem/Views/Shared/Instance/InstanceStatsView.swift +++ b/Mlem/Views/Shared/Instance/InstanceDetailsView.swift @@ -7,9 +7,7 @@ import SwiftUI -struct InstanceStatsView: View { - @AppStorage("developerMode") var developerMode: Bool = false - +struct InstanceDetailsView: View { @State var showingSlurRegex: Bool = false let instance: InstanceModel @@ -90,14 +88,6 @@ struct InstanceStatsView: View { systemImage: Icons.federation, value: instance.federates ?? false ) - if developerMode, let signedFetch = instance.federationSignedFetch { - Divider() - settingRow( - "Federation Signed Fetch", - systemImage: Icons.developerMode, - value: signedFetch - ) - } } .frame(maxWidth: .infinity) .background(Color(uiColor: .secondarySystemGroupedBackground)) @@ -154,23 +144,36 @@ struct InstanceStatsView: View { systemImage: Icons.filterFill, value: instance.slurFilterRegex != nil ) - if developerMode, let regex = instance.slurFilterString { + if let regex = instance.slurFilterString { Divider() - Text(showingSlurRegex ? regex : "Tap to show slur filter regex") - .textSelection(.enabled) - .font(.footnote) - .foregroundStyle(.secondary) - .padding(12) - .onTapGesture { - withAnimation { - showingSlurRegex.toggle() - } + VStack(alignment: .leading, spacing: 2) { + if showingSlurRegex { + Text(regex) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } else { + Text("Tap to slow slur filter regex.") + Label( + "This probably contains foul language.", + systemImage: Icons.warning + ) + .foregroundStyle(.orange) + } + } + .font(.footnote) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + .contentShape(.rect) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.2)) { + showingSlurRegex.toggle() } + } } - if developerMode, let feedType = instance.defaultFeedType { + if let feedType = instance.defaultFeedType { Divider() settingRow( - "Default Feed Type", + "Default Feed Type (Desktop)", systemImage: Icons.feeds, value: feedType.rawValue ) diff --git a/Mlem/Views/Shared/Instance/InstanceView.swift b/Mlem/Views/Shared/Instance/InstanceView.swift index ee9f90fab..edd7118de 100644 --- a/Mlem/Views/Shared/Instance/InstanceView.swift +++ b/Mlem/Views/Shared/Instance/InstanceView.swift @@ -126,7 +126,7 @@ struct InstanceView: View { case .details: if instance.userCount != nil { VStack(spacing: 0) { - InstanceStatsView(instance: instance) + InstanceDetailsView(instance: instance) .padding(.vertical, 16) .background(Color(uiColor: .systemGroupedBackground)) if colorScheme == .light { From 49fe30d505055edb75609c855b8b4557520ebcc5 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Wed, 31 Jan 2024 22:04:12 +0000 Subject: [PATCH 39/40] Bug fix --- Mlem/Models/Trackers/ContentTracker.swift | 2 +- Mlem/Views/Tabs/Search/SearchModel.swift | 1 - .../Views/Tabs/Search/SearchResultsView.swift | 8 ----- Mlem/Views/Tabs/Search/SearchView.swift | 36 +++++++++---------- 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/Mlem/Models/Trackers/ContentTracker.swift b/Mlem/Models/Trackers/ContentTracker.swift index d69a45891..fecd13535 100644 --- a/Mlem/Models/Trackers/ContentTracker.swift +++ b/Mlem/Models/Trackers/ContentTracker.swift @@ -68,7 +68,7 @@ class ContentTracker: ObservableObject { } currentTask = Task(priority: .userInitiated) { [self] in do { - let items = try await self.loadItems(page) + let items = try await self.loadItems(1) RunLoop.main.perform { [self] in self.replaceAll(with: items) } diff --git a/Mlem/Views/Tabs/Search/SearchModel.swift b/Mlem/Views/Tabs/Search/SearchModel.swift index 9fe05f367..258501722 100644 --- a/Mlem/Views/Tabs/Search/SearchModel.swift +++ b/Mlem/Views/Tabs/Search/SearchModel.swift @@ -79,7 +79,6 @@ class SearchModel: ObservableObject { } return try await searchUsers(page: page) case .instances: - print("SHORTCUT", SearchModel.allInstances.count) return searchInstances(page: page) } } diff --git a/Mlem/Views/Tabs/Search/SearchResultsView.swift b/Mlem/Views/Tabs/Search/SearchResultsView.swift index 71b186522..012a798eb 100644 --- a/Mlem/Views/Tabs/Search/SearchResultsView.swift +++ b/Mlem/Views/Tabs/Search/SearchResultsView.swift @@ -44,14 +44,6 @@ struct SearchResultsView: View { BubblePicker(SearchTab.allCases, selected: $searchModel.searchTab) { Text($0.label) } -// Group { -// if contentTracker.isLoading && contentTracker.page == 1 && !shouldLoad { -// ProgressView() -// .padding(.trailing) -// .transition(.opacity) -// } -// } -// .animation(.default, value: contentTracker.isLoading) } } } diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index 8f4c00fa3..ea987e42f 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -76,27 +76,25 @@ struct SearchView: View { .navigationSearchBarHiddenWhenScrolling(false) .autocorrectionDisabled(true) .textInputAutocapitalization(.never) - .onAppear { + .task(priority: .background) { if SearchModel.allInstances.isEmpty { - Task(priority: .background) { - var instances: [InstanceModel] = [] - do { - instances = try await apiClient.fetchInstanceList().map { InstanceModel(from: $0) } - DispatchQueue.main.async { - SearchModel.allInstances = instances - } - } catch { - print("Error while loading instance stubs: \(error.localizedDescription)") - errorHandler.handle(error) - } - do { - try await recentSearchesTracker.reloadRecentSearches( - accountId: appState.currentActiveAccount?.stableIdString, - instances: instances - ) - } catch { - print("Error while loading recent searches: \(error.localizedDescription)") + var instances: [InstanceModel] = [] + do { + instances = try await apiClient.fetchInstanceList().map { InstanceModel(from: $0) } + DispatchQueue.main.async { + SearchModel.allInstances = instances } + } catch { + print("Error while loading instance stubs: \(error.localizedDescription)") + errorHandler.handle(error) + } + do { + try await recentSearchesTracker.reloadRecentSearches( + accountId: appState.currentActiveAccount?.stableIdString, + instances: instances + ) + } catch { + print("Error while loading recent searches: \(error.localizedDescription)") } } } From b1d90e7f7cbdff3432b88ac9fdd55ae295e7bbe9 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Wed, 31 Jan 2024 22:07:43 +0000 Subject: [PATCH 40/40] Fix another bug --- Mlem/Views/Shared/BadgeView.swift | 32 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Mlem/Views/Shared/BadgeView.swift b/Mlem/Views/Shared/BadgeView.swift index c726c39c5..8551726df 100644 --- a/Mlem/Views/Shared/BadgeView.swift +++ b/Mlem/Views/Shared/BadgeView.swift @@ -76,22 +76,24 @@ struct BadgeView: View { self.label = "Unsupported Badge" if let host = url.host(), host == "img.shields.io" { let path = url.pathComponents - decodeBadgeType(path) - decodeLabel(path[2]) - if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) { - if let parameters = components.queryItems { - for parameter in parameters { - switch parameter.name { - case "logo": - if let value = parameter.value { - decodeLogo(name: value) + if path.count >= 3 { + decodeBadgeType(path) + decodeLabel(path[2]) + if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + if let parameters = components.queryItems { + for parameter in parameters { + switch parameter.name { + case "logo": + if let value = parameter.value { + decodeLogo(name: value) + } + case "color": + if let value = parameter.value { + decodeColor(value) + } + default: + break } - case "color": - if let value = parameter.value { - decodeColor(value) - } - default: - break } } }