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 all 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
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 @@ -4350,6 +4350,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)
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, 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) {
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)
.multilineTextAlignment(.leading)
.font(.clarity(.regular, textStyle: .footnote))
.lineSpacing(8)
}
}
Spacer()

NosRadioButton(isSelected: isSelected)
}
}
}

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)
}
}

#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.
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)
}
25 changes: 25 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,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
Loading