diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 1914cdea6..d5ade612e 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 0300309D2C4163C9009A65FF /* CommentTreeTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300309C2C4163C9009A65FF /* CommentTreeTracker.swift */; }; 030030A12C416B0B009A65FF /* RefreshPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030030A02C416B0B009A65FF /* RefreshPopupView.swift */; }; + 030050D32D109B7E002B1E99 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030050D22D109B7E002B1E99 /* ReportView.swift */; }; + 030050D52D10AE30002B1E99 /* Report+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030050D42D10AE30002B1E99 /* Report+Extensions.swift */; }; 03036C742C71408700C6DA1D /* CounterAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C732C71408700C6DA1D /* CounterAppearance.swift */; }; 03036C762C71427B00C6DA1D /* InteractionBarCounterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C752C71427B00C6DA1D /* InteractionBarCounterLabelView.swift */; }; 03036C832C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C822C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift */; }; @@ -82,6 +84,7 @@ 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 */; }; + 034690992D105DFD0073E664 /* InboxView+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034690982D105DFD0073E664 /* InboxView+Types.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 */; }; @@ -258,6 +261,8 @@ 03DAEA772C64074E0064DE64 /* SubscriptionListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DAEA762C64074E0064DE64 /* SubscriptionListItemView.swift */; }; 03E0EF432CA73D7A002CB66C /* PostPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E0EF422CA73D7A002CB66C /* PostPage.swift */; }; 03E0EF452CA74036002CB66C /* CommentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E0EF442CA74036002CB66C /* CommentPage.swift */; }; + 03E46ACB2D1216CE002589DB /* PostViewLinkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E46ACA2D1216CE002589DB /* PostViewLinkType.swift */; }; + 03E46ACD2D121C19002589DB /* HeadlinePostBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E46ACC2D121C19002589DB /* HeadlinePostBodyView.swift */; }; 03E614E42C0BCC7B00F692A4 /* Post1Providing+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E614E32C0BCC7B00F692A4 /* Post1Providing+Extensions.swift */; }; 03E614E52C0BCCAA00F692A4 /* PostSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB2EC7A2BFADA8B00DBC0EF /* PostSize.swift */; }; 03E614E72C0BCDC200F692A4 /* FullyQualifiedLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E614E62C0BCDC200F692A4 /* FullyQualifiedLinkView.swift */; }; @@ -441,6 +446,8 @@ /* Begin PBXFileReference section */ 0300309C2C4163C9009A65FF /* CommentTreeTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentTreeTracker.swift; sourceTree = ""; }; 030030A02C416B0B009A65FF /* RefreshPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshPopupView.swift; sourceTree = ""; }; + 030050D22D109B7E002B1E99 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = ""; }; + 030050D42D10AE30002B1E99 /* Report+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Report+Extensions.swift"; sourceTree = ""; }; 03036C732C71408700C6DA1D /* CounterAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterAppearance.swift; sourceTree = ""; }; 03036C752C71427B00C6DA1D /* InteractionBarCounterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionBarCounterLabelView.swift; sourceTree = ""; }; 03036C822C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InteractionBarEditorView+Logic.swift"; sourceTree = ""; }; @@ -512,6 +519,7 @@ 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 = ""; }; + 034690982D105DFD0073E664 /* InboxView+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InboxView+Types.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 = ""; }; @@ -688,6 +696,8 @@ 03DAEA762C64074E0064DE64 /* SubscriptionListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionListItemView.swift; sourceTree = ""; }; 03E0EF422CA73D7A002CB66C /* PostPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostPage.swift; sourceTree = ""; }; 03E0EF442CA74036002CB66C /* CommentPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentPage.swift; sourceTree = ""; }; + 03E46ACA2D1216CE002589DB /* PostViewLinkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewLinkType.swift; sourceTree = ""; }; + 03E46ACC2D121C19002589DB /* HeadlinePostBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinePostBodyView.swift; sourceTree = ""; }; 03E614E32C0BCC7B00F692A4 /* Post1Providing+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Post1Providing+Extensions.swift"; sourceTree = ""; }; 03E614E62C0BCDC200F692A4 /* FullyQualifiedLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullyQualifiedLinkView.swift; sourceTree = ""; }; 03ECD7182C81195000D48BF6 /* PostEditorView+LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostEditorView+LinkView.swift"; sourceTree = ""; }; @@ -1099,6 +1109,7 @@ isa = PBXGroup; children = ( 0369B3552BFA6824001EFEDF /* InboxView.swift */, + 034690982D105DFD0073E664 /* InboxView+Types.swift */, 0353948A2CA076D000795AA5 /* InboxView+Views.swift */, ); path = Inbox; @@ -1558,6 +1569,7 @@ 0382A7F12C0A758E00C79DDA /* ProfileDateView.swift */, 030030A02C416B0B009A65FF /* RefreshPopupView.swift */, 032C32152C36F65500595286 /* ReplyView.swift */, + 030050D22D109B7E002B1E99 /* ReportView.swift */, 03531EEF2C2DA291004A3464 /* Search */, 032C32092C34495D00595286 /* SelectTextView.swift */, 03500C252BF694A800CAA076 /* Toast */, @@ -1629,6 +1641,7 @@ CDA683F72C77E577000C4486 /* NsfwBlurBehavior.swift */, 039F58832C7A7F2C00C61658 /* CommentJumpButtonLocation.swift */, 0320B6622C8F8D5A00D38548 /* InstanceSort.swift */, + 03E46ACA2D1216CE002589DB /* PostViewLinkType.swift */, ); path = Enums; sourceTree = ""; @@ -1796,6 +1809,7 @@ CD8DCAF82C5E92E8003E4DD7 /* Profile1Providing+Extensions.swift */, 036ED6822D0C483B0018E5EA /* Profile2Providing+Extensions.swift */, 0320B6532C8B65EB00D38548 /* Captcha+Extensions.swift */, + 030050D42D10AE30002B1E99 /* Report+Extensions.swift */, ); path = "Content Models"; sourceTree = ""; @@ -1889,6 +1903,7 @@ CDE021AC2BFA43380052FD61 /* FeedPostView.swift */, CDB2EC7C2BFADAB300DBC0EF /* CompactPostView.swift */, CDB2EC7E2BFADACC00DBC0EF /* HeadlinePostView.swift */, + 03E46ACC2D121C19002589DB /* HeadlinePostBodyView.swift */, CDB2EC802BFADADF00DBC0EF /* LargePostView.swift */, 03B431BB2C455838001A1EB5 /* LargePostBodyView.swift */, CDBFCB6B2C054AA7008CD468 /* TilePostView.swift */, @@ -2231,6 +2246,7 @@ 035EDEF12C2DE94B00F51144 /* DefaultTextInputType.swift in Sources */, 039F588C2C7B574E00C61658 /* AdvancedSettingsView.swift in Sources */, 0320B6672C93504600D38548 /* SignUpView+Logic.swift in Sources */, + 03E46ACD2D121C19002589DB /* HeadlinePostBodyView.swift in Sources */, 039F58882C7B531800C61658 /* SquircleLabelStyle.swift in Sources */, 035EDEF22C2DE94B00F51144 /* _assignIfNotEqual.swift in Sources */, 035EDEF32C2DE94B00F51144 /* SearchBar.swift in Sources */, @@ -2248,6 +2264,7 @@ 03B25B332CC440A600EB6DF5 /* FediseerOpinionView.swift in Sources */, 03CCDAA02BF2795300C0C851 /* LoginPage.swift in Sources */, 032C32082C34469900595286 /* SelectableContentProviding+Extensions.swift in Sources */, + 03E46ACB2D1216CE002589DB /* PostViewLinkType.swift in Sources */, 037658DF2BE7D9EF00F4DD4D /* Community1Providing+Extensions.swift in Sources */, 032C32182C36F70300595286 /* ReplyBarConfiguration.swift in Sources */, CDD4A0A02C8B985D0001AD1A /* ImportExportSettingsPage.swift in Sources */, @@ -2372,6 +2389,8 @@ CD8DCAF92C5E92E8003E4DD7 /* Profile1Providing+Extensions.swift in Sources */, CDB2EC8A2BFAEFDF00DBC0EF /* ThumbnailImageView.swift in Sources */, 0397D4932C6CE87E002C6CDC /* PostEditorTargetView.swift in Sources */, + 034690992D105DFD0073E664 /* InboxView+Types.swift in Sources */, + 030050D52D10AE30002B1E99 /* Report+Extensions.swift in Sources */, CD57AFA32D03761500AB3956 /* WebpView.swift in Sources */, 0369B3562BFA6824001EFEDF /* InboxView.swift in Sources */, CD4D59162B87B38C00B82964 /* UIApplication+Extensions.swift in Sources */, @@ -2466,6 +2485,7 @@ 036ED6792D0AF3740018E5EA /* PinnedSortTracker.swift in Sources */, 0353948D2CA080EB00795AA5 /* FeedWelcomeView.swift in Sources */, 03049A202C650A8100FF6889 /* FormReadout.swift in Sources */, + 030050D32D109B7E002B1E99 /* ReportView.swift in Sources */, 0397D49A2C6EA6EE002C6CDC /* InteractionBarEditorView.swift in Sources */, 0300309D2C4163C9009A65FF /* CommentTreeTracker.swift in Sources */, 032C321A2C36F75800595286 /* Reply1Providing+Extensions.swift in Sources */, @@ -2970,8 +2990,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mlemgroup/MlemMiddleware"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.53.0; + branch = sjmarf/report; + kind = branch; }; }; CDE4AC402CA3706400981010 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { diff --git a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 47278bd4e..b8ac15ea9 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" : "a4dc838d0ff82ea76b71ee9e9568743d957e15a0", - "version" : "0.53.0" + "branch" : "sjmarf/report", + "revision" : "082d54e3e607ea8afcbc0f31006fd3ba05896e3d" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke", "state" : { - "revision" : "0ead44350d2737db384908569c012fe67c421e4d", - "version" : "12.8.0" + "revision" : "8e431251dea0081b6ab154dab61a6ec74e4b6577", + "version" : "12.6.0" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/Semaphore", "state" : { - "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", - "version" : "0.1.0" + "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", + "version" : "0.0.8" } }, { diff --git a/Mlem/App/Enums/PostViewLinkType.swift b/Mlem/App/Enums/PostViewLinkType.swift new file mode 100644 index 000000000..2cfa10b70 --- /dev/null +++ b/Mlem/App/Enums/PostViewLinkType.swift @@ -0,0 +1,12 @@ +// +// PostViewLinkType.swift +// Mlem +// +// Created by Sjmarf on 2024-12-17. +// + +import Foundation + +enum PostViewNavigationLink { + case creator, community +} diff --git a/Mlem/App/Models/Session/UserSession.swift b/Mlem/App/Models/Session/UserSession.swift index 0eeafa4e0..836f0a921 100644 --- a/Mlem/App/Models/Session/UserSession.swift +++ b/Mlem/App/Models/Session/UserSession.swift @@ -33,7 +33,7 @@ class UserSession: Session { } ) - Task { + Task { @MainActor in do { try await self.api.fetchSiteVersion(task: Task { let (person, instance, blocks) = try await self.api.getMyPerson() diff --git a/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift index b0118c0a9..a00101f6c 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift @@ -55,7 +55,8 @@ extension Comment1Providing { func allMenuActions( expanded: Bool = false, feedback: Set = [.haptic, .toast], - commentTreeTracker: CommentTreeTracker? = nil + commentTreeTracker: CommentTreeTracker? = nil, + report: Report? = nil ) -> [any Action] { basicMenuActions(feedback: feedback, commentTreeTracker: commentTreeTracker) if canModerate { @@ -63,7 +64,7 @@ extension Comment1Providing { 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, report: report) } } } @@ -96,7 +97,10 @@ extension Comment1Providing { } @ActionBuilder - func moderatorMenuActions(feedback: Set = [.haptic, .toast]) -> [any Action] { + func moderatorMenuActions( + feedback: Set = [.haptic, .toast], + report: Report? = nil + ) -> [any Action] { if let self2, !isOwnComment { self2.removeAction().disabled(!canModerate) banActions() @@ -107,6 +111,11 @@ extension Comment1Providing { purgeCreatorAction() } } + if let report { + ActionGroup { + report.menuActions() + } + } } func shouldShowLoadingSymbol(for barConfiguration: CommentBarConfiguration? = nil) -> Bool { diff --git a/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift index ec66b1087..c71691939 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Interactable1Providing+Extensions.swift @@ -165,10 +165,10 @@ extension Interactable1Providing { ) } - func banActions() -> [any Action] { + func banActions(communityId: Int? = nil) -> [any Action] { let isModerator: Bool - if let myPerson = api.myPerson, let community = community_ { - isModerator = myPerson.moderates(communityId: community.id) + if let myPerson = api.myPerson, let communityId = (communityId ?? community_?.id) { + isModerator = myPerson.moderates(communityId: communityId) } else { isModerator = false } diff --git a/Mlem/App/Utility/Extensions/Content Models/Message1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Message1Providing+Extensions.swift index 16a907c1d..522ebcaa4 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Message1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Message1Providing+Extensions.swift @@ -24,7 +24,26 @@ extension Message1Providing { } @ActionBuilder - func menuActions(feedback: Set = [.haptic, .toast]) -> [any Action] { + func allMenuActions( + feedback: Set = [.haptic, .toast], + report: Report? = nil + ) -> [any Action] { + basicMenuActions(feedback: feedback) + if api.isAdmin { + ActionGroup( + appearance: .init(label: "Moderation...", color: Palette.main.moderation, icon: Icons.moderation), + displayMode: Settings.main.moderatorActionGrouping == .divider ? .section : .disclosure + ) { + moderatorMenuActions(feedback: feedback, report: report) + } + } + } + + @ActionBuilder + func basicMenuActions( + feedback: Set = [.haptic, .toast], + report: Report? = nil + ) -> [any Action] { if !isOwnMessage { replyAction() markReadAction(feedback: feedback) @@ -35,11 +54,25 @@ extension Message1Providing { if isOwnMessage { deleteAction(feedback: feedback) } else { - reportAction() + if report == nil { + reportAction() + } blockCreatorAction(feedback: feedback) } } + @ActionBuilder + func moderatorMenuActions( + feedback: Set = [.haptic, .toast], + report: Report? = nil + ) -> [any Action] { + if let report { + ActionGroup { + report.menuActions() + } + } + } + // These actions are also defined in Interactable1Providing... another protocol for these may be a good idea func replyAction() -> BasicAction { diff --git a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift index 77cefda9c..9da101b90 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift @@ -132,7 +132,8 @@ extension Post1Providing { expanded: Bool = false, feedback: Set = [.haptic, .toast], showAllActions: Bool = true, - commentTreeTracker: CommentTreeTracker? = nil + commentTreeTracker: CommentTreeTracker? = nil, + report: Report? = nil ) -> [any Action] { basicMenuActions(feedback: feedback, commentTreeTracker: commentTreeTracker) if canModerate { @@ -140,7 +141,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, showAllActions: showAllActions) + moderatorMenuActions(feedback: feedback, showAllActions: showAllActions, report: report) } } } @@ -181,7 +182,8 @@ extension Post1Providing { @ActionBuilder func moderatorMenuActions( feedback: Set = [.haptic, .toast], - showAllActions: Bool = true + showAllActions: Bool = true, + report: Report? = nil ) -> [any Action] { if showAllActions || Settings.main.showAllModActions { pinToCommunityAction(feedback: feedback, verboseTitle: api.isAdmin) @@ -190,9 +192,9 @@ extension Post1Providing { } lockAction(feedback: feedback) } - if let self2, !isOwnPost { - self2.removeAction().disabled(!canModerate) - banActions() + if !isOwnPost { + removeAction().disabled(!canModerate) + banActions(communityId: communityId) } if api.isAdmin { purgeAction() @@ -200,6 +202,11 @@ extension Post1Providing { purgeCreatorAction() } } + if let report { + ActionGroup { + report.menuActions() + } + } } // swiftlint:disable:next cyclomatic_complexity diff --git a/Mlem/App/Utility/Extensions/Content Models/Report+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Report+Extensions.swift new file mode 100644 index 000000000..41df1926a --- /dev/null +++ b/Mlem/App/Utility/Extensions/Content Models/Report+Extensions.swift @@ -0,0 +1,37 @@ +// +// Report+Extensions.swift +// Mlem +// +// Created by Sjmarf on 2024-12-16. +// + +import MlemMiddleware + +extension Report { + func toggleResolved(feedback: Set) { + if feedback.contains(.haptic) { + HapticManager.main.play(haptic: .success, priority: .low) + } + toggleResolved() + } + + @ActionBuilder + func menuActions( + feedback: Set = [.haptic] + ) -> [any Action] { + resolveAction(feedback: feedback) + } + + func resolveAction(feedback: Set = []) -> BasicAction { + .init( + id: "resolve\(cacheId)", + appearance: .init( + label: resolved ? "Unresolve" : "Resolve", + color: resolved ? Palette.main.negative : Palette.main.positive, + icon: resolved ? Icons.failureCircle : Icons.successCircle, + swipeIcon2: resolved ? Icons.failureCircleFill : Icons.successCircleFill + ), + callback: api.canInteract ? { self.toggleResolved(feedback: feedback) } : nil + ) + } +} diff --git a/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift b/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift index 294e11554..1431ba529 100644 --- a/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift +++ b/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift @@ -12,6 +12,7 @@ extension EnvironmentValues { @Entry var postContext: (any Post1Providing)? @Entry var commentContext: (any Comment1Providing)? @Entry var communityContext: (any Community1Providing)? + @Entry var reportContext: Report? @Entry var parentFrameWidth: CGFloat = .zero @Entry var isRootView: Bool = false diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift index 6ce1d0289..844e4130d 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift @@ -9,26 +9,48 @@ import Foundation import MlemMiddleware import SwiftUI -struct FeedCommentView: View { - @Setting(\.postSize) var postSize +struct FeedCommentView: View { + @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? @Environment(Palette.self) var palette + @Environment(\.reportContext) var reportContext: Report? + + @Setting(\.postSize) var postSize + + let comment: any Comment + var overrideIsTiled: Bool? + @ViewBuilder var embeddedContent: () -> EmbeddedContent - let comment: Comment2 + init( + comment: any Comment, + overrideIsTiled: Bool? = nil, + @ViewBuilder embeddedContent: @escaping () -> EmbeddedContent = { EmptyView() } + ) { + self.comment = comment + self.overrideIsTiled = overrideIsTiled + self.embeddedContent = embeddedContent + } + + var overridenSize: PostSize { + if let overrideIsTiled { + return overrideIsTiled ? .tile : .large + } + return postSize + } var body: some View { content .contentShape(.interaction, .rect) - .quickSwipes(comment.swipeActions(behavior: postSize.swipeBehavior)) - .contextMenu { comment.allMenuActions() } + .quickSwipes(comment.swipeActions(behavior: overridenSize.swipeBehavior, commentTreeTracker: commentTreeTracker)) + .contextMenu { comment.allMenuActions(report: reportContext) } .paletteBorder(cornerRadius: postSize.swipeBehavior.cornerRadius) } @ViewBuilder var content: some View { - if postSize.tiled { + if overridenSize.tiled { TileCommentView(comment: comment) } else { - CommentView(comment: comment, inFeed: true) + CommentView(comment: comment, inFeed: true, embeddedContent: embeddedContent) } } } diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift index 6cf5256c8..7ef865767 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift @@ -14,7 +14,7 @@ struct TileCommentView: View { @Environment(Palette.self) var palette @Environment(\.parentFrameWidth) var parentFrameWidth: CGFloat - let comment: any Comment2Providing + let comment: any Comment @ScaledMetric(relativeTo: .footnote) var titleHeight: CGFloat = 36 // (2 * .footnote height), including built-in spacing @ScaledMetric(relativeTo: .caption) var communityHeight: CGFloat = 16 // .caption height, including built-in spacing @@ -45,16 +45,17 @@ struct TileCommentView: View { var content: some View { VStack(alignment: .leading, spacing: Constants.main.standardSpacing) { - titleSection - .typesettingLanguage(.init(languageCode: .english)) - .frame(height: titleHeight, alignment: .topLeading) - .padding(Constants.main.halfSpacing) - .background { - RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius) - .fill(palette.tertiaryGroupedBackground) - } - .paletteBorder(cornerRadius: Constants.main.smallItemCornerRadius) - + if let post = comment.post_ { + titleSection(post: post) + .typesettingLanguage(.init(languageCode: .english)) + .frame(height: titleHeight, alignment: .topLeading) + .padding(Constants.main.halfSpacing) + .background { + RoundedRectangle(cornerRadius: Constants.main.smallItemCornerRadius) + .fill(palette.tertiaryGroupedBackground) + } + .paletteBorder(cornerRadius: Constants.main.smallItemCornerRadius) + } MarkdownText(comment.content, configuration: .caption) .frame(height: contentHeight, alignment: .top) .clipped() @@ -65,8 +66,8 @@ struct TileCommentView: View { } @ViewBuilder - var titleSection: some View { - Text(comment.post.title) + func titleSection(post: any Post) -> some View { + Text(post.title) .lineLimit(2) .foregroundStyle(palette.secondary) .font(.footnote) @@ -102,7 +103,11 @@ struct TileCommentView: View { MenuButton(action: action) } } label: { - TileScoreView(comment) + if let comment = comment as? any Comment2Providing { + TileScoreView(comment) + } else { + Image(systemName: Icons.menu) + } } .onTapGesture {} .popupAnchor() diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/CompactPostView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/CompactPostView.swift index 26b47a201..fa9d343f5 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/CompactPostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/CompactPostView.swift @@ -64,6 +64,7 @@ struct CompactPostView: View { .padding(.bottom, -2) post.taggedTitle(communityContext: communityContext) + .multilineTextAlignment(.leading) .imageScale(.small) .foregroundStyle(post.read_ ?? false ? palette.secondary : palette.primary) .font(.subheadline) 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 e6c8afc1c..9068f183a 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/FeedPostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/FeedPostView.swift @@ -10,14 +10,29 @@ import MlemMiddleware import SwiftUI /// View for rendering posts in feed -struct FeedPostView: View { - @Setting(\.postSize) private var postSize - +struct FeedPostView: View { @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? @Environment(Palette.self) private var palette + @Setting(\.postSize) private var postSize + let post: any Post1Providing - var overridePostSize: PostSize? + let favoredLink: PostViewNavigationLink? + let overridePostSize: PostSize? + + @ViewBuilder let embeddedContent: () -> EmbeddedContent + + init( + post: any Post1Providing, + overridePostSize: PostSize? = nil, + favoredLink: PostViewNavigationLink? = nil, + @ViewBuilder embeddedContent: @escaping () -> EmbeddedContent = { EmptyView() } + ) { + self.post = post + self.overridePostSize = overridePostSize + self.favoredLink = favoredLink + self.embeddedContent = embeddedContent + } var body: some View { content @@ -36,9 +51,9 @@ struct FeedPostView: View { case .tile: TilePostView(post: post) case .headline: - HeadlinePostView(post: post) + HeadlinePostView(post: post, favoredLink: favoredLink, embeddedContent: embeddedContent) case .large: - LargePostView(post: post) + LargePostView(post: post, favoredLink: favoredLink) } } } diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostBodyView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostBodyView.swift new file mode 100644 index 000000000..c20e92398 --- /dev/null +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostBodyView.swift @@ -0,0 +1,63 @@ +// +// HeadlinePostBodyView.swift +// Mlem +// +// Created by Sjmarf on 2024-12-17. +// + +import MlemMiddleware +import SwiftUI + +struct HeadlinePostBodyView: View { + @Environment(Palette.self) var palette + @Environment(\.communityContext) var communityContext: (any Community1Providing)? + + @Setting(\.thumbnailLocation) var thumbnailLocation + @Setting(\.blurNsfw) var blurNsfw + + let post: any Post + + var blurred: Bool { + switch blurNsfw { + case .always: post.nsfw + case .outsideCommunity: post.nsfw && !(communityContext?.nsfw ?? false) + case .never: false + } + } + + var body: some View { + HStack(alignment: .top, spacing: Constants.main.standardSpacing) { + if thumbnailLocation == .left { + ThumbnailImageView( + post: post, + blurred: blurred, + size: .standard, + frame: .init(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize) + ) + } + + VStack(alignment: .leading, spacing: Constants.main.halfSpacing) { + post.taggedTitle(communityContext: communityContext) + .multilineTextAlignment(.leading) + .foregroundStyle((post.read_ ?? false) ? palette.secondary : palette.primary) + .font(.headline) + .imageScale(.small) + + if let host = post.linkHost { + PostLinkHostView(host: host) + .font(.subheadline) + } + } + + if thumbnailLocation == .right { + Spacer() + ThumbnailImageView( + post: post, + blurred: blurred, + size: .standard, + frame: .init(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize) + ) + } + } + } +} diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift index 1bc0b8f10..7ff00a035 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift @@ -9,41 +9,47 @@ import Foundation import MlemMiddleware import SwiftUI -struct HeadlinePostView: View { - @Setting(\.thumbnailLocation) var thumbnailLocation - @Setting(\.showPostCreator) var showCreator +struct HeadlinePostView: View { + @Setting(\.showPostCreator) var alwaysShowCreator @Setting(\.showPersonAvatar) var showPersonAvatar @Setting(\.showCommunityAvatar) var showCommunityAvatar - @Setting(\.blurNsfw) var blurNsfw - @Environment(\.communityContext) var communityContext: (any Community1Providing)? @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? @Environment(Palette.self) var palette: Palette + @Environment(\.communityContext) var communityContext: (any Community1Providing)? let post: any Post1Providing + let embeddedContent: EmbeddedContent + let favoredLink: PostViewNavigationLink? - var blurred: Bool { - switch blurNsfw { - case .always: post.nsfw - case .outsideCommunity: post.nsfw && !(communityContext?.nsfw ?? false) - case .never: false - } + init( + post: any Post1Providing, + favoredLink: PostViewNavigationLink? = nil, + @ViewBuilder embeddedContent: () -> EmbeddedContent = { EmptyView() } + ) { + self.post = post + self.favoredLink = favoredLink + self.embeddedContent = embeddedContent() + } + + var topNavigationLink: PostViewNavigationLink { + if let favoredLink { return favoredLink } + return communityContext == nil ? .community : .creator } var body: some View { - content + contentView .padding(Constants.main.standardSpacing) .background(palette.secondaryGroupedBackground) .environment(\.postContext, post) } - var content: some View { + var contentView: some View { VStack(alignment: .leading, spacing: Constants.main.standardSpacing) { HStack { - if communityContext == nil { - communityLink - } else { - personLink + switch topNavigationLink { + case .community: communityLink + case .creator: personLink } Spacer() @@ -56,43 +62,14 @@ struct HeadlinePostView: View { PostEllipsisMenus(post: post) } - HStack(alignment: .top, spacing: Constants.main.standardSpacing) { - if thumbnailLocation == .left { - ThumbnailImageView( - post: post, - blurred: blurred, - size: .standard, - frame: .init(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize) - ) - } - - VStack(alignment: .leading, spacing: Constants.main.halfSpacing) { - post.taggedTitle(communityContext: communityContext) - .foregroundStyle((post.read_ ?? false) ? palette.secondary : palette.primary) - .font(.headline) - .imageScale(.small) - - if let host = post.linkHost { - PostLinkHostView(host: host) - .font(.subheadline) - } - } - - if thumbnailLocation == .right { - Spacer() - ThumbnailImageView( - post: post, - blurred: blurred, - size: .standard, - frame: .init(width: Constants.main.thumbnailSize, height: Constants.main.thumbnailSize) - ) - } - } + HeadlinePostBodyView(post: post) - if showCreator, communityContext == nil { + if alwaysShowCreator, communityContext == nil { personLink } + embeddedContent + InteractionBarView( post: post, configuration: InteractionBarTracker.main.postInteractionBar, diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift index 4c60aef03..c97c65ae9 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift @@ -10,7 +10,7 @@ import MlemMiddleware import SwiftUI struct LargePostView: View { - @Setting(\.showPostCreator) private var showCreator + @Setting(\.showPostCreator) private var alwaysShowCreator @Setting(\.showPersonAvatar) private var showPersonAvatar @Setting(\.showCommunityAvatar) private var showCommunityAvatar @Setting(\.blurNsfw) var blurNsfw @@ -20,7 +20,18 @@ struct LargePostView: View { @Environment(\.communityContext) private var communityContext let post: any Post1Providing - var isPostPage: Bool = false + let isPostPage: Bool + let favoredLink: PostViewNavigationLink? + + init( + post: any Post1Providing, + isPostPage: Bool = false, + favoredLink: PostViewNavigationLink? = nil + ) { + self.post = post + self.isPostPage = isPostPage + self.favoredLink = favoredLink + } var shouldBlur: Bool { switch blurNsfw { @@ -30,6 +41,11 @@ struct LargePostView: View { } } + var topNavigationLink: PostViewNavigationLink { + if let favoredLink { return favoredLink } + return communityContext == nil || isPostPage ? .community : .creator + } + var body: some View { content .padding(.vertical, Constants.main.standardSpacing) @@ -40,10 +56,9 @@ struct LargePostView: View { var content: some View { VStack(alignment: .leading, spacing: Constants.main.standardSpacing) { HStack { - if communityContext == nil || isPostPage { - communityLink - } else { - personLink + switch topNavigationLink { + case .community: communityLink + case .creator: personLink } Spacer() @@ -62,7 +77,7 @@ struct LargePostView: View { LargePostBodyView(post: post, isPostPage: isPostPage, shouldBlur: shouldBlur) .padding(.horizontal, Constants.main.standardSpacing) - if (showCreator && communityContext == nil) || isPostPage { + if (alwaysShowCreator && communityContext == nil) || isPostPage { personLink .padding(.horizontal, Constants.main.standardSpacing) } diff --git a/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Types.swift b/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Types.swift new file mode 100644 index 000000000..888267c32 --- /dev/null +++ b/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Types.swift @@ -0,0 +1,72 @@ +// +// InboxView+Types.swift +// Mlem +// +// Created by Sjmarf on 2024-12-16. +// + +import SwiftUI + +extension InboxView { + enum Feed: CaseIterable, Identifiable { + case inbox, modMail + + var id: Feed { self } + + var label: LocalizedStringResource { + switch self { + case .inbox: "Inbox" + case .modMail: "Mod Mail" + } + } + + func subtitle(isAdmin: Bool) -> LocalizedStringResource { + switch self { + case .inbox: + return "Replies, mentions and messages" + case .modMail: + if isAdmin { + return "Reports and Registration Applications" + } else { + return "Reports from communities you moderate" + } + } + } + + var systemImage: String { + switch self { + case .inbox: Icons.inbox + case .modMail: Icons.moderation + } + } + + var systemImageFill: String { + switch self { + case .inbox: Icons.inboxFill + case .modMail: Icons.moderationFill + } + } + + var color: Color { + switch self { + case .inbox: Palette.main.inbox + case .modMail: Palette.main.moderation + } + } + } + + enum Tab: CaseIterable, Identifiable { + case all, replies, mentions, messages + + var id: Tab { self } + + var label: LocalizedStringResource { + switch self { + case .all: "All" + case .replies: "Replies" + case .mentions: "Mentions" + case .messages: "Messages" + } + } + } +} diff --git a/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift b/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift index 607b0f826..28d775d04 100644 --- a/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift +++ b/Mlem/App/Views/Root/Tabs/Inbox/InboxView+Views.swift @@ -5,9 +5,55 @@ // Created by Sjmarf on 22/09/2024. // +import MlemMiddleware import SwiftUI extension InboxView { + @ViewBuilder + var inboxFeedView: some View { + LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { + Section { + ForEach(feedLoader.items, id: \.actorId) { item in + Group { + switch item { + case let .message(message): + MessageView(message: message, isInInbox: true) + case let .reply(reply): + ReplyView(reply: reply) + } + } + .padding([.horizontal, .bottom], Constants.main.standardSpacing) + .onAppear { + do { + try inboxFeedLoader.loadIfThreshold(item) + } catch { + handleError(error) + } + } + } + + EndOfFeedView(loadingState: feedLoader.loadingState, loadMore: nil, viewType: .cartoon) + } header: { sectionHeader } + } + } + + @ViewBuilder + var modMailFeedView: some View { + LazyVStack(spacing: 0) { + if let reports { + ForEach(reports, id: \.cacheId) { report in + ReportView(report: report) + .padding([.horizontal, .bottom], Constants.main.standardSpacing) + } + } else { + ProgressView() + .padding(.top) + } + } + .onAppear(perform: loadReports) + .padding(.top, Constants.main.standardSpacing) + } + @ViewBuilder var sectionHeader: some View { BubblePicker( @@ -89,6 +135,33 @@ extension InboxView { } } + @ViewBuilder + var headerView: some View { + let availableFeeds = availableFeeds + Menu { + if availableFeeds.count > 1 { + Picker("Feed", selection: $selectedFeed) { + ForEach(availableFeeds) { feedType in + Label(String(localized: feedType.label), systemImage: feedType.systemImage) + .tag(feedType) + } + } + } + } label: { + FeedHeaderView( + feedDescription: .init( + label: selectedFeed.label, + subtitle: selectedFeed.subtitle(isAdmin: appState.firstApi.isAdmin), + color: { _ in selectedFeed.color }, + iconName: selectedFeed.systemImage, + iconNameFill: selectedFeed.systemImageFill, + iconScaleFactor: 0.5 + ), + dropdownStyle: availableFeeds.count > 1 ? .enabled(showBadge: false) : .disabled + ) + } + } + @ViewBuilder var signedOutInfoView: some View { VStack { @@ -132,4 +205,27 @@ extension InboxView { .frame(minWidth: 100) } } + + func loadReports() { + if reports == nil { + Task { + do { + async let postReports = await appState.firstApi.getPostReports() + async let commentReports = await appState.firstApi.getCommentReports() + async let messageReports: [Report] = await { + if await appState.firstApi.isAdmin { + return try await appState.firstApi.getMessageReports() + } else { + return [] + } + }() + + let combined = try await (postReports + commentReports + messageReports) + self.reports = combined.sorted { $0.created > $1.created } + } catch { + handleError(error) + } + } + } + } } diff --git a/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift b/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift index 2121c57ec..59c57e6e9 100644 --- a/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift +++ b/Mlem/App/Views/Root/Tabs/Inbox/InboxView.swift @@ -10,21 +10,6 @@ import MlemMiddleware import SwiftUI struct InboxView: View { - enum Tab: CaseIterable, Identifiable { - case all, replies, mentions, messages - - var id: Tab { self } - - var label: LocalizedStringResource { - switch self { - case .all: "All" - case .replies: "Replies" - case .mentions: "Mentions" - case .messages: "Messages" - } - } - } - @Environment(NavigationLayer.self) var navigation @Environment(AppState.self) var appState @Environment(Palette.self) var palette @@ -32,8 +17,11 @@ struct InboxView: View { @Setting(\.showReadInInbox) var showRead @State var headerPinned: Bool = false + @State var selectedFeed: Feed = .inbox @State var selectedTab: Tab = .all + @State var reports: [Report]? + @State var replyFeedLoader: ReplyFeedLoader @State var mentionFeedLoader: MentionFeedLoader @State var messageFeedLoader: MessageFeedLoader @@ -43,19 +31,6 @@ struct InboxView: View { @State var waitingOnMarkAllAsRead: Bool = false @State var markAllAsReadTrigger: Bool = false - var feedLoader: StandardFeedLoader { - switch selectedTab { - case .all: - inboxFeedLoader - case .replies: - replyFeedLoader - case .mentions: - mentionFeedLoader - case .messages: - messageFeedLoader - } - } - init() { @Setting(\.internetSpeed) var internetSpeed @Setting(\.showReadInInbox) var showRead @@ -93,6 +68,26 @@ struct InboxView: View { self._inboxFeedLoader = .init(wrappedValue: inboxFeedLoader) } + var feedLoader: StandardFeedLoader { + switch selectedTab { + case .all: + inboxFeedLoader + case .replies: + replyFeedLoader + case .mentions: + mentionFeedLoader + case .messages: + messageFeedLoader + } + } + + var availableFeeds: [Feed] { + if appState.firstApi.isAdmin || !(appState.firstPerson?.moderatedCommunities.isEmpty ?? true) { + return [.inbox, .modMail] + } + return [.inbox] + } + var body: some View { if appState.firstSession is GuestSession { signedOutInfoView @@ -149,17 +144,7 @@ struct InboxView: View { var content: some View { FancyScrollView { VStack(spacing: 0) { - FeedHeaderView( - feedDescription: .init( - label: "Inbox", - subtitle: "Replies, mentions and messages", - color: { $0.inbox }, - iconName: Icons.inbox, - iconNameFill: Icons.inboxFill, - iconScaleFactor: 0.5 - ), - dropdownStyle: .disabled - ) + headerView GeometryReader { geo in Color.red.preference( key: ScrollOffsetKey.self, @@ -172,29 +157,11 @@ struct InboxView: View { headerPinned = value } }) - LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { - Section { - ForEach(feedLoader.items, id: \.actorId) { item in - Group { - switch item { - case let .message(message): - MessageView(message: message) - case let .reply(reply): - ReplyView(reply: reply) - } - } - .padding([.horizontal, .bottom], Constants.main.standardSpacing) - .onAppear { - do { - try inboxFeedLoader.loadIfThreshold(item) - } catch { - handleError(error) - } - } - } - - EndOfFeedView(loadingState: feedLoader.loadingState, loadMore: nil, viewType: .cartoon) - } header: { sectionHeader } + switch selectedFeed { + case .inbox: + inboxFeedView + case .modMail: + modMailFeedView } } } diff --git a/Mlem/App/Views/Shared/CommentView.swift b/Mlem/App/Views/Shared/CommentView.swift index db81f713c..997acba34 100644 --- a/Mlem/App/Views/Shared/CommentView.swift +++ b/Mlem/App/Views/Shared/CommentView.swift @@ -9,10 +9,11 @@ import LemmyMarkdownUI import MlemMiddleware import SwiftUI -struct CommentView: View { +struct CommentView: View { @Environment(Palette.self) private var palette @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? @Environment(\.communityContext) var communityContext: (any Community1Providing)? + @Environment(\.reportContext) private var reportContext: Report? @Setting(\.compactComments) var compactComments @Setting(\.tapCommentsToCollapse) var tapCommentsToCollapse @@ -21,9 +22,24 @@ struct CommentView: View { private let indent: CGFloat = 10 let comment: any Comment1Providing - var highlight: Bool = false - var inFeed: Bool = false // flag to suppress threading/collapsing behavior - var depthOffset: Int = 0 + let embeddedContent: EmbeddedContent + let inFeed: Bool + let highlight: Bool + let depthOffset: Int + + init( + comment: any Comment1Providing, + inFeed: Bool = false, // flag to suppress threading/collapsing behavior + highlight: Bool = false, + depthOffset: Int = 0, + @ViewBuilder embeddedContent: () -> EmbeddedContent = { EmptyView() } + ) { + self.comment = comment + self.inFeed = inFeed + self.highlight = highlight + self.depthOffset = depthOffset + self.embeddedContent = embeddedContent() + } var depth: Int { inFeed ? 0 : comment.depth - depthOffset @@ -89,6 +105,7 @@ struct CommentView: View { } .id("\(comment.id)_commment_footer") } + embeddedContent if !compactComments { InteractionBarView( comment: comment, @@ -109,11 +126,8 @@ struct CommentView: View { .background(highlight ? palette.accent.opacity(0.2) : .clear) .background(palette.secondaryGroupedBackground) .clipShape(.rect(cornerRadius: Constants.main.standardSpacing)) - .quickSwipes(comment.swipeActions(behavior: .standard, commentTreeTracker: commentTreeTracker)) .contentShape(.interaction, .rect) .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing)) - .contextMenu { comment.allMenuActions(commentTreeTracker: commentTreeTracker) } - .clipShape(.rect(cornerRadius: Constants.main.standardSpacing)) .environment(\.commentContext, comment) .paletteBorder(cornerRadius: Constants.main.standardSpacing) } @@ -126,7 +140,7 @@ struct CommentView: View { if moderatorActionGrouping == .separateMenu { if comment.canModerate { EllipsisMenu(systemImage: Icons.moderation, size: 24) { - comment.moderatorMenuActions() + comment.moderatorMenuActions(report: reportContext) } } EllipsisMenu(size: 24) { @@ -134,7 +148,7 @@ struct CommentView: View { } } else { EllipsisMenu(size: 24) { - comment.allMenuActions(commentTreeTracker: commentTreeTracker) + comment.allMenuActions(commentTreeTracker: commentTreeTracker, report: reportContext) } } } diff --git a/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift b/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift index eff550a40..d77ab2211 100644 --- a/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift +++ b/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift @@ -34,6 +34,9 @@ extension ExpandedPostView { highlight: [scrollTargetedComment?.actorId, highlightedComment?.actorId].contains(comment.actorId), depthOffset: tracker.proposedDepthOffset ) + .quickSwipes(comment.swipeActions(behavior: .standard, commentTreeTracker: tracker)) + .contextMenu { comment.allMenuActions() } + .paletteBorder(cornerRadius: Constants.main.standardSpacing) .transition(.move(edge: .top).combined(with: .opacity)) .zIndex(1000 - Double(comment.depth)) .anchorPreference( diff --git a/Mlem/App/Views/Shared/MessageView.swift b/Mlem/App/Views/Shared/MessageView.swift index 06396a437..af5580f27 100644 --- a/Mlem/App/Views/Shared/MessageView.swift +++ b/Mlem/App/Views/Shared/MessageView.swift @@ -9,21 +9,38 @@ import LemmyMarkdownUI import MlemMiddleware import SwiftUI -struct MessageView: View { +struct MessageView: View { @Environment(Palette.self) private var palette @Environment(AppState.self) private var appState + @Environment(\.reportContext) private var reportContext + + @Setting(\.moderatorActionGrouping) var moderatorActionGrouping let message: any Message + let isInInbox: Bool + let embeddedContent: EmbeddedContent + + init( + message: any Message, + isInInbox: Bool = false, + @ViewBuilder embeddedContent: () -> EmbeddedContent = { EmptyView() } + ) { + self.message = message + self.isInInbox = isInInbox + self.embeddedContent = embeddedContent() + } var body: some View { VStack(alignment: .leading, spacing: Constants.main.standardSpacing) { HStack { FullyQualifiedLinkView(entity: message.creator_, labelStyle: .small, showAvatar: true) Spacer() - Image(systemName: message.isOwnMessage ? Icons.send : Icons.message) - .symbolVariant(message.read ? .none : .fill) - .foregroundStyle(palette.accent) - EllipsisMenu(size: 24) { message.menuActions() } + if isInInbox { + Image(systemName: message.isOwnMessage ? Icons.send : Icons.message) + .symbolVariant(message.read ? .none : .fill) + .foregroundStyle(palette.accent) + } + ellipsisMenus .frame(height: 10) } if message.deleted { @@ -42,6 +59,7 @@ struct MessageView: View { } .font(.caption) .foregroundStyle(palette.secondary) + embeddedContent } .padding(.vertical, 2) .padding(Constants.main.standardSpacing) @@ -51,7 +69,26 @@ struct MessageView: View { .quickSwipes(message.swipeActions(behavior: .standard)) .clipShape(.rect(cornerRadius: Constants.main.standardSpacing)) .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing)) - .contextMenu { message.menuActions() } + .contextMenu { message.allMenuActions(report: reportContext) } .paletteBorder(cornerRadius: Constants.main.standardSpacing) } + + var ellipsisMenus: some View { + HStack { + if moderatorActionGrouping == .separateMenu { + if message.api.isAdmin { + EllipsisMenu(systemImage: Icons.moderation, size: 24) { + message.moderatorMenuActions(report: reportContext) + } + } + EllipsisMenu(size: 24) { + message.basicMenuActions() + } + } else { + EllipsisMenu(size: 24) { + message.allMenuActions(report: reportContext) + } + } + } + } } diff --git a/Mlem/App/Views/Shared/PostEllipsisMenus.swift b/Mlem/App/Views/Shared/PostEllipsisMenus.swift index 2324e3276..aca690db4 100644 --- a/Mlem/App/Views/Shared/PostEllipsisMenus.swift +++ b/Mlem/App/Views/Shared/PostEllipsisMenus.swift @@ -10,6 +10,7 @@ import SwiftUI struct PostEllipsisMenus: View { @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? + @Environment(\.reportContext) private var reportContext: Report? @Setting(\.moderatorActionGrouping) var moderatorActionGrouping @@ -26,7 +27,7 @@ struct PostEllipsisMenus: View { if moderatorActionGrouping == .separateMenu { if post.canModerate { EllipsisMenu(systemImage: Icons.moderation, size: 24) { - post.moderatorMenuActions() + post.moderatorMenuActions(showAllActions: false, report: reportContext) } } EllipsisMenu(size: 24) { @@ -34,7 +35,11 @@ struct PostEllipsisMenus: View { } } else { EllipsisMenu(size: 24) { - post.allMenuActions(showAllActions: false, commentTreeTracker: commentTreeTracker) + post.allMenuActions( + showAllActions: false, + commentTreeTracker: commentTreeTracker, + report: reportContext + ) } } } diff --git a/Mlem/App/Views/Shared/ReportView.swift b/Mlem/App/Views/Shared/ReportView.swift new file mode 100644 index 000000000..80538907e --- /dev/null +++ b/Mlem/App/Views/Shared/ReportView.swift @@ -0,0 +1,115 @@ +// +// ReportView.swift +// Mlem +// +// Created by Sjmarf on 2024-12-16. +// + +import LemmyMarkdownUI +import MlemMiddleware +import SwiftUI + +struct ReportView: View { + @Environment(Palette.self) var palette + + let report: Report + + var body: some View { + targetView + .buttonStyle(.empty) + .background(palette.secondaryGroupedBackground) + .clipShape(.rect(cornerRadius: Constants.main.standardSpacing)) + .environment(\.reportContext, report) + } + + @ViewBuilder + var targetView: some View { + switch report.target { + case let .post(post): + NavigationLink(.post(post)) { + FeedPostView(post: post, overridePostSize: .headline, favoredLink: .creator) { embeddedContent } + } + case let .comment(comment): + NavigationLink(.comment(comment)) { + FeedCommentView(comment: comment, overrideIsTiled: false) { embeddedContent } + } + case let .message(message): + MessageView(message: message) { embeddedContent } + case let .legacyPost(post, community: community, creator: creator): + legacyPostView(post: post, community: community, creator: creator) + case let .legacyComment(comment, community: community, creator: creator): + legacyCommentView(comment: comment, community: community, creator: creator) + } + } + + @ViewBuilder + var embeddedContent: some View { + VStack(alignment: .leading) { + Text("Reported \(report.created.getRelativeTime()) by \(report.creator.fullName ?? "")") + .foregroundStyle(.secondary) // No palette! + .font(.footnote) + Text(report.reason) + } + .foregroundStyle(palette.warning) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(Constants.main.standardSpacing) + .background( + palette.warning.opacity(0.1), + in: .rect(cornerRadius: Constants.main.standardSpacing) + ) + if report.resolved, let resolver = report.resolver { + Label("Resolved by \(resolver.fullName ?? "")", systemImage: Icons.successCircleFill) + .foregroundStyle(palette.positive) + .font(.footnote) + .padding(.horizontal, Constants.main.halfSpacing) + } + } + + @ViewBuilder + func legacyPostView(post: Post1, community: Community1, creator: Person1) -> some View { + NavigationLink(.post(post)) { + VStack(alignment: .leading, spacing: Constants.main.standardSpacing) { + HStack { + FullyQualifiedLinkView(entity: creator, labelStyle: .medium, showAvatar: true) + Spacer() + resolveButton + } + HeadlinePostBodyView(post: post) + embeddedContent + } + .padding(Constants.main.standardSpacing) + .background(palette.secondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing)) + .paletteBorder(cornerRadius: Constants.main.standardSpacing) + } + } + + @ViewBuilder + func legacyCommentView(comment: Comment1, community: Community1, creator: Person1) -> some View { + NavigationLink(.comment(comment)) { + VStack(alignment: .leading, spacing: Constants.main.standardSpacing) { + HStack { + FullyQualifiedLinkView(entity: creator, labelStyle: .medium, showAvatar: true) + Spacer() + resolveButton + } + Markdown(comment.content, configuration: .default) + embeddedContent + } + .padding(Constants.main.standardSpacing) + .background(palette.secondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing)) + .paletteBorder(cornerRadius: Constants.main.standardSpacing) + } + } + + @ViewBuilder + var resolveButton: some View { + Button( + report.resolved ? "Unresolve" : "Resolve", + systemImage: report.resolved ? Icons.successCircleFill : Icons.successCircle + ) { + report.toggleResolved(feedback: [.haptic]) + } + .foregroundStyle(palette.positive) + .labelStyle(.iconOnly) + } +} diff --git a/Mlem/Localizable.xcstrings b/Mlem/Localizable.xcstrings index 3cf2e3838..7c62a59a2 100644 --- a/Mlem/Localizable.xcstrings +++ b/Mlem/Localizable.xcstrings @@ -636,6 +636,9 @@ }, "Fediseer GUI" : { + }, + "Feed" : { + }, "Feed is outdated" : { @@ -891,6 +894,9 @@ }, "Mlem will always try to load from the proxy first." : { + }, + "Mod Mail" : { + }, "Moderated" : { @@ -1191,9 +1197,25 @@ }, "Report" : { + }, + "Reported %@ by %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Reported %1$@ by %2$@" + } + } + } + }, + "Reports and Registration Applications" : { + }, "Reports Email Admins" : { + }, + "Reports from communities you moderate" : { + }, "Requires Application" : { @@ -1203,6 +1225,12 @@ }, "Reset Settings State" : { + }, + "Resolve" : { + + }, + "Resolved by %@" : { + }, "Response Time" : { @@ -1719,6 +1747,9 @@ }, "Unpin From Instance" : { + }, + "Unresolve" : { + }, "Unsave" : {