Skip to content

Commit

Permalink
Merge pull request #1513 from planetary-social/content-flag-category
Browse files Browse the repository at this point in the history
Content flag category selection
  • Loading branch information
pelumy authored Sep 23, 2024
2 parents 3d3fb8d + b4c9bbe commit 343221e
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 4 deletions.
20 changes: 20 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,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 @@ -652,6 +655,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 Down Expand Up @@ -1401,6 +1407,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 @@ -1465,6 +1479,7 @@
3FFB1D88299FF37C002A755D /* AvatarView.swift */,
C95D68A0299E6D3E00429F86 /* BioView.swift */,
C9DFA968299BEC33006929C1 /* CardStyle.swift */,
0496D6302C975E6900D29375 /* FlagOptionPicker.swift */,
C9B678E629F01A8500303F33 /* FullscreenProgressView.swift */,
C9CDBBA329A8FA2900C555C7 /* GoldenPostView.swift */,
C930E0562BA49DAD002B5776 /* GridPattern.swift */,
Expand All @@ -1485,6 +1500,7 @@
3F60F42829B27D3E000D62C4 /* ThreadView.swift */,
C913DA0D2AEB3265003BDD6D /* WarningView.swift */,
C9CE5B132A0172CF008E198C /* WebView.swift */,
04368D302C99A78800DEAA2E /* NosRadioButton.swift */,
03618C642C8267A900BCBC55 /* Author */,
03618C232C82668600BCBC55 /* Button */,
03618C7E2C82685500BCBC55 /* Event */,
Expand Down Expand Up @@ -1909,6 +1925,7 @@
65BD8DC12BDAF2C300802039 /* Discover */,
03618B112C825D8700BCBC55 /* Fixtures */,
C96877B32B4EDCCF0051ED2F /* Home */,
04368D542C99D32B00DEAA2E /* Moderation */,
C9CFF6D02AB241EB00D4B368 /* Modifiers */,
03618C6D2C8267E600BCBC55 /* Note */,
C9EE3E652A053CF1008A7491 /* NoteComposer */,
Expand Down Expand Up @@ -2261,6 +2278,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 @@ -2314,6 +2332,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 @@ -2327,6 +2346,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
22 changes: 22 additions & 0 deletions Nos/Assets/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -4405,6 +4405,28 @@
}
}
},
"flagContentCategoryDescription" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Select a tag for the content"
}
}
}
},
"flagContentCategoryTitle" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Create a content flag for this post that other users in the network can see."
}
}
}
},
"flagContentHarassmentDescription" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
109 changes: 109 additions & 0 deletions Nos/Views/Components/FlagOptionPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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)

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

private var flagOptionsListView: some View {
VStack(alignment: .leading, spacing: 0) {
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
struct FlagPickerRow: View {
var flag: FlagOption
@Binding var selection: FlagOption?

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

var body: some View {
Button(action: {
selection = flag
}, label: {
buttonLabel
})
.padding(14)
}

private var buttonLabel: some View {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 8) {
Text(flag.title)
.foregroundColor(.primaryTxt)
.font(.clarity(.regular))

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

NosRadioButton(isSelected: isSelected)
}
}
}

private struct HeaderView: View {
var text: String
var body: some View {
Text(text)
.lineSpacing(5)
.foregroundColor(.primaryTxt)
.font(.clarity(.bold))
.padding(.bottom, 28)
}
}

#Preview {
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)
}
}
return 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.
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)
}
25 changes: 25 additions & 0 deletions Nos/Views/Moderation/ContentFlagView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 body: some View {
ScrollView {
VStack {
FlagOptionPicker(
selectedOption: $selectedFlagOptionCategory,
options: FlagOption.flagContentCategories,
title: String(localized: .localizable.flagContentCategoryTitle),
subtitle: String(localized: .localizable.flagContentCategoryDescription)
)
}
}
.background(Color.appBg)
}
}

#Preview {
ContentFlagView(selectedFlagOptionCategory: .constant(nil))
}
37 changes: 33 additions & 4 deletions Nos/Views/Modifiers/ReportMenuModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,40 @@ 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

// swiftlint:disable function_body_length
@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) {
ContentFlagView(
selectedFlagOptionCategory: $selectedFlagOption
)
}
case .author:
oldModerationFlow(content: content)
}
}

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

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

Expand Down

0 comments on commit 343221e

Please sign in to comment.