Skip to content

Commit

Permalink
Merge pull request #1544 from planetary-social/account-flagging
Browse files Browse the repository at this point in the history
User flagging
  • Loading branch information
pelumy authored Oct 4, 2024
2 parents b742d8e + 4dfa6f7 commit 2b0c57b
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added SwiftSoup to parse Open Graph metadata. [#1165](https://github.com/planetary-social/nos/issues/1165)
- Parse Open Graph metadata whenever an event contains a URL, doesn’t have `imeta` tags, and the URL points to an HTML document. [#1425](https://github.com/planetary-social/nos/issues/1425)
- Added a new flow to flag notes. Currently behind the “Enable new moderation flow” feature flag. [#1489](https://github.com/planetary-social/nos/issues/1489)
- Added a new flow to flag users. Currently behind the “Enable new moderation flow” feature flag. [#1493](https://github.com/planetary-social/nos/issues/1493)

## [0.1.26] - 2024-09-09Z

Expand Down
8 changes: 8 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@
03FE3F7C2C87AC9900D25810 /* Event+InlineMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FE3F7B2C87AC9900D25810 /* Event+InlineMetadata.swift */; };
03FE3F7D2C87AC9900D25810 /* Event+InlineMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FE3F7B2C87AC9900D25810 /* Event+InlineMetadata.swift */; };
03FE3F8C2C87BC9500D25810 /* text_note_multiple_media.json in Resources */ = {isa = PBXBuildFile; fileRef = 03FE3F8A2C87BC9500D25810 /* text_note_multiple_media.json */; };
041C56C42CA1B48E007D3BB2 /* UserFlagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041C56C32CA1B48E007D3BB2 /* UserFlagView.swift */; };
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 */; };
045EDCF32CAAF47600B67964 /* FlagSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045EDCF22CAAF47600B67964 /* FlagSuccessView.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 */; };
Expand Down Expand Up @@ -668,10 +670,12 @@
03FE3F782C87A9D900D25810 /* EventError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventError.swift; sourceTree = "<group>"; };
03FE3F7B2C87AC9900D25810 /* Event+InlineMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+InlineMetadata.swift"; sourceTree = "<group>"; };
03FE3F8A2C87BC9500D25810 /* text_note_multiple_media.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = text_note_multiple_media.json; sourceTree = "<group>"; };
041C56C32CA1B48E007D3BB2 /* UserFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFlagView.swift; 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>"; };
045EDCF22CAAF47600B67964 /* FlagSuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagSuccessView.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>"; };
Expand Down Expand Up @@ -1458,6 +1462,8 @@
isa = PBXGroup;
children = (
04368D4A2C99CFC700DEAA2E /* ContentFlagView.swift */,
041C56C32CA1B48E007D3BB2 /* UserFlagView.swift */,
045EDCF22CAAF47600B67964 /* FlagSuccessView.swift */,
);
path = Moderation;
sourceTree = "<group>";
Expand Down Expand Up @@ -2325,6 +2331,7 @@
C9C2B78229E0735400548B4A /* RelaySubscriptionManager.swift in Sources */,
3FFB1D9629A6BBEC002A755D /* Collection+SafeSubscript.swift in Sources */,
A34E439929A522F20057AFCB /* CurrentUser.swift in Sources */,
045EDCF32CAAF47600B67964 /* FlagSuccessView.swift in Sources */,
03E1812F2C753C9B00886CC6 /* ImageButton.swift in Sources */,
C9A0DADD29C689C900466635 /* NosNavigationBar.swift in Sources */,
3F30020529C1FDD9003D4F8B /* OnboardingStartView.swift in Sources */,
Expand Down Expand Up @@ -2397,6 +2404,7 @@
C9F75AD22A02D41E005BBE45 /* ComposerActionBar.swift in Sources */,
2D06BB9D2AE249D70085F509 /* ThreadRootView.swift in Sources */,
C9DEBFD2298941000078B43A /* NosApp.swift in Sources */,
041C56C42CA1B48E007D3BB2 /* UserFlagView.swift in Sources */,
5BFBB28B2BD9D79F002E909F /* URLParser.swift in Sources */,
C930055F2A6AF8320098CA9E /* LoadingContent.swift in Sources */,
5B79F6462BA11725002DA9BE /* WizardSheetVStack.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions Nos/Models/FlagOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ struct FlagOption: Identifiable, Equatable {
title: String(localized: .localizable.flagUserDontMuteTitle),
description: String(localized: .localizable.flagUserDontMuteDescription),
info: nil,
category: .visibility(.unmute)
category: .visibility(.dontMute)
)
]

Expand Down Expand Up @@ -153,5 +153,5 @@ enum SendFlagPrivacy {
/// Specifies whether a flagged user should be muted or not.
enum FlagUserVisibility {
case mute
case unmute
case dontMute
}
1 change: 1 addition & 0 deletions Nos/Views/Components/FlagOptionPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ struct FlagOptionPicker: View {
}
.background(LinearGradient.cardBackground)
.clipShape(RoundedRectangle(cornerRadius: 15))
.mimicCardButtonStyle()
}
}

Expand Down
33 changes: 7 additions & 26 deletions Nos/Views/Moderation/ContentFlagView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ struct ContentFlagView: View {

/// The target of the report.
let flagTarget: ReportTarget

/// Defines the action to be performed when the user sends a flag report.
/// It is called when the user taps the "Send" button after selecting all required options.
var sendAction: () -> Void

@Environment(\.dismiss) private var dismiss
Expand All @@ -20,7 +23,7 @@ struct ContentFlagView: View {
Color.appBg.ignoresSafeArea()
Group {
if showSuccessView {
successView
FlagSuccessView()
} else {
categoryView
}
Expand All @@ -30,13 +33,13 @@ struct ContentFlagView: View {
.nosNavigationBar(title: .localizable.flagContent)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
Button {
dismiss()
resetSelections()
}, label: {
} label: {
Text(.localizable.cancel)
.foregroundColor(.primaryTxt)
})
}
.opacity(showSuccessView ? 0 : 1)
.disabled(showSuccessView)
}
Expand All @@ -53,7 +56,6 @@ struct ContentFlagView: View {
}
}
)
.opacity(selectedSendOptionCategory == nil ? 0.5 : 1)
.disabled(selectedSendOptionCategory == nil)
}
}
Expand Down Expand Up @@ -92,27 +94,6 @@ struct ContentFlagView: View {
}
}
}

private var successView: some View {
VStack(spacing: 30) {
Image.circularCheckmark
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 116)

Text(String(localized: .localizable.thanksForTag))
.foregroundColor(.primaryTxt)
.font(.clarity(.regular, textStyle: .title2))
.padding(.horizontal, 62)

Text(String(localized: .localizable.keepOnHelpingUs))
.padding(.horizontal, 68)
.foregroundColor(.secondaryTxt)
.multilineTextAlignment(.center)
.lineSpacing(6)
.font(.clarity(.regular, textStyle: .subheadline))
}
}
}

#Preview {
Expand Down
29 changes: 29 additions & 0 deletions Nos/Views/Moderation/FlagSuccessView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import SwiftUI

/// Shows a success message after the user has successfully flagged a user or content
struct FlagSuccessView: View {
var body: some View {
VStack(spacing: 30) {
Image.circularCheckmark
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 116)

Text(String(localized: .localizable.thanksForTag))
.foregroundColor(.primaryTxt)
.font(.clarity(.regular, textStyle: .title2))
.padding(.horizontal, 25)

Text(String(localized: .localizable.keepOnHelpingUs))
.padding(.horizontal, 25)
.foregroundColor(.secondaryTxt)
.multilineTextAlignment(.center)
.lineSpacing(6)
.font(.clarity(.regular, textStyle: .subheadline))
}
}
}

#Preview {
FlagSuccessView()
}
156 changes: 156 additions & 0 deletions Nos/Views/Moderation/UserFlagView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import SwiftUI

/// Displays pickers for selecting user flag options, with additional stages shown
/// based on previous selections.
struct UserFlagView: View {
@Binding var selectedFlagOption: FlagOption?
@Binding var selectedSendOption: FlagOption?
@Binding var selectedVisibilityOption: FlagOption?
@Binding var showSuccessView: Bool

@Environment(\.dismiss) private var dismiss

@State private var flagCategories: [FlagOption] = []

/// The target of the report.
let flagTarget: ReportTarget

/// Defines the action to be performed when the user sends a flag report.
/// It is called when the user taps the "Send" button after selecting all required options.
var sendAction: () -> Void

/// Indicates whether the target of the report is muted.
var isUserMuted: Bool {
flagTarget.author?.muted ?? false
}

/// Determines if the send button should be disabled.
///
/// The button's disabled state depends on two factors:
/// - For muted users: disabled when no send option is selected.
/// - For non-muted users: disabled when no visibility option is selected.
var isSendButtonDisabled: Bool {
if isUserMuted {
return selectedSendOption == nil
}
return selectedVisibilityOption == nil
}

var body: some View {
ZStack {
Color.appBg.ignoresSafeArea()
Group {
if showSuccessView {
FlagSuccessView()
} else {
categoryView
}
}
.padding()
.nosNavigationBar(title: .localizable.flagUserTitle)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
resetSelections()
} label: {
Text(.localizable.cancel)
.foregroundColor(.primaryTxt)
}
.opacity(showSuccessView ? 0 : 1)
.disabled(showSuccessView)
}
ToolbarItem(placement: .navigationBarTrailing) {
ActionButton(
title: showSuccessView ? .localizable.done : .localizable.send,
action: {
if showSuccessView {
dismiss()
resetSelections()
showSuccessView = false
} else {
sendAction()
}
}
)
.disabled(isSendButtonDisabled)
}
}
}
.onAppear {
flagCategories = FlagOption.createFlagCategories(for: flagTarget)
}
}

private func resetSelections() {
selectedFlagOption = nil
selectedSendOption = nil
selectedVisibilityOption = nil
}

private var categoryView: some View {
ScrollView {
VStack(spacing: 30) {
FlagOptionPicker(
previousSelection: .constant(nil),
currentSelection: $selectedFlagOption,
options: flagCategories,
title: String(localized: .localizable.flagUserCategoryTitle),
subtitle: String(localized: .localizable.flagUserCategoryDescription)
)

if selectedFlagOption != nil {
FlagOptionPicker(
previousSelection: $selectedFlagOption,
currentSelection: $selectedSendOption,
options: FlagOption.flagUserSendOptions,
title: String(localized: .localizable.flagSendTitle),
subtitle: nil
)
.transition(.move(edge: .leading).combined(with: .opacity))
}

if selectedSendOption != nil && !isUserMuted {
FlagOptionPicker(
previousSelection: .constant(nil),
currentSelection: $selectedVisibilityOption,
options: FlagOption.flagUserVisibilityOptions,
title: String(localized: .localizable.flagUserMuteCategoryTitle),
subtitle: nil
)
.transition(.move(edge: .leading).combined(with: .opacity))
}
}
}
.animation(.easeInOut, value: selectedFlagOption)
.animation(.easeInOut, value: selectedSendOption)
}
}

#Preview {
struct PreviewWrapper: View {
@State private var selectedFlagOption: FlagOption?
@State private var selectedSendOption: FlagOption?
@State private var selectedVisibilityOption: FlagOption?
@State private var showSuccessView = false
let author = Author()

var body: some View {
NavigationStack {
UserFlagView(
selectedFlagOption: $selectedFlagOption,
selectedSendOption: $selectedSendOption,
selectedVisibilityOption: $selectedVisibilityOption,
showSuccessView: $showSuccessView,
flagTarget: .author(author),
sendAction: {}
)
}
.onAppear {
selectedFlagOption = nil
}
.background(Color.appBg)
}
}
return PreviewWrapper()
}
Loading

0 comments on commit 2b0c57b

Please sign in to comment.