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

Slur Filter Warning #867

Merged
merged 14 commits into from
Jan 28, 2024
2 changes: 1 addition & 1 deletion Mlem/API/Models/Site/APILocalSite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct APILocalSite: Decodable {
// let legalInformation: String?
// let hideModlogModNames: Bool
// let applicationEmailAdmins: Bool
// let slurFilterRegex: String?
let slurFilterRegex: String?
// let actorNameMaxLength: Int
// let federationEnabled: Bool
// let federationDebug: Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ protocol ResponseEditorModel: Identifiable {

var modalName: String { get } // what to title the modal

var showSlurWarning: Bool { get }

var prefillContents: String? { get } // optional, contents to prepopulate the editor with

func embeddedView() -> AnyView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct CommentEditor: ResponseEditorModel {
@Dependency(\.commentRepository) var commentRepository

let canUpload: Bool = true
let showSlurWarning: Bool = true
let modalName: String = "Edit Comment"
var prefillContents: String? { comment.comment.content }
let comment: APICommentView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct ReplyToComment: ResponseEditorModel {

var id: Int { comment.id }
let canUpload: Bool = true
let showSlurWarning: Bool = true
let modalName: String = "New Comment"
let comment: APICommentView
let prefillContents: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct ReplyToCommentReply: ResponseEditorModel {
@Dependency(\.commentRepository) var commentRepository

let canUpload: Bool = true
let showSlurWarning: Bool = true
let modalName: String = "New Comment"
let prefillContents: String? = nil
let commentReply: ReplyModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct ReplyToMention: ResponseEditorModel {
@Dependency(\.commentRepository) var commentRepository

let canUpload: Bool = true
let showSlurWarning: Bool = true
let modalName: String = "New Comment"
let prefillContents: String? = nil
let mention: MentionModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct ReplyToMessage: ResponseEditorModel {

var id: Int { message.id }
let canUpload: Bool = true
let showSlurWarning: Bool = true
let modalName: String = "New Message"
let prefillContents: String? = nil
let message: MessageModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct ReplyToPost: ResponseEditorModel {
@Dependency(\.commentRepository) var commentRepository

let canUpload: Bool = true
let showSlurWarning: Bool = true
let modalName: String = "New Comment"
let prefillContents: String?
let commentTracker: CommentTracker?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct ReportComment: ResponseEditorModel {

var id: Int { comment.id }
let canUpload: Bool = false
let showSlurWarning: Bool = false
let modalName: String = "Report Comment"
let prefillContents: String? = nil
let comment: APICommentView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct ReportCommentReply: ResponseEditorModel {

var id: Int { commentReply.id }
let canUpload: Bool = false
let showSlurWarning: Bool = false
let modalName: String = "Report Comment"
let prefillContents: String? = nil
let commentReply: ReplyModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct ReportMention: ResponseEditorModel {

var id: Int { mention.id }
let canUpload: Bool = false
let showSlurWarning: Bool = false
let modalName: String = "Report Comment"
let prefillContents: String? = nil
let mention: MentionModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct ReportMessage: ResponseEditorModel {

var id: Int { message.id }
let canUpload: Bool = false
let showSlurWarning: Bool = false
let modalName: String = "Report Message"
let prefillContents: String? = nil
let message: MessageModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct ReportPost: ResponseEditorModel {

var id: Int { post.postId }
let canUpload: Bool = false
let showSlurWarning: Bool = false
let modalName: String = "Report Post"
let prefillContents: String? = nil
let post: PostModel
Expand Down
26 changes: 26 additions & 0 deletions Mlem/Models/Content/Instance/InstanceModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ struct InstanceModel {
var url: URL!
var version: SiteVersion?

var slurFilterRegex: Regex<AnyRegexOutput>?

init(from response: SiteResponse) {
self.update(with: response)
}
Expand All @@ -33,6 +35,17 @@ struct InstanceModel {
return user
}
self.version = SiteVersion(response.version)

let localSite = response.siteView.localSite

do {
if let regex = localSite.slurFilterRegex {
self.slurFilterRegex = try .init(regex)
}
} catch {
print("Invalid slur filter regex")
}

self.update(with: response.siteView.site)
}

Expand All @@ -48,6 +61,19 @@ struct InstanceModel {
url = components.url
}
}

func firstSlurFilterMatch(_ input: String) -> String? {
do {
if let slurFilterRegex {
if let output = try slurFilterRegex.firstMatch(in: input.lowercased()) {
return String(input[output.range])
}
}
} catch {
print("REGEX FAILED")
}
return nil
}
}

extension InstanceModel: Identifiable {
Expand Down
2 changes: 2 additions & 0 deletions Mlem/Models/Trackers/SiteInformationTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class SiteInformationTracker: ObservableObject {
@Published var myUserInfo: APIMyUserInfo?

func load(account: SavedAccount) {

version = account.siteVersion
Task {
do {
Expand All @@ -36,6 +37,7 @@ class SiteInformationTracker: ObservableObject {
}
myUserInfo = response.myUser
allLanguages = response.allLanguages

} catch {
errorHandler.handle(error)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ class LinkAttachmentModel: ObservableObject {
if let task = self.uploadTask {
task.cancel()
}
self.photosPickerItem = nil
switch self.imageModel?.state {
case .uploaded(file: let file):
if let file = file {
self.photosPickerItem = nil
Task {
do {
if let compareUrl {
Expand Down
2 changes: 1 addition & 1 deletion Mlem/Views/Shared/Composer/PostComposerView+Logic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension PostComposerView {
var isReadyToPost: Bool {
switch attachmentModel.imageModel?.state {
case nil, .uploaded:
return postTitle.trimmed.isNotEmpty
return postTitle.trimmed.isNotEmpty && titleSlurMatch == nil && bodySlurMatch == nil
default:
return false
}
Expand Down
64 changes: 46 additions & 18 deletions Mlem/Views/Shared/Composer/PostComposerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ struct PostComposerView: View {

@FocusState private var focusedField: Field?

@State var titleSlurMatch: String?
@State var bodySlurMatch: String?

init(editModel: PostEditorModel) {
self.editModel = editModel

Expand All @@ -73,22 +76,7 @@ struct PostComposerView: View {
.edgesIgnoringSafeArea(.bottom)
VStack(spacing: 0) {
// Community Row
HStack {
CommunityLabelView(
community: editModel.community,
serverInstanceLocation: .bottom,
overrideShowAvatar: true
)
Spacer()
if let person = siteInformation.myUserInfo?.localUserView.person {
UserLabelView(
person: person,
serverInstanceLocation: .bottom,
overrideShowAvatar: true
)
.environment(\.layoutDirection, layoutDirection == .leftToRight ? .rightToLeft : .leftToRight)
}
}
headerView
.padding(.bottom, 15)
.padding(.horizontal)
.zIndex(1)
Expand All @@ -103,17 +91,22 @@ struct PostComposerView: View {
}
.padding(.top)
.padding(.horizontal)
.onChange(of: postTitle) { newValue in
titleSlurMatch = siteInformation.instance?.firstSlurFilterMatch(newValue)
}

if attachmentModel.imageModel != nil || attachmentModel.url.isNotEmpty {
VStack {
let url = URL(string: attachmentModel.url)
if !(url?.isImage ?? true) {
HStack(spacing: AppConstants.postAndCommentSpacing) {
Image(systemName: "link")
Image(systemName: Icons.websiteAddress)
.foregroundStyle(.blue)
.padding(.leading, 5)
Text(attachmentModel.url)
.foregroundStyle(.secondary)
.lineLimit(1)
Spacer()
Button(action: attachmentModel.removeLinkAction, label: {
Image(systemName: Icons.close)
.fontWeight(.semibold)
Expand Down Expand Up @@ -186,6 +179,9 @@ struct PostComposerView: View {
.accessibilityLabel("Post Body")
.focused($focusedField, equals: .body)
.padding(.horizontal)
.onChange(of: postBody) { newValue in
bodySlurMatch = siteInformation.instance?.firstSlurFilterMatch(newValue)
}

Spacer()
}
Expand Down Expand Up @@ -236,7 +232,7 @@ struct PostComposerView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
LinkUploadOptionsView(model: attachmentModel) {
Label("Attach image or link", systemImage: "link")
Label("Attach Image or Link", systemImage: Icons.websiteAddress)
}
.disabled(attachmentModel.imageModel != nil || attachmentModel.url.isNotEmpty)
}
Expand All @@ -261,4 +257,36 @@ struct PostComposerView: View {
.navigationBarColor()
.navigationBarTitleDisplayMode(.inline)
}

@ViewBuilder
var headerView: some View {
HStack {
CommunityLabelView(
community: editModel.community,
serverInstanceLocation: .bottom,
overrideShowAvatar: true
)
Spacer()
if let person = siteInformation.myUserInfo?.localUserView.person {
UserLabelView(
person: person,
serverInstanceLocation: .bottom,
overrideShowAvatar: true
)
.environment(\.layoutDirection, layoutDirection == .leftToRight ? .rightToLeft : .leftToRight)
}
}
.overlay {
if let slurMatch = titleSlurMatch == nil ? bodySlurMatch : titleSlurMatch {
ZStack {
Capsule()
.fill(.red)
Text("\"\(slurMatch)\" is disallowed.")
EricBAndrews marked this conversation as resolved.
Show resolved Hide resolved
.foregroundStyle(.white)
}
.padding(-2)
}
}
.animation(.default, value: titleSlurMatch == nil && bodySlurMatch == nil)
}
}
36 changes: 26 additions & 10 deletions Mlem/Views/Shared/Composer/ResponseEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct ResponseEditorView: View {
}

@Dependency(\.errorHandler) var errorHandler
@Dependency(\.siteInformation) var siteInformation

let editorModel: any ResponseEditorModel

Expand All @@ -28,20 +29,14 @@ struct ResponseEditorView: View {
@State var editorBody: String
@State var isSubmitting: Bool = false

@State var slurMatch: String?

@FocusState private var focusedField: Field?

private var isReadyToReply: Bool {
editorBody.trimmed.isNotEmpty
}

func uploadImage() {
if editorModel.canUpload {
print("Uploading")
} else {
print("Uploading disabled for this sort of response")
}
}


var body: some View {
ScrollView {
VStack(spacing: AppConstants.postAndCommentSpacing) {
Expand All @@ -58,11 +53,32 @@ struct ResponseEditorView: View {
.onAppear {
focusedField = .editorBody
}
.onChange(of: editorBody) { newValue in
if editorModel.showSlurWarning {
slurMatch = siteInformation.instance?.firstSlurFilterMatch(newValue)
}
}

Divider()

editorModel.embeddedView()
if let slurMatch {
VStack {
Text("\"\(slurMatch)\" is disallowed.")
.foregroundStyle(.white)
Text("You can still post this comment, but your instance will replace \"\(slurMatch)\" with \"*removed*\".")
.multilineTextAlignment(.center)
.font(.footnote)
.foregroundStyle(.white.opacity(0.8))
}
.padding()
.frame(maxWidth: .infinity)
.background(RoundedRectangle(cornerRadius: AppConstants.largeItemCornerRadius).fill(.red))
.padding(.horizontal, 10)
} else {
editorModel.embeddedView()
}
}
.animation(.default, value: slurMatch)
.padding(.bottom, AppConstants.editorOverscroll)
}
.scrollDismissesKeyboard(.automatic)
Expand Down
Loading