Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content flag category selection #1513

Merged
merged 22 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
03FE3F8C2C87BC9500D25810 /* text_note_multiple_media.json in Resources */ = {isa = PBXBuildFile; fileRef = 03FE3F8A2C87BC9500D25810 /* text_note_multiple_media.json */; };
042406F32C907A15008F2A21 /* NosToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042406F22C907A15008F2A21 /* NosToggle.swift */; };
04368D2B2C99A2C400DEAA2E /* FlagOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04368D2A2C99A2C400DEAA2E /* FlagOption.swift */; };
04368D312C99A78800DEAA2E /* NosRadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04368D302C99A78800DEAA2E /* NosRadioButton.swift */; };
04368D4B2C99CFC700DEAA2E /* ContentFlagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04368D4A2C99CFC700DEAA2E /* ContentFlagView.swift */; };
0496D6312C975E6900D29375 /* FlagOptionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0496D6302C975E6900D29375 /* FlagOptionPicker.swift */; };
2D06BB9D2AE249D70085F509 /* ThreadRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D06BB9C2AE249D70085F509 /* ThreadRootView.swift */; };
2D4010A22AD87DF300F93AD4 /* KnownFollowersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4010A12AD87DF300F93AD4 /* KnownFollowersView.swift */; };
3A1C296F2B2A537C0020B753 /* Moderation.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 3A1C296E2B2A537C0020B753 /* Moderation.xcstrings */; };
Expand Down Expand Up @@ -138,13 +141,13 @@
3FFB1D9729A6BBEC002A755D /* Collection+SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFB1D9529A6BBEC002A755D /* Collection+SafeSubscript.swift */; };
3FFB1D9C29A7DF9D002A755D /* StackedAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */; };
3FFF3BD029A9645F00DD0B72 /* AuthorReference+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F43C47529A9625700E896A0 /* AuthorReference+CoreDataClass.swift */; };
500899F32C95C1F900834588 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
50089A012C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */; };
50089A022C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */; };
50089A0C2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */; };
50089A0D2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */; };
500899F32C95C1F900834588 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
502B6C3D2C9462A400446316 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
50089A172C98678600834588 /* View+ListRowGradientBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A162C98678600834588 /* View+ListRowGradientBackground.swift */; };
502B6C3D2C9462A400446316 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
5044546E2C90726A00251A7E /* Event+Fetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5044546D2C90726A00251A7E /* Event+Fetching.swift */; };
504454702C90728500251A7E /* Event+Hydration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5044546F2C90728500251A7E /* Event+Hydration.swift */; };
504454712C90728E00251A7E /* Event+Fetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5044546D2C90726A00251A7E /* Event+Fetching.swift */; };
Expand Down Expand Up @@ -649,6 +652,9 @@
03FE3F8A2C87BC9500D25810 /* text_note_multiple_media.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = text_note_multiple_media.json; sourceTree = "<group>"; };
042406F22C907A15008F2A21 /* NosToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NosToggle.swift; sourceTree = "<group>"; };
04368D2A2C99A2C400DEAA2E /* FlagOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagOption.swift; sourceTree = "<group>"; };
04368D302C99A78800DEAA2E /* NosRadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NosRadioButton.swift; sourceTree = "<group>"; };
04368D4A2C99CFC700DEAA2E /* ContentFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFlagView.swift; sourceTree = "<group>"; };
0496D6302C975E6900D29375 /* FlagOptionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagOptionPicker.swift; sourceTree = "<group>"; };
2D06BB9C2AE249D70085F509 /* ThreadRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadRootView.swift; sourceTree = "<group>"; };
2D4010A12AD87DF300F93AD4 /* KnownFollowersView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KnownFollowersView.swift; sourceTree = "<group>"; };
3A1C296E2B2A537C0020B753 /* Moderation.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Moderation.xcstrings; sourceTree = "<group>"; };
Expand All @@ -671,8 +677,8 @@
3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackedAvatarsView.swift; sourceTree = "<group>"; };
50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEvent+Kinds.swift"; sourceTree = "<group>"; };
50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentUser+PublishEvents.swift"; sourceTree = "<group>"; };
502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationRegistrar.swift; sourceTree = "<group>"; };
50089A162C98678600834588 /* View+ListRowGradientBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ListRowGradientBackground.swift"; sourceTree = "<group>"; };
502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationRegistrar.swift; sourceTree = "<group>"; };
5044546D2C90726A00251A7E /* Event+Fetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+Fetching.swift"; sourceTree = "<group>"; };
5044546F2C90728500251A7E /* Event+Hydration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+Hydration.swift"; sourceTree = "<group>"; };
5045540C2C81E10C0044ECAE /* EditableAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableAvatarView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1397,6 +1403,14 @@
path = OpenGraph;
sourceTree = "<group>";
};
04368D542C99D32B00DEAA2E /* Moderation */ = {
isa = PBXGroup;
children = (
04368D4A2C99CFC700DEAA2E /* ContentFlagView.swift */,
);
path = Moderation;
sourceTree = "<group>";
};
3AAB61B12B24CC8A00717A07 /* Extensions */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1461,6 +1475,7 @@
3FFB1D88299FF37C002A755D /* AvatarView.swift */,
C95D68A0299E6D3E00429F86 /* BioView.swift */,
C9DFA968299BEC33006929C1 /* CardStyle.swift */,
0496D6302C975E6900D29375 /* FlagOptionPicker.swift */,
C9B678E629F01A8500303F33 /* FullscreenProgressView.swift */,
C9CDBBA329A8FA2900C555C7 /* GoldenPostView.swift */,
C930E0562BA49DAD002B5776 /* GridPattern.swift */,
Expand All @@ -1481,6 +1496,7 @@
3F60F42829B27D3E000D62C4 /* ThreadView.swift */,
C913DA0D2AEB3265003BDD6D /* WarningView.swift */,
C9CE5B132A0172CF008E198C /* WebView.swift */,
04368D302C99A78800DEAA2E /* NosRadioButton.swift */,
03618C642C8267A900BCBC55 /* Author */,
03618C232C82668600BCBC55 /* Button */,
03618C7E2C82685500BCBC55 /* Event */,
Expand Down Expand Up @@ -1818,7 +1834,6 @@
5B6EB48D29EDBE0E006E750C /* NoteParser.swift */,
C9AC31AC2A55E0BD00A94E5A /* NotificationViewModel.swift */,
C9CF23162A38A58B00EBEC31 /* ParseQueue.swift */,
5BCA95D12C8A5F0D00A52D1A /* PreviewEventRepository.swift */,
C9F0BB6A29A503D6000547FC /* PublicKey.swift */,
C96D39262B61B6D200D3D0A1 /* RawNostrID.swift */,
C9C2B78429E073E300548B4A /* RelaySubscription.swift */,
Expand Down Expand Up @@ -1906,6 +1921,7 @@
65BD8DC12BDAF2C300802039 /* Discover */,
03618B112C825D8700BCBC55 /* Fixtures */,
C96877B32B4EDCCF0051ED2F /* Home */,
04368D542C99D32B00DEAA2E /* Moderation */,
C9CFF6D02AB241EB00D4B368 /* Modifiers */,
03618C6D2C8267E600BCBC55 /* Note */,
C9EE3E652A053CF1008A7491 /* NoteComposer */,
Expand Down Expand Up @@ -2258,6 +2274,7 @@
C987F81A29BA4D0E00B44E7A /* ActionButton.swift in Sources */,
C9E37E122A1E7EC5003D4B0A /* PreviewContainer.swift in Sources */,
C9A0DAF829C92F4500466635 /* UNSAPI.swift in Sources */,
04368D312C99A78800DEAA2E /* NosRadioButton.swift in Sources */,
5B79F60B2B98ACA0002DA9BE /* PickYourUsernameSheet.swift in Sources */,
5BFF66B62A58A8A000AA79DD /* MutesView.swift in Sources */,
C913DA0A2AEAF52B003BDD6D /* NoteWarningController.swift in Sources */,
Expand Down Expand Up @@ -2311,6 +2328,7 @@
C9EE3E632A053910008A7491 /* ExpirationTimeOption.swift in Sources */,
C9A0DAE029C697A100466635 /* AboutView.swift in Sources */,
C9E8C1132B081E9C002D46B0 /* UNSNameView.swift in Sources */,
0496D6312C975E6900D29375 /* FlagOptionPicker.swift in Sources */,
A351E1A229BA92240009B7F6 /* ProfileEditView.swift in Sources */,
C9DFA969299BEC33006929C1 /* CardStyle.swift in Sources */,
C95D68AD299E721700429F86 /* ProfileView.swift in Sources */,
Expand All @@ -2324,6 +2342,7 @@
5BFBB28B2BD9D79F002E909F /* URLParser.swift in Sources */,
C930055F2A6AF8320098CA9E /* LoadingContent.swift in Sources */,
5B79F6462BA11725002DA9BE /* WizardSheetVStack.swift in Sources */,
04368D4B2C99CFC700DEAA2E /* ContentFlagView.swift in Sources */,
C930E0572BA49DAD002B5776 /* GridPattern.swift in Sources */,
C9A6C74D2AD98E2A001F9500 /* UNSNameTakenView.swift in Sources */,
C92F015B2AC4D74E00972489 /* NosTextEditor.swift in Sources */,
Expand Down
108 changes: 108 additions & 0 deletions Nos/Views/Components/FlagOptionPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import SwiftUI
/// Displays a list of selectable flag options
struct FlagOptionPicker: View {
@Binding private var selectedOption: FlagOption?
var options: [FlagOption]
var title: String
var subTitle: String?

init(selectedOption: Binding<FlagOption?>, options: [FlagOption], title: String, subTitle: String?) {
self._selectedOption = selectedOption
self.options = options
self.title = title
self.subTitle = subTitle
}

var body: some View {
VStack(alignment: .leading) {
HeaderView(text: title)
pelumy marked this conversation as resolved.
Show resolved Hide resolved

if let subTitle = subTitle {
HeaderView(text: subTitle)
}
flagOptionsListView
}
.padding()
}

private var flagOptionsListView: some View {
VStack(alignment: .leading) {
ForEach(options) { flag in
FlagPickerRow(flag: flag, selection: $selectedOption)
BeveledSeparator()
}
}
.background(LinearGradient.cardBackground)
.clipShape(RoundedRectangle(cornerRadius: 15))
}
}

/// A single row for a single flag option
pelumy marked this conversation as resolved.
Show resolved Hide resolved
struct FlagPickerRow: View {
var flag: FlagOption
@Binding var selection: FlagOption?

var isSelected: Bool {
selection?.id == flag.id
}

var body: some View {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 8) {
joshuatbrown marked this conversation as resolved.
Show resolved Hide resolved
Text(flag.title)
.foregroundColor(.primaryTxt)
.font(.clarity(.regular))

if let description = flag.description {
Text(description)
.foregroundColor(.secondaryTxt)
.font(.clarity(.regular, textStyle: .footnote))
.lineSpacing(8)
}
}

Spacer()

Button {
selection = flag
} label: {
NosRadioButton(isSelected: isSelected)
pelumy marked this conversation as resolved.
Show resolved Hide resolved
}
}
.padding(12)
pelumy marked this conversation as resolved.
Show resolved Hide resolved
}
}

private struct HeaderView: View {
joshuatbrown marked this conversation as resolved.
Show resolved Hide resolved
var text: String
var body: some View {
Text(text)
.lineSpacing(5)
.foregroundColor(.primaryTxt)
.font(.clarity(.bold))
.padding(.bottom, 28)
}
}

struct FlagOptionPicker_Previews: PreviewProvider {
pelumy marked this conversation as resolved.
Show resolved Hide resolved
struct PreviewWrapper: View {
@State private var selectedFlag: FlagOption?

var body: some View {
FlagOptionPicker(
selectedOption: $selectedFlag,
options: FlagOption.flagContentCategories,
title: "Create a tag for this content that other people in your network can see.",
subTitle: "Select a tag for the content"
)
.onAppear {
selectedFlag = FlagOption.flagContentCategories.first
}
.background(Color.appBg)
}
}

static var previews: some View {
PreviewWrapper()
}
}
65 changes: 65 additions & 0 deletions Nos/Views/Components/NosRadioButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import SwiftUI
/// A custom radio button.
struct NosRadioButton: View {
var isSelected: Bool
var body: some View {
ZStack {
RadioButtonBackground()
if isSelected {
RadioButtonSelectedIndicator()
}
}
}
}

/// A custom background for a radio button.
private struct RadioButtonBackground: View {
var body: some View {
ZStack {
Circle()
.fill(
LinearGradient(
colors: [Color.radioButtonBgTop, Color.radioButtonBgBottom],
startPoint: .top,
endPoint: .bottom
)
)
.frame(width: 25, height: 25)
// Inner shadow effect
.overlay(
Circle()
.fill(
LinearGradient(
colors: [Color.radioButtonBgTop, Color.radioButtonBgBottom],
startPoint: .top,
endPoint: .bottom
)
)
.stroke(Color.radioButtonInnerDropShadow, lineWidth: 1)
.blur(radius: 1.67)
.offset(x: 0, y: 0.67)
.mask(Circle()) // Ensures the inner shadow stays within the circle's shape
)

// Outer shadow effect
.shadow(color: Color.radioButtonOuterDropShadow, radius: 0, x: 0, y: 0.99)
}
}
}

/// A colorful selector (inner circle) of a radio button with a gradient fill.
joshuatbrown marked this conversation as resolved.
Show resolved Hide resolved
private struct RadioButtonSelectedIndicator: View {
var body: some View {
Circle()
.fill(LinearGradient.verticalAccentPrimary)
.frame(width: 17, height: 17)
}
}

#Preview("Selected") {
NosRadioButton(isSelected: true)
}

#Preview("Not Selected") {
NosRadioButton(isSelected: false)
}
27 changes: 27 additions & 0 deletions Nos/Views/Moderation/ContentFlagView.swift
joshuatbrown marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftUI

/// Displays pickers for selecting content flag option category, with additional stages shown
/// based on previous selections.
struct ContentFlagView: View {
@Binding var selectedFlagOptionCategory: FlagOption?
var title: String
var subTitle: String?
joshuatbrown marked this conversation as resolved.
Show resolved Hide resolved

var body: some View {
ScrollView {
VStack {
FlagOptionPicker(
selectedOption: $selectedFlagOptionCategory,
options: FlagOption.flagContentCategories,
title: title,
subTitle: subTitle
)
}
}
.background(Color.appBg)
}
}

#Preview {
ContentFlagView(selectedFlagOptionCategory: .constant(nil), title: "Title")
}
61 changes: 58 additions & 3 deletions Nos/Views/Modifiers/ReportMenuModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,66 @@ struct ReportMenuModifier: ViewModifier {
@State private var confirmReport = false
@State private var showMuteDialog = false
@State private var confirmationDialogState: ConfirmationDialogState<UserSelection>?

@State private var selectedFlagOption: FlagOption?

@Environment(\.managedObjectContext) private var viewContext
@Dependency(\.featureFlags) private var featureFlags

func body(content: Content) -> some View {
Group {
if featureFlags.isEnabled(.newModerationFlow) {
newModerationFlow(content: content)
} else {
oldModerationFlow(content: content)
}
}
}

/// Displays the moderation flow based on the reported object type. The old flow is still displayed for the author.
@ViewBuilder
private func newModerationFlow(content: Content) -> some View {
switch reportedObject {
case .note:
content
.sheet(isPresented: $isPresented) {
flagOptionPickerForSelection(userSelection)
}
case .author:
oldModerationFlow(content: content)
}
}

/// Provides the flag option picker based on the user's selection.
/// - Parameter userSelection: The optional user selection that determines the flagging options.
/// - Returns: A view containing the flag option picker for the current selection.
private func flagOptionPickerForSelection(_ userSelection: UserSelection?) -> some View {
var title = String(localized: .localizable.reportContent)
var subTitle = String(localized: .localizable.reportContentMessage)

guard let userSelection = userSelection else {
return ContentFlagView(
selectedFlagOptionCategory: $selectedFlagOption,
title: title,
subTitle: subTitle
)
}

switch userSelection {
default:
title = String(localized: .localizable.reportContent)
subTitle = String(localized: .localizable.reportContentMessage)
}

return ContentFlagView(
selectedFlagOptionCategory: $selectedFlagOption,
title: title,
subTitle: subTitle
)
}

// swiftlint:disable function_body_length
func body(content: Content) -> some View {
@ViewBuilder
func oldModerationFlow(content: Content) -> some View {
content
// ReportCategory menu
.confirmationDialog(unwrapping: $confirmationDialogState, action: processUserSelection)
Expand Down Expand Up @@ -85,7 +140,7 @@ struct ReportMenuModifier: ViewModifier {
}
}
// swiftlint:enable function_body_length

func processUserSelection(_ userSelection: UserSelection?) {
self.userSelection = userSelection

Expand Down
Loading