From 20a92ec338d06f7100e9027c5c7c4eca7ed6984f Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:11:55 +0000 Subject: [PATCH 1/5] Moderator "Show All Actions in Feed" setting (#1500) Co-authored-by: Eric Andrews --- .../User Settings/CodableSettings.swift | 8 +++---- .../User Settings/Settings.swift | 3 +++ .../Post1Providing+Extensions.swift | 18 ++++++++++----- .../Tabs/Feeds/Feed Posts/FeedPostView.swift | 3 ++- .../Tabs/Feeds/Feed Posts/TilePostView.swift | 3 ++- .../Tabs/Settings/ModeratorSettingsView.swift | 22 ++++++++++++++++++- .../ExpandedPost/ExpandedPostView.swift | 2 +- Mlem/App/Views/Shared/PostEllipsisMenus.swift | 2 +- Mlem/Localizable.xcstrings | 6 +++++ 9 files changed, 52 insertions(+), 15 deletions(-) diff --git a/Mlem/App/Configuration/User Settings/CodableSettings.swift b/Mlem/App/Configuration/User Settings/CodableSettings.swift index a52d36a0f..f2314ac3a 100644 --- a/Mlem/App/Configuration/User Settings/CodableSettings.swift +++ b/Mlem/App/Configuration/User Settings/CodableSettings.swift @@ -55,7 +55,7 @@ struct CodableSettings: Codable { var links_readerMode: Bool var links_tappableLinksDisplayMode: TappableLinksDisplayMode var menus_allModActions: Bool - var menus_modActionGrouping: String // TODO: pending mod actions + var menus_modActionGrouping: ModeratorActionGrouping var post_defaultSort: ApiSortType var post_fallbackSort: ApiSortType var post_limitImageHeight: Bool @@ -132,7 +132,7 @@ struct CodableSettings: Codable { self.links_readerMode = try container.decodeIfPresent(Bool.self, forKey: .links_readerMode) ?? false self.links_tappableLinksDisplayMode = try container.decodeIfPresent(TappableLinksDisplayMode.self, forKey: .links_tappableLinksDisplayMode) ?? .contextual self.menus_allModActions = try container.decodeIfPresent(Bool.self, forKey: .menus_allModActions) ?? false - self.menus_modActionGrouping = try container.decodeIfPresent(String.self, forKey: .menus_modActionGrouping) ?? "none" + self.menus_modActionGrouping = try container.decodeIfPresent(ModeratorActionGrouping.self, forKey: .menus_modActionGrouping) ?? .divider self.post_defaultSort = try container.decodeIfPresent(ApiSortType.self, forKey: .post_defaultSort) ?? .hot self.post_fallbackSort = try container.decodeIfPresent(ApiSortType.self, forKey: .post_fallbackSort) ?? .hot self.post_limitImageHeight = try container.decodeIfPresent(Bool.self, forKey: .post_limitImageHeight) ?? true @@ -209,8 +209,8 @@ struct CodableSettings: Codable { self.links_openInBrowser = settings.openLinksInBrowser self.links_readerMode = settings.openLinksInReaderMode self.links_tappableLinksDisplayMode = settings.tappableLinksDisplayMode - self.menus_allModActions = false - self.menus_modActionGrouping = "none" + self.menus_allModActions = settings.showAllModActions + self.menus_modActionGrouping = settings.moderatorActionGrouping self.post_defaultSort = settings.defaultPostSort self.post_fallbackSort = settings.fallbackPostSort self.post_limitImageHeight = true diff --git a/Mlem/App/Configuration/User Settings/Settings.swift b/Mlem/App/Configuration/User Settings/Settings.swift index e8af87973..df089f2c7 100644 --- a/Mlem/App/Configuration/User Settings/Settings.swift +++ b/Mlem/App/Configuration/User Settings/Settings.swift @@ -82,6 +82,7 @@ class Settings: ObservableObject { @AppStorage("navigation.swipeAnywhere") var swipeAnywhereToNavigate: Bool = false @AppStorage("menus.moderatorActionGrouping") var moderatorActionGrouping: ModeratorActionGrouping = .divider + @AppStorage("menus.allModActions") var showAllModActions: Bool = false var codable: CodableSettings { .init(from: self) } @@ -149,5 +150,7 @@ class Settings: ObservableObject { autoBypassImageProxy = settings.privacy_autoBypassImageProxy sidebarVisibleByDefault = settings.navigation_sidebarVisibleByDefault swipeAnywhereToNavigate = settings.navigation_swipeAnywhere + moderatorActionGrouping = settings.menus_modActionGrouping + showAllModActions = settings.menus_allModActions } } diff --git a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift index e6b8c02f0..42850f7f0 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift @@ -131,6 +131,7 @@ extension Post1Providing { func allMenuActions( expanded: Bool = false, feedback: Set = [.haptic, .toast], + showAllActions: Bool = true, commentTreeTracker: CommentTreeTracker? = nil ) -> [any Action] { basicMenuActions(feedback: feedback, commentTreeTracker: commentTreeTracker) @@ -139,7 +140,7 @@ extension Post1Providing { appearance: .init(label: "Moderation...", color: Palette.main.moderation, icon: Icons.moderation), displayMode: Settings.main.moderatorActionGrouping == .divider || expanded ? .section : .disclosure ) { - moderatorMenuActions(feedback: feedback) + moderatorMenuActions(feedback: feedback, showAllActions: showAllActions) } } } @@ -178,12 +179,17 @@ extension Post1Providing { } @ActionBuilder - func moderatorMenuActions(feedback: Set = [.haptic, .toast]) -> [any Action] { - pinToCommunityAction(feedback: feedback, verboseTitle: api.isAdmin) - if api.isAdmin { - pinToInstanceAction(feedback: feedback) + func moderatorMenuActions( + feedback: Set = [.haptic, .toast], + showAllActions: Bool = true + ) -> [any Action] { + if showAllActions || Settings.main.showAllModActions { + pinToCommunityAction(feedback: feedback, verboseTitle: api.isAdmin) + if api.isAdmin { + pinToInstanceAction(feedback: feedback) + } + lockAction(feedback: feedback) } - lockAction(feedback: feedback) if let self2, !isOwnPost { self2.removeAction() banActions() diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/FeedPostView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/FeedPostView.swift index afa0bb5c4..e6c8afc1c 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/FeedPostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/FeedPostView.swift @@ -13,6 +13,7 @@ import SwiftUI struct FeedPostView: View { @Setting(\.postSize) private var postSize + @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? @Environment(Palette.self) private var palette let post: any Post1Providing @@ -23,7 +24,7 @@ struct FeedPostView: View { .contentShape(.interaction, .rect) .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing)) .quickSwipes(post.swipeActions(behavior: postSize.swipeBehavior)) - .contextMenu { post.allMenuActions() } + .contextMenu { post.allMenuActions(showAllActions: false, commentTreeTracker: commentTreeTracker) } .paletteBorder(cornerRadius: postSize.swipeBehavior.cornerRadius) } diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/TilePostView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/TilePostView.swift index 1836f87be..e9f403ef7 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/TilePostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/TilePostView.swift @@ -12,6 +12,7 @@ import NukeUI import SwiftUI struct TilePostView: View { + @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? @Environment(Palette.self) var palette: Palette @Environment(\.communityContext) var communityContext: (any Community1Providing)? @Environment(\.parentFrameWidth) var parentFrameWidth: CGFloat @@ -93,7 +94,7 @@ struct TilePostView: View { var score: some View { Menu { - ForEach(post.allMenuActions(), id: \.id) { action in + ForEach(post.allMenuActions(showAllActions: false, commentTreeTracker: commentTreeTracker), id: \.id) { action in MenuButton(action: action) } } label: { diff --git a/Mlem/App/Views/Root/Tabs/Settings/ModeratorSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/ModeratorSettingsView.swift index b866c3904..47a6356e9 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/ModeratorSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/ModeratorSettingsView.swift @@ -9,6 +9,7 @@ import SwiftUI struct ModeratorSettingsView: View { @Setting(\.moderatorActionGrouping) var moderatorActionGrouping + @Setting(\.showAllModActions) var showAllModActions var body: some View { Form { @@ -27,11 +28,30 @@ struct ModeratorSettingsView: View { Text("Separate moderator actions using...") .textCase(nil) } + Section { + Toggle("Show All Actions in Feed", isOn: $showAllModActions) + } footer: { + Text("When disabled, some moderator actions will only be accessible from the post page.") + } } .navigationTitle("Moderation") } } -enum ModeratorActionGrouping: String { +enum ModeratorActionGrouping: String, Codable { case divider, disclosureGroup, separateMenu + + init?(rawValue: String) { + switch rawValue { + // Decode v1 case + case "none", "divider": + self = .divider + case "disclosureGroup": + self = .disclosureGroup + case "separateMenu": + self = .separateMenu + default: + return nil + } + } } diff --git a/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView.swift b/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView.swift index e9e7d68c1..823c1c12d 100644 --- a/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView.swift +++ b/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView.swift @@ -205,7 +205,7 @@ struct ExpandedPostView: View { } .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing)) .quickSwipes(post.swipeActions(behavior: .standard, commentTreeTracker: tracker)) - .contextMenu { post.allMenuActions() } + .contextMenu { post.allMenuActions(showAllActions: false, commentTreeTracker: tracker) } .paletteBorder(cornerRadius: PostSize.large.swipeBehavior.cornerRadius) .onTapGesture { withAnimation { diff --git a/Mlem/App/Views/Shared/PostEllipsisMenus.swift b/Mlem/App/Views/Shared/PostEllipsisMenus.swift index 871af4272..2324e3276 100644 --- a/Mlem/App/Views/Shared/PostEllipsisMenus.swift +++ b/Mlem/App/Views/Shared/PostEllipsisMenus.swift @@ -34,7 +34,7 @@ struct PostEllipsisMenus: View { } } else { EllipsisMenu(size: 24) { - post.allMenuActions(commentTreeTracker: commentTreeTracker) + post.allMenuActions(showAllActions: false, commentTreeTracker: commentTreeTracker) } } } diff --git a/Mlem/Localizable.xcstrings b/Mlem/Localizable.xcstrings index 0e94e522b..b793e14b9 100644 --- a/Mlem/Localizable.xcstrings +++ b/Mlem/Localizable.xcstrings @@ -1299,6 +1299,9 @@ }, "Show" : { + }, + "Show All Actions in Feed" : { + }, "Show Bot Accounts" : { @@ -1809,6 +1812,9 @@ }, "What is Federation?" : { + }, + "When disabled, some moderator actions will be hidden from the feed and will only be visible from when viewing a post page." : { + }, "Wrap Code Block Lines" : { From bdd39c79fa131e6ffa83ce1f55db126daa520049 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:24:54 +0000 Subject: [PATCH 2/5] Fix glide animation bug (#1502) --- Mlem/App/Views/Shared/CommentView.swift | 1 + Mlem/App/Views/Shared/Labels/FullyQualifiedLinkView.swift | 3 +++ Mlem/App/Views/Shared/ReplyView.swift | 1 + 3 files changed, 5 insertions(+) diff --git a/Mlem/App/Views/Shared/CommentView.swift b/Mlem/App/Views/Shared/CommentView.swift index 6be49db5b..db81f713c 100644 --- a/Mlem/App/Views/Shared/CommentView.swift +++ b/Mlem/App/Views/Shared/CommentView.swift @@ -87,6 +87,7 @@ struct CommentView: View { NavigationLink(.post(post)) { FooterLinkView(title: post.title, subtitle: comment.community_?.fullNameWithPrefix) } + .id("\(comment.id)_commment_footer") } if !compactComments { InteractionBarView( diff --git a/Mlem/App/Views/Shared/Labels/FullyQualifiedLinkView.swift b/Mlem/App/Views/Shared/Labels/FullyQualifiedLinkView.swift index 11744cf8d..ea021e9e4 100644 --- a/Mlem/App/Views/Shared/Labels/FullyQualifiedLinkView.swift +++ b/Mlem/App/Views/Shared/Labels/FullyQualifiedLinkView.swift @@ -17,6 +17,8 @@ struct FullyQualifiedLinkView: View { let showInstance: Bool let blurred: Bool + @State private var id = UUID() + init( entity: (any CommunityOrPersonStub & Profile2Providing)?, labelStyle: FullyQualifiedLabelStyle, @@ -48,5 +50,6 @@ struct FullyQualifiedLinkView: View { ) } .buttonStyle(.plain) + .id(id) } } diff --git a/Mlem/App/Views/Shared/ReplyView.swift b/Mlem/App/Views/Shared/ReplyView.swift index 5252fbcd8..4aeb45e8e 100644 --- a/Mlem/App/Views/Shared/ReplyView.swift +++ b/Mlem/App/Views/Shared/ReplyView.swift @@ -31,6 +31,7 @@ struct ReplyView: View { NavigationLink(.post(reply.post)) { FooterLinkView(title: reply.post.title, subtitle: reply.community.fullNameWithPrefix) } + .id("\(reply.id)_reply_footer") InteractionBarView( reply: reply, configuration: InteractionBarTracker.main.replyInteractionBar From 35f9def7832c3462a446427d930df7e9db8aa9e7 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:38:17 +0000 Subject: [PATCH 3/5] Admin, Bot, Developer flairs in PersonView (#1506) --- Mlem.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- Mlem/App/Views/Pages/Person/PersonView.swift | 48 +++++++++++++++++++ .../Components/SquircleLabelStyle.swift | 1 + Mlem/Localizable.xcstrings | 3 ++ 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 612e7ec32..a640dd92e 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -2946,8 +2946,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mlemgroup/MlemMiddleware"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.52.0; + kind = upToNextMinorVersion; + minimumVersion = 0.53.0; }; }; CDE4AC402CA3706400981010 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { diff --git a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 53c8be7e0..47278bd4e 100644 --- a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mlemgroup/MlemMiddleware", "state" : { - "revision" : "51db2875046e216fb01c8233cc43eadd147dd240", - "version" : "0.52.0" + "revision" : "a4dc838d0ff82ea76b71ee9e9568743d957e15a0", + "version" : "0.53.0" } }, { diff --git a/Mlem/App/Views/Pages/Person/PersonView.swift b/Mlem/App/Views/Pages/Person/PersonView.swift index f3849fe80..79d811c84 100644 --- a/Mlem/App/Views/Pages/Person/PersonView.swift +++ b/Mlem/App/Views/Pages/Person/PersonView.swift @@ -5,6 +5,7 @@ // Created by Sjmarf on 30/05/2024. // +import Flow import LemmyMarkdownUI import MlemMiddleware import SwiftUI @@ -36,10 +37,12 @@ struct PersonView: View { @State private var selectedContentType: PersonContentType = .all @State private var isAtTop: Bool = true @State var feedLoader: PersonContentFeedLoader? + @State var isAdmin: Bool let isProfileTab: Bool init(person: AnyPerson, isProfileTab: Bool = false) { self._person = .init(wrappedValue: person) + self._isAdmin = .init(wrappedValue: person.wrappedValue.isAdmin_ ?? false) self.isProfileTab = isProfileTab if let person1 = person.wrappedValue as? any Person1Providing, person1.api === AppState.main.firstApi { @@ -114,6 +117,12 @@ struct PersonView: View { } return try await entity.upgrade() } + // This prevents the admin flair from disappearing if the `ContentLoader` + // switches from an external ApiClient to the active ApiClient, e.g. when + // navigating to `PersonView` from the administrator list in `InstanceView`. + if model.wrappedValue.isAdmin_ ?? false { + isAdmin = true + } } .navigationTitle(isAtTop ? "" : (person.wrappedValue.displayName_ ?? person.wrappedValue.name)) .navigationBarTitleDisplayMode(.inline) @@ -125,6 +134,7 @@ struct PersonView: View { VStack(spacing: 0) { VStack(spacing: Constants.main.standardSpacing) { ProfileHeaderView(person, fallback: .person) + flairsView(person: person) bio(person: person) } .padding([.horizontal], Constants.main.standardSpacing) @@ -175,6 +185,27 @@ struct PersonView: View { } } + @ViewBuilder + func flairsView(person: any Person) -> some View { + if person.isBot || person.isMlemDeveloper || isAdmin { + HFlow(spacing: Constants.main.halfSpacing) { + if person.isMlemDeveloper { + Label("Mlem Developer", systemImage: Icons.developerFlair) + .tint(palette.colorfulAccent(4)) + } + if isAdmin { + Label("\(person.host ?? "") Administrator", systemImage: Icons.adminFlair) + .tint(palette.administration) + } + if person.isBot { + Label("Bot Account", systemImage: Icons.botFlair) + .tint(palette.colorfulAccent(5)) + } + } + .labelStyle(FlairLabelStyle()) + } + } + @ViewBuilder func dateLabel(person: any Person) -> some View { ProfileDateView(profilable: person) @@ -233,3 +264,20 @@ struct PersonView: View { .padding([.horizontal, .bottom], Constants.main.standardSpacing) } } + +private struct FlairLabelStyle: LabelStyle { + @Environment(Palette.self) private var palette + + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 5) { + configuration.icon + .imageScale(.small) + configuration.title + } + .font(.footnote) + .padding(.vertical, 2) + .padding(.horizontal, 8) + .foregroundStyle(.tint) + .background(.tint.opacity(0.2), in: .capsule) + } +} diff --git a/Mlem/App/Views/Root/Tabs/Settings/Components/SquircleLabelStyle.swift b/Mlem/App/Views/Root/Tabs/Settings/Components/SquircleLabelStyle.swift index 651f56aaa..c8e202168 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/Components/SquircleLabelStyle.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/Components/SquircleLabelStyle.swift @@ -9,6 +9,7 @@ import SwiftUI struct SquircleLabelStyle: LabelStyle { @Environment(Palette.self) private var palette + func makeBody(configuration: Configuration) -> some View { HStack(alignment: .center, spacing: 16) { configuration.icon diff --git a/Mlem/Localizable.xcstrings b/Mlem/Localizable.xcstrings index b793e14b9..7a1b16865 100644 --- a/Mlem/Localizable.xcstrings +++ b/Mlem/Localizable.xcstrings @@ -30,6 +30,9 @@ } } } + }, + "%@ Administrator" : { + }, "%@ has been unresponsive recently." : { From 20837e46d426f3d309100f7016bb5d03cdbd2a2c Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:43:38 +0000 Subject: [PATCH 4/5] Remove & Purge Community (#1503) --- Mlem.xcodeproj/project.pbxproj | 4 ++++ Mlem/App/Models/Account/GuestAccount.swift | 1 + Mlem/App/Models/Account/UserAccount.swift | 1 + Mlem/App/Models/Action/BasicAction.swift | 8 +++++++ Mlem/App/Models/Session/UserSession.swift | 2 +- .../Comment1Providing+Extensions.swift | 4 ++-- .../Community1Providing+Extensions.swift | 6 +++++ .../Interactable1Providing+Extensions.swift | 16 -------------- .../Post1Providing+Extensions.swift | 4 ++-- .../RemovableProviding+Extensions.swift | 22 +++++++++++++++++++ .../Views/Pages/Community/CommunityView.swift | 21 +++++++++++++----- .../Pages/ContentRemovalEditorView.swift | 12 +++++----- .../Shared/Navigation/NavigationPage.swift | 16 ++++++++++++-- Mlem/Localizable.xcstrings | 3 +++ 14 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 Mlem/App/Utility/Extensions/Content Models/RemovableProviding+Extensions.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index a640dd92e..a2ca2022b 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 033FCB292C5E3933007B7CD1 /* IconSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB252C5E3933007B7CD1 /* IconSettingsView.swift */; }; 033FCB2A2C5E3933007B7CD1 /* AlternateIconCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB232C5E3933007B7CD1 /* AlternateIconCell.swift */; }; 033FCB3E2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033FCB3D2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift */; }; + 034690932D0F4D720073E664 /* RemovableProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034690922D0F4D720073E664 /* RemovableProviding+Extensions.swift */; }; 034B947F2C091EDD00039AF4 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B947E2C091EDD00039AF4 /* ProfileHeaderView.swift */; }; 034B94812C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B94802C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift */; }; 034B94832C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034B94822C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift */; }; @@ -505,6 +506,7 @@ 033FCB242C5E3933007B7CD1 /* AlternateIconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternateIconLabel.swift; sourceTree = ""; }; 033FCB252C5E3933007B7CD1 /* IconSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSettingsView.swift; sourceTree = ""; }; 033FCB3D2C5E7FA9007B7CD1 /* View+OutdatedFeedPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+OutdatedFeedPopup.swift"; sourceTree = ""; }; + 034690922D0F4D720073E664 /* RemovableProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RemovableProviding+Extensions.swift"; sourceTree = ""; }; 034B947E2C091EDD00039AF4 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = ""; }; 034B94802C09306D00039AF4 /* CommunityOrPersonStub+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityOrPersonStub+Extensions.swift"; sourceTree = ""; }; 034B94822C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MarkdownConfiguration+Extensions.swift"; sourceTree = ""; }; @@ -1763,6 +1765,7 @@ 03AFD0E42C3C14D50054B8AD /* InstanceStubProviding+Extensions.swift */, 035394962CA1AFAF00795AA5 /* InstanceStubProviding+Uptime.swift */, CDC199E92BE449790077B4F1 /* Interactable1Providing+Extensions.swift */, + 034690922D0F4D720073E664 /* RemovableProviding+Extensions.swift */, 0397D4852C6A24D2002C6CDC /* ReportableProviding+Extensions.swift */, 03CBD1922C61369A00E870BC /* Interactable2Providing+Extensions.swift */, 0389DDC22C38907C0005B808 /* Message1Providing+Extensions.swift */, @@ -2201,6 +2204,7 @@ CDB41E8E2C84CFA200BD2DE9 /* FixedImageView.swift in Sources */, 033F84C12C2AD072002E3EDF /* CommentWrapper.swift in Sources */, 034B94832C09340A00039AF4 /* MarkdownConfiguration+Extensions.swift in Sources */, + 034690932D0F4D720073E664 /* RemovableProviding+Extensions.swift in Sources */, 039F58952C7B618F00C61658 /* InboxSettingsView.swift in Sources */, CD4D58CF2B86DDEC00B82964 /* AccountSortMode.swift in Sources */, CDE1F18F2C63D75A008AF042 /* Settings.swift in Sources */, diff --git a/Mlem/App/Models/Account/GuestAccount.swift b/Mlem/App/Models/Account/GuestAccount.swift index 2d6467658..7839d5abb 100644 --- a/Mlem/App/Models/Account/GuestAccount.swift +++ b/Mlem/App/Models/Account/GuestAccount.swift @@ -71,6 +71,7 @@ class GuestAccount: Account { try container.encode(api.baseUrl, forKey: .instanceLink) } + @MainActor func update(instance: Instance3) { var shouldSave = false if avatar != instance.avatar { diff --git a/Mlem/App/Models/Account/UserAccount.swift b/Mlem/App/Models/Account/UserAccount.swift index 2ed4e2059..abb16590b 100644 --- a/Mlem/App/Models/Account/UserAccount.swift +++ b/Mlem/App/Models/Account/UserAccount.swift @@ -96,6 +96,7 @@ class UserAccount: Account, CommunityOrPersonStub { getKeychainId(actorId: actorId) } + @MainActor func update(person: Person4, instance: Instance3) { var shouldSave = false if avatar != person.avatar { diff --git a/Mlem/App/Models/Action/BasicAction.swift b/Mlem/App/Models/Action/BasicAction.swift index ea3acf39f..7455e0e07 100644 --- a/Mlem/App/Models/Action/BasicAction.swift +++ b/Mlem/App/Models/Action/BasicAction.swift @@ -53,4 +53,12 @@ struct BasicAction: Action { } } } + + func disabled(_ value: Bool) -> BasicAction { + var new = self + if value { + new.callback = nil + } + return new + } } diff --git a/Mlem/App/Models/Session/UserSession.swift b/Mlem/App/Models/Session/UserSession.swift index c5b59e8b7..0eeafa4e0 100644 --- a/Mlem/App/Models/Session/UserSession.swift +++ b/Mlem/App/Models/Session/UserSession.swift @@ -38,7 +38,7 @@ class UserSession: Session { try await self.api.fetchSiteVersion(task: Task { let (person, instance, blocks) = try await self.api.getMyPerson() if let person { - self.account.update(person: person, instance: instance) + await self.account.update(person: person, instance: instance) self.person = person } self.blocks = blocks diff --git a/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift index e88fb1d76..b0118c0a9 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift @@ -98,7 +98,7 @@ extension Comment1Providing { @ActionBuilder func moderatorMenuActions(feedback: Set = [.haptic, .toast]) -> [any Action] { if let self2, !isOwnComment { - self2.removeAction() + self2.removeAction().disabled(!canModerate) banActions() } if api.isAdmin { @@ -126,7 +126,7 @@ extension Comment1Providing { case .share: shareAction() case .selectText: selectTextAction() case .report: reportAction(communityContext: communityContext) - case .remove: removeAction() + case .remove: removeAction().disabled(!canModerate) } } diff --git a/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift index 90ba08908..46a5cd857 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift @@ -121,6 +121,12 @@ extension Community1Providing { copyNameAction() shareAction() blockAction(feedback: feedback) + if api.isAdmin { + ActionGroup { + removeAction() + purgeAction() + } + } } func swipeActions(behavior: SwipeBehavior) -> SwipeConfiguration { diff --git a/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift index eb56e2fcc..ec66b1087 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift @@ -86,14 +86,6 @@ extension Interactable1Providing { } } } - - func showRemoveSheet() { - guard let self2 else { - print("DEBUG no self2 found in toggleRemoved!") - return - } - NavigationModel.main.openSheet(.remove(self2)) - } // MARK: Counters @@ -173,14 +165,6 @@ extension Interactable1Providing { ) } - func removeAction(feedback: Set = []) -> BasicAction { - .init( - id: "remove\(uid)", - appearance: .remove(isOn: self2?.removed ?? false, isInProgress: !(self2?.removedManager.isInSync ?? true)), - callback: api.canInteract && (self2?.canModerate ?? false) ? showRemoveSheet : nil - ) - } - func banActions() -> [any Action] { let isModerator: Bool if let myPerson = api.myPerson, let community = community_ { diff --git a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift index 42850f7f0..77cefda9c 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift @@ -191,7 +191,7 @@ extension Post1Providing { lockAction(feedback: feedback) } if let self2, !isOwnPost { - self2.removeAction() + self2.removeAction().disabled(!canModerate) banActions() } if api.isAdmin { @@ -225,7 +225,7 @@ extension Post1Providing { // in parenthesis, but the pre-commit hook removed the paranthesis // swiftlint:disable:next void_function_in_ternary case .pin: api.isAdmin ? pinAction(feedback: feedback) : pinToCommunityAction(feedback: feedback) - case .remove: removeAction(feedback: feedback) + case .remove: removeAction(feedback: feedback).disabled(!canModerate) } } diff --git a/Mlem/App/Utility/Extensions/Content Models/RemovableProviding+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/RemovableProviding+Extensions.swift new file mode 100644 index 000000000..165361a28 --- /dev/null +++ b/Mlem/App/Utility/Extensions/Content Models/RemovableProviding+Extensions.swift @@ -0,0 +1,22 @@ +// +// RemovableProviding+Extensions.swift +// Mlem +// +// Created by Sjmarf on 2024-12-15. +// + +import MlemMiddleware + +extension RemovableProviding { + func showRemoveSheet() { + NavigationModel.main.openSheet(.remove(self)) + } + + func removeAction(feedback: Set = []) -> BasicAction { + .init( + id: "remove\(uid)", + appearance: .remove(isOn: removed, isInProgress: !removedManager.isInSync), + callback: api.canInteract ? showRemoveSheet : nil + ) + } +} diff --git a/Mlem/App/Views/Pages/Community/CommunityView.swift b/Mlem/App/Views/Pages/Community/CommunityView.swift index d8c5ba3ac..3def5d5cd 100644 --- a/Mlem/App/Views/Pages/Community/CommunityView.swift +++ b/Mlem/App/Views/Pages/Community/CommunityView.swift @@ -128,12 +128,23 @@ struct CommunityView: View { @ViewBuilder func postsTab(community: any Community, postFeedLoader: CommunityPostFeedLoader) -> some View { - PostGridView(postFeedLoader: postFeedLoader) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - FeedSortPicker(feedLoader: postFeedLoader) - } + if community.removed { + VStack(spacing: Constants.main.standardSpacing) { + Image(systemName: Icons.remove) + .font(.title) + Text("This community has been removed.") + .fontWeight(.semibold) } + .foregroundStyle(palette.warning) + .padding(.top, Constants.main.doubleSpacing) + } else { + PostGridView(postFeedLoader: postFeedLoader) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + FeedSortPicker(feedLoader: postFeedLoader) + } + } + } } @ViewBuilder diff --git a/Mlem/App/Views/Pages/ContentRemovalEditorView.swift b/Mlem/App/Views/Pages/ContentRemovalEditorView.swift index e5266cc1e..8b7a48f30 100644 --- a/Mlem/App/Views/Pages/ContentRemovalEditorView.swift +++ b/Mlem/App/Views/Pages/ContentRemovalEditorView.swift @@ -17,18 +17,18 @@ struct ContentRemovalEditorView: View { case remove, restore } - let target: any Interactable2Providing + let target: any RemovableProviding @State var mode: Mode - @State var community: any Community + @State var community: (any Community)? @State var reason: String = "" @FocusState var reasonFocused: Bool @State var presentationSelection: PresentationDetent = .large - init(target: any Interactable2Providing) { + init(target: any RemovableProviding) { self.target = target self._mode = .init(wrappedValue: target.removed ? .restore : .remove) - self._community = .init(wrappedValue: target.community) + self._community = .init(wrappedValue: (target as? any Interactable2Providing)?.community) } var body: some View { @@ -41,7 +41,9 @@ struct ContentRemovalEditorView: View { Section { ReasonShortcutView(reason: $reason) } - RulesListView(model: community, reason: $reason) + if let community { + RulesListView(model: community, reason: $reason) + } if let instance = appState.firstSession.instance { RulesListView(model: instance, reason: $reason) } diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift index 9216f3a61..74d0f95ca 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift @@ -37,7 +37,7 @@ enum NavigationPage: Hashable { case createComment(_ context: CommentEditorView.Context, commentTreeTracker: CommentTreeTracker? = nil) case editComment(_ comment: Comment2, context: CommentEditorView.Context?) case report(_ interactable: ReportableHashWrapper, community: AnyCommunity? = nil) - case remove(_ interactable: Interactable2HashWrapper) + case remove(_ removable: RemovableHashWrapper) case purge(_ purgable: PurgableHashWrapper) case ban(_ person: AnyPerson, isBannedFromCommunity: Bool, shouldBan: Bool, community: AnyCommunity?) case createPost( @@ -211,7 +211,7 @@ enum NavigationPage: Hashable { return report(.init(wrappedValue: interactable), community: anyCommunity) } - static func remove(_ interactable: any Interactable2Providing) -> NavigationPage { + static func remove(_ interactable: any RemovableProviding) -> NavigationPage { remove(.init(wrappedValue: interactable)) } @@ -313,6 +313,18 @@ struct ReportableHashWrapper: Hashable { } } +struct RemovableHashWrapper: Hashable { + var wrappedValue: any RemovableProviding + + func hash(into hasher: inout Hasher) { + hasher.combine(wrappedValue.hashValue) + } + + static func == (lhs: RemovableHashWrapper, rhs: RemovableHashWrapper) -> Bool { + lhs.hashValue == rhs.hashValue + } +} + struct PurgableHashWrapper: Hashable { var wrappedValue: any PurgableProviding diff --git a/Mlem/Localizable.xcstrings b/Mlem/Localizable.xcstrings index 7a1b16865..54a4662fb 100644 --- a/Mlem/Localizable.xcstrings +++ b/Mlem/Localizable.xcstrings @@ -1594,6 +1594,9 @@ } } } + }, + "This community has been removed." : { + }, "This community likely contains graphic or explicit content." : { From 45ed9d0de12588bf095c98c1b8d48ce343d19a34 Mon Sep 17 00:00:00 2001 From: Sjmarf <78750526+Sjmarf@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:49:21 +0000 Subject: [PATCH 5/5] Sort Picker Changes (#1494) --- Mlem.xcodeproj/project.pbxproj | 20 ++++ .../User Settings/PinnedSortTracker.swift | 29 +++++ .../Definitions/PersistenceRepository.swift | 11 ++ .../Extensions/AVPlayer+Extensions.swift | 2 +- .../Extensions/ApiSortType+Extensions.swift | 100 ++++++++++++----- .../AdvancedSortView+SortButton.swift | 95 ++++++++++++++++ .../Pages/Community/AdvancedSortView.swift | 104 ++++++++++++++++++ .../Community/CommunitySearchSortPicker.swift | 45 ++++++++ .../Pages/Community/FeedSortPicker.swift | 94 +++++++++------- .../Views/Pages/Community/TopSortPicker.swift | 79 +++++++++++++ Mlem/App/Views/Root/ContentView.swift | 11 +- Mlem/App/Views/Root/MlemApp.swift | 2 +- .../Tabs/Settings/SortingSettingsView.swift | 4 +- .../Images/Core/Animated/VideoView.swift | 6 +- .../Navigation/NavigationPage+View.swift | 2 + .../Shared/Navigation/NavigationPage.swift | 7 +- .../Search/SearchView+FiltersView.swift | 25 ++--- Mlem/Localizable.xcstrings | 44 +++----- 18 files changed, 557 insertions(+), 123 deletions(-) create mode 100644 Mlem/App/Configuration/User Settings/PinnedSortTracker.swift create mode 100644 Mlem/App/Views/Pages/Community/AdvancedSortView+SortButton.swift create mode 100644 Mlem/App/Views/Pages/Community/AdvancedSortView.swift create mode 100644 Mlem/App/Views/Pages/Community/CommunitySearchSortPicker.swift create mode 100644 Mlem/App/Views/Pages/Community/TopSortPicker.swift diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index a2ca2022b..1914cdea6 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -126,6 +126,10 @@ 0369B3562BFA6824001EFEDF /* InboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0369B3552BFA6824001EFEDF /* InboxView.swift */; }; 0369B35D2BFB86E3001EFEDF /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0369B35A2BFB86E3001EFEDF /* Account.swift */; }; 036CC3AF2B8145C30098B6A1 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036CC3AE2B8145C30098B6A1 /* AppState.swift */; }; + 036ED6792D0AF3740018E5EA /* PinnedSortTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED6782D0AF3740018E5EA /* PinnedSortTracker.swift */; }; + 036ED67B2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED67A2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift */; }; + 036ED67D2D0B006C0018E5EA /* TopSortPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED67C2D0B006C0018E5EA /* TopSortPicker.swift */; }; + 036ED67F2D0B9A520018E5EA /* CommunitySearchSortPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED67E2D0B9A520018E5EA /* CommunitySearchSortPicker.swift */; }; 036ED6832D0C483B0018E5EA /* Profile2Providing+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036ED6822D0C483B0018E5EA /* Profile2Providing+Extensions.swift */; }; 037331A42C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037331A32C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift */; }; 037386472BDAFE81007492B5 /* LemmyMarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 037386462BDAFE81007492B5 /* LemmyMarkdownUI */; }; @@ -157,6 +161,7 @@ 0389DDDB2C3AB6340005B808 /* ActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */; }; 0391E0F92CFF17AE0040CCA8 /* ProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */; }; 0391E0FB2D0066240040CCA8 /* ImageUploadMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */; }; + 0391E0FE2D05B2DF0040CCA8 /* AdvancedSortView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0391E0FD2D05B2DF0040CCA8 /* AdvancedSortView.swift */; }; 0397D4602C66113F002C6CDC /* CommentBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D45F2C66113F002C6CDC /* CommentBodyView.swift */; }; 0397D4622C676B46002C6CDC /* ApiSortType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4612C676B46002C6CDC /* ApiSortType+Extensions.swift */; }; 0397D4642C676CA8002C6CDC /* FeedSortPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */; }; @@ -553,6 +558,10 @@ 0369B3552BFA6824001EFEDF /* InboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxView.swift; sourceTree = ""; }; 0369B35A2BFB86E3001EFEDF /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; 036CC3AE2B8145C30098B6A1 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + 036ED6782D0AF3740018E5EA /* PinnedSortTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedSortTracker.swift; sourceTree = ""; }; + 036ED67A2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdvancedSortView+SortButton.swift"; sourceTree = ""; }; + 036ED67C2D0B006C0018E5EA /* TopSortPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSortPicker.swift; sourceTree = ""; }; + 036ED67E2D0B9A520018E5EA /* CommunitySearchSortPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunitySearchSortPicker.swift; sourceTree = ""; }; 036ED6822D0C483B0018E5EA /* Profile2Providing+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile2Providing+Extensions.swift"; sourceTree = ""; }; 037331A32C9CB12D00C826E1 /* EnvironmentValues+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+Extensions.swift"; sourceTree = ""; }; 037658DE2BE7D9EF00F4DD4D /* Community1Providing+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Community1Providing+Extensions.swift"; sourceTree = ""; }; @@ -583,6 +592,7 @@ 0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBuilder.swift; sourceTree = ""; }; 0391E0F82CFF17AE0040CCA8 /* ProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingsView.swift; sourceTree = ""; }; 0391E0FA2D0066240040CCA8 /* ImageUploadMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadMenu.swift; sourceTree = ""; }; + 0391E0FD2D05B2DF0040CCA8 /* AdvancedSortView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSortView.swift; sourceTree = ""; }; 0397D45F2C66113F002C6CDC /* CommentBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBodyView.swift; sourceTree = ""; }; 0397D4612C676B46002C6CDC /* ApiSortType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApiSortType+Extensions.swift"; sourceTree = ""; }; 0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedSortPicker.swift; sourceTree = ""; }; @@ -992,6 +1002,10 @@ 033FCAF32C59843E007B7CD1 /* CommunityView.swift */, 03049A212C650B2C00FF6889 /* CommunityDetailsView.swift */, 0397D4632C676CA8002C6CDC /* FeedSortPicker.swift */, + 036ED67C2D0B006C0018E5EA /* TopSortPicker.swift */, + 036ED67E2D0B9A520018E5EA /* CommunitySearchSortPicker.swift */, + 0391E0FD2D05B2DF0040CCA8 /* AdvancedSortView.swift */, + 036ED67A2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift */, ); path = Community; sourceTree = ""; @@ -1899,6 +1913,7 @@ CDE1F18E2C63D75A008AF042 /* Settings.swift */, CDE1F19B2C63E2EB008AF042 /* SettingPropertyWrapper.swift */, 0315B1C92C767847006D4F82 /* InteractionBarTracker.swift */, + 036ED6782D0AF3740018E5EA /* PinnedSortTracker.swift */, CDD4A09B2C8A122F0001AD1A /* CodableSettings.swift */, ); path = "User Settings"; @@ -2194,6 +2209,7 @@ CD7928232C73CBA400FA712D /* TileScoreView.swift in Sources */, 03AF91E12C1B25DE00E56644 /* UIDevice+Extensions.swift in Sources */, 0389DDC52C38917A0005B808 /* InboxItemProviding+Extensions.swift in Sources */, + 036ED67B2D0B004D0018E5EA /* AdvancedSortView+SortButton.swift in Sources */, CD13CC5B2C588B34001AF428 /* WebView.swift in Sources */, CDB41E8C2C84CED500BD2DE9 /* FixedImageLoader.swift in Sources */, 039EFEC32BEEBEE0003AC372 /* LoginInstancePickerView.swift in Sources */, @@ -2225,6 +2241,7 @@ 03134A522BEAD69F002662CC /* SettingsPage.swift in Sources */, 03049A1C2C65039400FF6889 /* ActiveUserCountView.swift in Sources */, 0389DDDB2C3AB6340005B808 /* ActionBuilder.swift in Sources */, + 036ED67F2D0B9A520018E5EA /* CommunitySearchSortPicker.swift in Sources */, 038028FA2CB097CB0091A8A2 /* SearchView+LocationPicker.swift in Sources */, CDD8B94C2C8234BC00510EBB /* Form.swift in Sources */, 0320B6652C91DBD500D38548 /* NavigationPage+View.swift in Sources */, @@ -2392,6 +2409,7 @@ CD4D58DA2B86E11D00B82964 /* Mockable.swift in Sources */, 034B94892C09360A00039AF4 /* Int+Extensions.swift in Sources */, 039D75642C4EEE69004F24C2 /* DeletableProviding+Extensions.swift in Sources */, + 0391E0FE2D05B2DF0040CCA8 /* AdvancedSortView.swift in Sources */, 0315B1C82C7635D4006D4F82 /* ApiCommentSortType+Extensions.swift in Sources */, 035394972CA1AFAF00795AA5 /* InstanceStubProviding+Uptime.swift in Sources */, CDB2EC882BFAE14800DBC0EF /* FullyQualifiedNameView.swift in Sources */, @@ -2445,6 +2463,7 @@ 03A82FA32C0D1F2400D01A5C /* View+ExternalApiWarning.swift in Sources */, 033F84C32C2B12AA002E3EDF /* InstanceSummary.swift in Sources */, 0369B3532BFA514B001EFEDF /* ToastLocation.swift in Sources */, + 036ED6792D0AF3740018E5EA /* PinnedSortTracker.swift in Sources */, 0353948D2CA080EB00795AA5 /* FeedWelcomeView.swift in Sources */, 03049A202C650A8100FF6889 /* FormReadout.swift in Sources */, 0397D49A2C6EA6EE002C6CDC /* InteractionBarEditorView.swift in Sources */, @@ -2468,6 +2487,7 @@ 032C320C2C3482CA00595286 /* Person1Providing+Extensions.swift in Sources */, 038028D32CAB3D2D0091A8A2 /* ShareActivity.swift in Sources */, CD09BA7F2CB4698E00C93926 /* OledPalette.swift in Sources */, + 036ED67D2D0B006C0018E5EA /* TopSortPicker.swift in Sources */, 030FF67F2BC8544700F6BFAC /* CustomTabBarController.swift in Sources */, CD4D58F82B87B0D100B82964 /* InternetSpeed.swift in Sources */, 0353949C2CA4B3E800795AA5 /* CrossPostListView.swift in Sources */, diff --git a/Mlem/App/Configuration/User Settings/PinnedSortTracker.swift b/Mlem/App/Configuration/User Settings/PinnedSortTracker.swift new file mode 100644 index 000000000..581372497 --- /dev/null +++ b/Mlem/App/Configuration/User Settings/PinnedSortTracker.swift @@ -0,0 +1,29 @@ +// +// PinnedSortTracker.swift +// Mlem +// +// Created by Sjmarf on 2024-12-12. +// + +import Dependencies +import Foundation +import MlemMiddleware +import Observation + +@Observable +class PinnedSortTracker { + @ObservationIgnored @Dependency(\.persistenceRepository) + private var persistenceRepository + + var pinnedSortTypes: Set { + didSet { Task.detached { + try await self.persistenceRepository.savePinnedSortTypes(self.pinnedSortTypes) + } } + } + + init() { + self.pinnedSortTypes = PersistenceRepository.liveValue.loadPinnedSortTypes() + } + + public static let main: PinnedSortTracker = .init() +} diff --git a/Mlem/App/Globals/Definitions/PersistenceRepository.swift b/Mlem/App/Globals/Definitions/PersistenceRepository.swift index 19fcbca98..94e13f4d8 100644 --- a/Mlem/App/Globals/Definitions/PersistenceRepository.swift +++ b/Mlem/App/Globals/Definitions/PersistenceRepository.swift @@ -32,6 +32,7 @@ private enum Path { static var easterFlags = root.appendingPathComponent("Easter eggs flags", conformingTo: .json) static var instanceMetadata = root.appendingPathComponent("Instance Metadata", conformingTo: .json) static var layoutWidgets = root.appendingPathComponent("Layout Widgets", conformingTo: .json) + static var pinnedSortTypes = root.appendingPathComponent("Sort Settings", conformingTo: .json) static var systemSettings = root.appendingPathComponent("System Settings", conformingTo: .directory) static var userSettings = root.appendingPathComponent("User Settings", conformingTo: .directory) } @@ -156,6 +157,16 @@ class PersistenceRepository { try await save(value, to: Path.layoutWidgets) } + func loadPinnedSortTypes() -> Set { + load(Set.self, from: Path.pinnedSortTypes) ?? [ + .hot, .new, .topSixHour, .topDay, .topWeek, .topMonth, .topYear, .topAll + ] + } + + func savePinnedSortTypes(_ value: Set) async throws { + try await save(value, to: Path.pinnedSortTypes) + } + /// Saves the given user settings func saveUserSettings(_ settings: CodableSettings, name: String) async throws { try await save(settings, to: Path.userSettings.appendingPathComponent(name, conformingTo: .json)) diff --git a/Mlem/App/Utility/Extensions/AVPlayer+Extensions.swift b/Mlem/App/Utility/Extensions/AVPlayer+Extensions.swift index 748156d7c..5ec7396e0 100644 --- a/Mlem/App/Utility/Extensions/AVPlayer+Extensions.swift +++ b/Mlem/App/Utility/Extensions/AVPlayer+Extensions.swift @@ -10,6 +10,6 @@ import AVFoundation extension AVPlayer { func isAudioAvailable() async throws -> Bool? { - return try await self.currentItem?.asset.loadTracks(withMediaType: .audio).count != 0 + try await currentItem?.asset.loadTracks(withMediaType: .audio).count != 0 } } diff --git a/Mlem/App/Utility/Extensions/ApiSortType+Extensions.swift b/Mlem/App/Utility/Extensions/ApiSortType+Extensions.swift index 06178e3bb..f95e0a0b7 100644 --- a/Mlem/App/Utility/Extensions/ApiSortType+Extensions.swift +++ b/Mlem/App/Utility/Extensions/ApiSortType+Extensions.swift @@ -8,7 +8,7 @@ import Foundation import MlemMiddleware -extension ApiSortType { +extension ApiSortType: @retroactive CaseIterable { static let nonTopCases: [Self] = [ .hot, .scaled, @@ -34,7 +34,16 @@ extension ApiSortType { .topAll ] - static let communitySearchCases: [Self] = [.controversial, .new, .old] + topCases + public static let allCases: [Self] = nonTopCases + topCases + + enum TopSortModeFormatStyle { + case topOnly + case timescaleAbbreviated + case timescaleFull + case topAndTimescale + } + + static let communitySearchCases: [Self] = [.controversial, .new, .old] static let personSearchCases: [Self] = [.new, .old, .topAll] var minimumVersion: SiteVersion { @@ -45,35 +54,44 @@ extension ApiSortType { } } - var label: LocalizedStringResource { + private func basicLabel(abbreviateUnits: Bool = false) -> String { switch self { - case .active: "Active" - case .hot: "Hot" - case .new: "New" - case .old: "Old" - case .topDay: "Day" - case .topWeek: "Week" - case .topMonth: "Month" - case .topYear: "Year" - case .topAll: "All Time" - case .mostComments: "Most Comments" - case .newComments: "New Comments" - case .topHour: "Hour" - case .topSixHour: "6 Hours" - case .topTwelveHour: "12 Hours" - case .topThreeMonths: "3 Months" - case .topSixMonths: "6 Months" - case .topNineMonths: "9 Months" - case .controversial: "Controversial" - case .scaled: "Scaled" + case .active: + .init(localized: "Active") + case .hot: + .init(localized: "Hot") + case .new: + .init(localized: "New") + case .old: + .init(localized: "Old") + case .topAll: + .init(localized: "All Time") + case .mostComments: + .init(localized: "Most Comments") + case .newComments: + .init(localized: "New Comments") + case .controversial: + .init(localized: "Controversial") + case .scaled: + .init(localized: "Scaled") + default: + formatter(unitsStyle: abbreviateUnits ? .abbreviated : .full) + .string(for: dateComponents)? + .capitalized ?? "" } } - func fullLabel(shortTopMode: Bool = false) -> String { + func label(topFormat: TopSortModeFormatStyle = .timescaleFull) -> String { if ApiSortType.topCases.contains(self) { - return shortTopMode ? String(localized: "Top") : String(localized: "Top: \(String(localized: label))") + switch topFormat { + case .topOnly: + return String(localized: "Top") + case .topAndTimescale: + return String(localized: "Top: \(basicLabel(abbreviateUnits: true))") + default: break + } } - return String(localized: label) + return basicLabel(abbreviateUnits: topFormat != .timescaleFull) } var systemImage: String { @@ -89,4 +107,36 @@ extension ApiSortType { default: Icons.topSort } } + + var dateComponents: DateComponents? { + switch self { + case .topHour: .init(hour: 1) + case .topSixHour: .init(hour: 6) + case .topTwelveHour: .init(hour: 12) + case .topDay: .init(day: 1) + case .topWeek: .init(weekOfMonth: 1) + case .topMonth: .init(month: 1) + case .topThreeMonths: .init(month: 3) + case .topSixMonths: .init(month: 6) + case .topNineMonths: .init(month: 9) + case .topYear: .init(year: 1) + default: nil + } + } + + var explanation: LocalizedStringResource? { + switch self { + case .hot: "Ranks posts based on the post score and creation time." + case .scaled: "Similar to \"Hot\", but ranks posts from smaller communities higher." + case .active: "Ranks posts based on the post score and the time since the last comment was created." + default: nil + } + } + + private func formatter(unitsStyle: DateComponentsFormatter.UnitsStyle) -> DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .full + formatter.maximumUnitCount = 1 + return formatter + } } diff --git a/Mlem/App/Views/Pages/Community/AdvancedSortView+SortButton.swift b/Mlem/App/Views/Pages/Community/AdvancedSortView+SortButton.swift new file mode 100644 index 000000000..b7085f9b9 --- /dev/null +++ b/Mlem/App/Views/Pages/Community/AdvancedSortView+SortButton.swift @@ -0,0 +1,95 @@ +// +// AdvancedSortView+SortButton.swift +// Mlem +// +// Created by Sjmarf on 2024-12-12. +// + +import MlemMiddleware +import SwiftUI + +extension AdvancedSortView { + struct SortButton: View { + @Environment(AppState.self) var appState + @Environment(Palette.self) var palette + @Environment(\.dismiss) var dismiss + + let type: ApiSortType + var topFormat: ApiSortType.TopSortModeFormatStyle = .timescaleFull + + @Binding var selectedSort: ApiSortType + + @State var showingExplanation: Bool = false + + var body: some View { + HStack(spacing: Constants.main.standardSpacing) { + Button { + selectedSort = type + dismiss() + } label: { + HStack(spacing: Constants.main.standardSpacing) { + Image(systemName: type.systemImage) + .symbolVariant(type == selectedSort ? .fill : .none) + .frame(width: 30, alignment: .center) + .foregroundStyle(type == selectedSort ? .primary : .secondary) // No palette! + VStack(alignment: .leading) { + titleView + if (appState.firstApi.fetchedVersion ?? .infinity) < type.minimumVersion { + Text("Requires Lemmy \(type.minimumVersion) or later") + .multilineTextAlignment(.leading) + .foregroundStyle(palette.warning) + .font(.footnote) + } + } + .padding(.vertical, Constants.main.halfSpacing) + Spacer() + Button("Pin", systemImage: PinnedSortTracker.main.pinnedSortTypes.contains(type) ? Icons.pinFill : Icons.pin) { + HapticManager.main.play(haptic: .gentleInfo, priority: .low) + if PinnedSortTracker.main.pinnedSortTypes.contains(type) { + PinnedSortTracker.main.pinnedSortTypes.remove(type) + } else { + PinnedSortTracker.main.pinnedSortTypes.insert(type) + } + } + .labelStyle(.iconOnly) + .foregroundStyle(type == selectedSort ? palette.selectedInteractionBarItem : palette.accent) + } + .frame(minHeight: 45) + .buttonStyle(.plain) + .padding(.horizontal, Constants.main.standardSpacing) + .foregroundStyle(type == selectedSort ? palette.selectedInteractionBarItem : palette.primary) + .background( + type == selectedSort ? palette.accent : palette.secondaryGroupedBackground, + in: .rect(cornerRadius: Constants.main.standardSpacing) + ) + .paletteBorder(cornerRadius: Constants.main.standardSpacing) + } + } + .disabled((appState.firstApi.fetchedVersion ?? .infinity) < type.minimumVersion) + } + + @ViewBuilder + var titleView: some View { + HStack(spacing: Constants.main.standardSpacing) { + Text(type.label(topFormat: topFormat)) + if let explanation = type.explanation { + Button { + showingExplanation.toggle() + } label: { + Image(systemName: "questionmark.circle") + .foregroundStyle(.secondary) // No palette! + } + .popover(isPresented: $showingExplanation) { + Text(explanation) + .fixedSize(horizontal: false, vertical: true) + .font(.footnote) + .frame(maxWidth: 200) + .padding(10) + .presentationCompactAdaptation(.popover) + } + .environment(\.isEnabled, true) // Janky fix to override the higher-level `.disabled` modifier. + } + } + } + } +} diff --git a/Mlem/App/Views/Pages/Community/AdvancedSortView.swift b/Mlem/App/Views/Pages/Community/AdvancedSortView.swift new file mode 100644 index 000000000..c97b71f4a --- /dev/null +++ b/Mlem/App/Views/Pages/Community/AdvancedSortView.swift @@ -0,0 +1,104 @@ +// +// AdvancedSortView.swift +// Mlem +// +// Created by Sjmarf on 2024-12-08. +// + +import MlemMiddleware +import SwiftUI + +struct AdvancedSortView: View { + enum Tab: CaseIterable { + case sort, filter + + var label: LocalizedStringResource { + switch self { + case .sort: "Sort" + case .filter: "Filter" + } + } + } + + @Environment(AppState.self) var appState + @Environment(Palette.self) var palette + + @State var selectedTab: Tab = .sort + @Binding var selectedSort: ApiSortType + + var body: some View { + VStack { + switch selectedTab { + case .sort: sortTab + case .filter: filterTab + } + } + .background(palette.groupedBackground) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + CloseButtonView() + } +// Intentionally left commented out! +// +// ToolbarItem(placement: .principal) { +// Picker("Tab", selection: $selectedTab) { +// ForEach(Tab.allCases, id: \.self) { +// Text($0.label) +// } +// } +// .frame(maxWidth: .infinity) +// .pickerStyle(.segmented) +// } + } + } + + @ViewBuilder + var sortTab: some View { + ScrollView { + VStack(alignment: .leading, spacing: Constants.main.standardSpacing) { + ForEach(nonTopCases, id: \.self) { type in + SortButton(type: type, selectedSort: $selectedSort) + } + subtitle("Top of...") + ForEach(topCases, id: \.self) { type in + SortButton(type: type, selectedSort: $selectedSort) + } + let unavailableCases = unavailableCases + if !unavailableCases.isEmpty { + subtitle("Unavailable") + ForEach(unavailableCases, id: \.self) { type in + SortButton(type: type, topFormat: .topAndTimescale, selectedSort: $selectedSort) + } + } + } + .padding(.horizontal, 15) + } + } + + @ViewBuilder + func subtitle(_ title: LocalizedStringResource) -> some View { + Text(title) + .foregroundStyle(.secondary) + .fontWeight(.semibold) + .padding(.leading, Constants.main.standardSpacing) + .padding(.top, Constants.main.standardSpacing) + } + + @ViewBuilder + var filterTab: some View { + Text("Filter") + } + + var nonTopCases: [ApiSortType] { + ApiSortType.nonTopCases.filter { (appState.firstApi.fetchedVersion ?? .infinity) >= $0.minimumVersion } + } + + var topCases: [ApiSortType] { + ApiSortType.topCases.filter { (appState.firstApi.fetchedVersion ?? .infinity) >= $0.minimumVersion } + } + + var unavailableCases: [ApiSortType] { + ApiSortType.allCases.filter { (appState.firstApi.fetchedVersion ?? .zero) < $0.minimumVersion } + } +} diff --git a/Mlem/App/Views/Pages/Community/CommunitySearchSortPicker.swift b/Mlem/App/Views/Pages/Community/CommunitySearchSortPicker.swift new file mode 100644 index 000000000..ab7811626 --- /dev/null +++ b/Mlem/App/Views/Pages/Community/CommunitySearchSortPicker.swift @@ -0,0 +1,45 @@ +// +// CommunitySearchSortPicker.swift +// Mlem +// +// Created by Sjmarf on 2024-12-12. +// + +import MlemMiddleware +import SwiftUI + +struct CommunitySearchSortPicker: View { + @Environment(AppState.self) var appState + + @Binding var sort: ApiSortType + + @State var topSortPopupPresented: Bool = false + + var sortTypes: [ApiSortType] { + ApiSortType.communitySearchCases + .filter { (appState.firstApi.fetchedVersion ?? .infinity) >= $0.minimumVersion } + } + + var body: some View { + Menu(sort.label(topFormat: .topAndTimescale), systemImage: sort.systemImage) { + ForEach(sortTypes, id: \.self) { type in + Toggle( + type.label(), + systemImage: type.systemImage, + isOn: .init(get: { sort == type }, set: { _ in sort = type }) + ) + } + Toggle( + "Top...", + systemImage: Icons.topSort, + isOn: .init(get: { ApiSortType.topCases.contains(sort) }, set: { _ in topSortPopupPresented = true }) + ) + } + .popover(isPresented: $topSortPopupPresented) { + TopSortPicker(selected: $sort, includeAll: true) + .presentationBackground(.clear) + .presentationCornerRadius(18) + .presentationCompactAdaptation(.popover) + } + } +} diff --git a/Mlem/App/Views/Pages/Community/FeedSortPicker.swift b/Mlem/App/Views/Pages/Community/FeedSortPicker.swift index 82953dd3e..2eb98a142 100644 --- a/Mlem/App/Views/Pages/Community/FeedSortPicker.swift +++ b/Mlem/App/Views/Pages/Community/FeedSortPicker.swift @@ -5,75 +5,87 @@ // Created by Sjmarf on 10/08/2024. // +import Flow import MlemMiddleware import SwiftUI struct FeedSortPicker: View { - enum Filter { - case alwaysAvailable, availableOnInstance, communitySearchable, personSearchable - } - @Environment(AppState.self) var appState + @Environment(NavigationLayer.self) var navigation - let filters: Set @Binding var sort: ApiSortType - init(sort: Binding, filters: Set = []) { + @State var topSortPopupPresented: Bool = false + + init(sort: Binding) { self._sort = sort - self.filters = filters } init(feedLoader: CorePostFeedLoader) { self.init(sort: .init(get: { feedLoader.sortType }, set: { newSort in - Task { + Task { @MainActor in do { try await feedLoader.changeSortType(to: newSort, forceRefresh: false) } catch { handleError(error) } } - }), filters: [.availableOnInstance]) + })) + } + + var nonTopSortTypes: [ApiSortType] { + ApiSortType.nonTopCases + .filter { PinnedSortTracker.main.pinnedSortTypes.contains($0) } + .filter { (appState.firstApi.fetchedVersion ?? .infinity) >= $0.minimumVersion } + } + + var topSortTypes: [ApiSortType] { + ApiSortType.topCases + .filter { PinnedSortTracker.main.pinnedSortTypes.contains($0) } + .filter { (appState.firstApi.fetchedVersion ?? .infinity) >= $0.minimumVersion } } var body: some View { - let topModes = filterSortModes(ApiSortType.topCases) - Menu(sort.fullLabel(shortTopMode: topModes.count == 1), systemImage: sort.systemImage) { - Picker("Sort", selection: $sort) { - itemLabels(filterSortModes(ApiSortType.nonTopCases)) - if topModes.count == 1, let first = topModes.first { - Label("Top", systemImage: Icons.topSort) - .tag(first) + Menu(sort.label(topFormat: topSortTypes.count == 1 ? .topOnly : .topAndTimescale), systemImage: sort.systemImage) { + Section { + ForEach(nonTopSortTypes, id: \.self) { type in + Toggle( + type.label(), + systemImage: type.systemImage, + isOn: .init(get: { sort == type }, set: { _ in sort = type }) + ) + } + let topSortTypes = topSortTypes + if topSortTypes.count > 3 { + Toggle( + "Top...", + systemImage: Icons.topSort, + isOn: .init(get: { ApiSortType.topCases.contains(sort) }, set: { _ in topSortPopupPresented = true }) + ) } else { - Picker("Top...", systemImage: Icons.topSort, selection: $sort) { - itemLabels(topModes) + ForEach(topSortTypes, id: \.self) { type in + Toggle( + type.label(topFormat: .topAndTimescale), + systemImage: type.systemImage, + isOn: .init(get: { sort == type }, set: { _ in sort = type }) + ) } - .pickerStyle(.menu) } } - } - .disabled(filters.contains(.availableOnInstance) && appState.firstApi.fetchedVersion == nil) - } - - @ViewBuilder - func itemLabels(_ collection: [ApiSortType]) -> some View { - ForEach(collection, id: \.self) { item in - Label(String(localized: item.label), systemImage: item.systemImage) - } - } - - private func filterSortModes(_ collection: any Collection) -> [ApiSortType] { - collection.filter { sortType in - filters.allSatisfy { filter in - switch filter { - case .alwaysAvailable: sortType.minimumVersion == .zero - case .availableOnInstance: - (appState.firstApi.fetchedVersion ?? .infinity) >= sortType.minimumVersion - case .communitySearchable: - ApiSortType.communitySearchCases.contains(sortType) - case .personSearchable: - ApiSortType.personSearchCases.contains(sortType) + Section { + Button("More...", systemImage: Icons.menuCircle) { + navigation.openSheet(.advancedSorting($sort)) } } } + .disabled(appState.firstApi.fetchedVersion == nil) + .popover(isPresented: $topSortPopupPresented) { + TopSortPicker(selected: $sort) + // This background is always drawn over a material background unfortunately, + // meaning that we can't use thin materials + .presentationBackground(.clear) + .presentationCornerRadius(18) + .presentationCompactAdaptation(.popover) + } } } diff --git a/Mlem/App/Views/Pages/Community/TopSortPicker.swift b/Mlem/App/Views/Pages/Community/TopSortPicker.swift new file mode 100644 index 000000000..ca69ce843 --- /dev/null +++ b/Mlem/App/Views/Pages/Community/TopSortPicker.swift @@ -0,0 +1,79 @@ +// +// FeedSortPicker+TopSortPicker.swift +// Mlem +// +// Created by Sjmarf on 2024-12-12. +// + +import Flow +import MlemMiddleware +import SwiftUI + +struct TopSortPicker: View { + @Environment(AppState.self) var appState + @Environment(Palette.self) var palette + @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme + + @Binding var selected: ApiSortType + var includeAll: Bool = false + + var sortTypes: [ApiSortType] { + ApiSortType.topCases + .filter { includeAll ? true : PinnedSortTracker.main.pinnedSortTypes.contains($0) } + .filter { (appState.firstApi.fetchedVersion ?? .infinity) >= $0.minimumVersion } + } + + var body: some View { + HFlow(spacing: 10, justification: .stretchItems) { + ForEach(sortTypes, id: \.self) { type in + button(type) + .frame(minWidth: 60) + } + } + .padding(10) + .frame(width: 222) + } + + @ViewBuilder + func button(_ type: ApiSortType) -> some View { + Button { + selected = type + dismiss() + } label: { + Group { + if type == .topAll { + if sortTypes.count % 3 == 0 { + Text("All") + } else { + Text("All Time") + } + } else { + Text(formatter.string(from: type.dateComponents ?? .init()) ?? "") + } + } + .frame(maxWidth: .infinity) + .contentShape(.rect) + } + .frame(maxWidth: .infinity) + .frame(height: 40) + .background { + if colorScheme == .dark { + RoundedRectangle(cornerRadius: 8) + .fill(palette.primary.opacity(0.2)) + } else { + RoundedRectangle(cornerRadius: 8) + .fill(palette.background) + .shadow(color: .black.opacity(0.05), radius: 3) + } + } + .foregroundStyle(palette.primary) + } + + var formatter: DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .abbreviated + formatter.maximumUnitCount = 1 + return formatter + } +} diff --git a/Mlem/App/Views/Root/ContentView.swift b/Mlem/App/Views/Root/ContentView.swift index 6b501b8b2..7f620c1a9 100644 --- a/Mlem/App/Views/Root/ContentView.swift +++ b/Mlem/App/Views/Root/ContentView.swift @@ -175,10 +175,13 @@ struct ContentView: View { .overlay { if let url = mediaState.url { NavigationLayerView( - layer: .init(root: .imageViewer(url), - model: navigationModel, - hasNavigationStack: false), - hasSheetModifiers: false) + layer: .init( + root: .imageViewer(url), + model: navigationModel, + hasNavigationStack: false + ), + hasSheetModifiers: false + ) } } } diff --git a/Mlem/App/Views/Root/MlemApp.swift b/Mlem/App/Views/Root/MlemApp.swift index 5d413569e..853e165f3 100644 --- a/Mlem/App/Views/Root/MlemApp.swift +++ b/Mlem/App/Views/Root/MlemApp.swift @@ -5,10 +5,10 @@ // Created by Eric Andrews on 2024-02-21. // +import AVFAudio import Nuke import SDWebImageWebPCoder import SwiftUI -import AVFAudio /// Root view for the app @main diff --git a/Mlem/App/Views/Root/Tabs/Settings/SortingSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/SortingSettingsView.swift index 2ebcf65e4..25dc7a1ad 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/SortingSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/SortingSettingsView.swift @@ -30,7 +30,7 @@ struct SortingSettingsView: View { HStack { Text("Fallback") Spacer() - FeedSortPicker(sort: $fallbackPostSort, filters: [.alwaysAvailable]) + FeedSortPicker(sort: $fallbackPostSort) .foregroundStyle(palette.accent) .frame(minHeight: 50) .buttonStyle(.bordered) @@ -39,7 +39,7 @@ struct SortingSettingsView: View { } footer: { if defaultPostSort.minimumVersion != .zero { // swiftlint:disable:next line_length - Text("The \"\(defaultPostSort.fullLabel())\" sort mode is only available on instances running version \(defaultPostSort.minimumVersion.description) or later. On instances running earlier versions, the \"Fallback\" sort mode will be used instead.") + Text("The \"\(defaultPostSort.label())\" sort mode is only available on instances running version \(defaultPostSort.minimumVersion.description) or later. On instances running earlier versions, the \"Fallback\" sort mode will be used instead.") } } diff --git a/Mlem/App/Views/Shared/Images/Core/Animated/VideoView.swift b/Mlem/App/Views/Shared/Images/Core/Animated/VideoView.swift index ae7bdcbd4..752650616 100644 --- a/Mlem/App/Views/Shared/Images/Core/Animated/VideoView.swift +++ b/Mlem/App/Views/Shared/Images/Core/Animated/VideoView.swift @@ -6,9 +6,9 @@ // import AVFoundation +import AVKit import NukeVideo import SwiftUI -import AVKit struct VideoView: View { let player: AVQueuePlayer @@ -32,8 +32,8 @@ struct VideoView: View { init(asset: AVAsset) { // set up AVQueuePlayer and AVPlayerLooper to loop the video let playerItem: AVPlayerItem = .init(asset: asset) - player = .init(playerItem: playerItem) - playerLooper = .init(player: player, templateItem: playerItem) + self.player = .init(playerItem: playerItem) + self.playerLooper = .init(player: player, templateItem: playerItem) @Setting(\.muteVideos) var muteVideos player.isMuted = muteVideos diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift index ff43fc2af..9d0ef2d32 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift @@ -156,6 +156,8 @@ extension NavigationPage { .presentationDetents([.medium, .large]) case .blockList: BlockListView() + case let .advancedSorting(sort): + AdvancedSortView(selectedSort: sort.wrappedValue) } } } diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift index 74d0f95ca..ba5f563fd 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift @@ -53,6 +53,7 @@ enum NavigationPage: Hashable { case confirmUpload(imageData: Data, imageManager: ImageUploadManager, uploadApi: ApiClient) case rulesList(_ model: Profile2HashWrapper, callback: HashWrapper<(String) -> Void>) case blockList + case advancedSorting(_ sort: HashWrapper>) static func post(_ post: any PostStubProviding, scrollTargetedComment: (any CommentStubProviding)? = nil) -> NavigationPage { if let scrollTargetedComment { @@ -244,6 +245,10 @@ enum NavigationPage: Hashable { rulesList(.init(wrappedValue: model), callback: .init(wrappedValue: callback)) } + static func advancedSorting(_ sort: Binding) -> NavigationPage { + advancedSorting(.init(wrappedValue: sort)) + } + var hasNavigationStack: Bool { switch self { case .quickSwitcher, .report, .externalApiInfo, .selectText, .createComment, .editComment, .createPost, .editPost: @@ -255,7 +260,7 @@ enum NavigationPage: Hashable { var canDisplayToasts: Bool { switch self { - case .quickSwitcher, .externalApiInfo, .selectText: + case .quickSwitcher, .externalApiInfo, .selectText, .advancedSorting: false default: true diff --git a/Mlem/App/Views/Shared/Search/SearchView+FiltersView.swift b/Mlem/App/Views/Shared/Search/SearchView+FiltersView.swift index 2848fa744..fcf46f60f 100644 --- a/Mlem/App/Views/Shared/Search/SearchView+FiltersView.swift +++ b/Mlem/App/Views/Shared/Search/SearchView+FiltersView.swift @@ -45,21 +45,21 @@ extension SearchView { @ViewBuilder private var communityFiltersView: some View { - FeedSortPicker( - sort: $communityFilters.sort, - filters: [.availableOnInstance, .communitySearchable] - ) - .buttonStyle(FilterButtonStyle(isOn: communityFilters.sort != .topAll)) + CommunitySearchSortPicker(sort: $communityFilters.sort) + .buttonStyle(FilterButtonStyle(isOn: communityFilters.sort != .topAll)) InstancePicker(filter: $communityFilters.instance, isForPersonSearch: false) .buttonStyle(FilterButtonStyle(isOn: communityFilters.instance != .any)) } @ViewBuilder private var personFiltersView: some View { - FeedSortPicker( - sort: $personFilters.sort, - filters: [.availableOnInstance, .personSearchable] - ) + Menu(personFilters.sort.label(topFormat: .topOnly), systemImage: personFilters.sort.systemImage) { + Picker("Sort", selection: $personFilters.sort) { + ForEach(ApiSortType.personSearchCases, id: \.self) { item in + Label(item.label(topFormat: .topOnly), systemImage: item.systemImage) + } + } + } .buttonStyle(FilterButtonStyle(isOn: personFilters.sort != .topAll)) InstancePicker(filter: $personFilters.instance, isForPersonSearch: true) .buttonStyle(FilterButtonStyle(isOn: personFilters.instance != .any)) @@ -67,11 +67,8 @@ extension SearchView { @ViewBuilder private var postFiltersView: some View { - FeedSortPicker( - sort: $postFilters.sort, - filters: [.availableOnInstance] - ) - .buttonStyle(FilterButtonStyle(isOn: postFilters.sort != .topAll)) + FeedSortPicker(sort: $postFilters.sort) + .buttonStyle(FilterButtonStyle(isOn: postFilters.sort != .topAll)) LocationPicker(filter: $postFilters.location) .buttonStyle(FilterButtonStyle(isOn: postFilters.location != .any)) Button(postFilters.creator?.name ?? .init(localized: "Anyone"), systemImage: Icons.person) { diff --git a/Mlem/Localizable.xcstrings b/Mlem/Localizable.xcstrings index 54a4662fb..3cf2e3838 100644 --- a/Mlem/Localizable.xcstrings +++ b/Mlem/Localizable.xcstrings @@ -96,21 +96,6 @@ }, "%lld on your instance" : { - }, - "3 Months" : { - - }, - "6 Hours" : { - - }, - "6 Months" : { - - }, - "9 Months" : { - - }, - "12 Hours" : { - }, "A censure signifies that an instance disapproves of another instance. Like an endorsement, it is completely subjective and a reason does not have to be given." : { @@ -456,9 +441,6 @@ } } } - }, - "Day" : { - }, "Days:" : { @@ -663,6 +645,9 @@ }, "Files" : { + }, + "Filter" : { + }, "Filters" : { @@ -729,9 +714,6 @@ }, "Hot" : { - }, - "Hour" : { - }, "I think I've found the bottom!" : { @@ -924,9 +906,6 @@ }, "Monochrome" : { - }, - "Month" : { - }, "More" : { @@ -1140,6 +1119,9 @@ }, "Quote" : { + }, + "Ranks posts based on the post score and creation time." : { + }, "Readouts:" : { @@ -1645,6 +1627,9 @@ }, "Top" : { + }, + "Top of..." : { + }, "Top: %@" : { @@ -1666,6 +1651,9 @@ }, "Two-Factor Authentication" : { + }, + "Unavailable" : { + }, "Unban" : { @@ -1803,9 +1791,6 @@ }, "Website" : { - }, - "Week" : { - }, "Welcome %@" : { "comment" : "Example: \"Welcome John\"" @@ -1819,7 +1804,7 @@ "What is Federation?" : { }, - "When disabled, some moderator actions will be hidden from the feed and will only be visible from when viewing a post page." : { + "When disabled, some moderator actions will only be accessible from the post page." : { }, "Wrap Code Block Lines" : { @@ -1827,9 +1812,6 @@ }, "Write a bit about yourself..." : { - }, - "Year" : { - }, "Yes" : {