1
0
mirror of git://jb55.com/damus synced 2024-09-30 00:40:45 +00:00

Add max length truncation to displayed profile attributes to mitigate spam

Changelog-Fixed: Add max length truncation to displayed profile attributes to mitigate spam
Fixes: #1237
This commit is contained in:
Terry Yiu 2023-06-04 17:49:37 -04:00
parent 952d6746d5
commit 8ca377bec9
No known key found for this signature in database
GPG Key ID: 108645AE8A19B71A
15 changed files with 71 additions and 23 deletions

View File

@ -19,6 +19,7 @@
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; }; 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; }; 3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; }; 3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; }; 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; }; 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@ -381,6 +382,7 @@
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtil.swift; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@ -1140,6 +1142,7 @@
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */, 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
50B5685229F97CB400A23243 /* CredentialHandler.swift */, 50B5685229F97CB400A23243 /* CredentialHandler.swift */,
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */, 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1670,6 +1673,7 @@
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */, 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */, 4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */, 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */, 4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */, 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,

View File

@ -12,7 +12,7 @@ struct TruncatedText: View {
let maxChars: Int = 280 let maxChars: Int = 280
var body: some View { var body: some View {
let truncatedAttributedString: AttributedString? = getTruncatedString() let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
if let truncatedAttributedString { if let truncatedAttributedString {
Text(truncatedAttributedString) Text(truncatedAttributedString)
@ -28,16 +28,6 @@ struct TruncatedText: View {
.allowsHitTesting(false) .allowsHitTesting(false)
} }
} }
func getTruncatedString() -> AttributedString? {
let nsAttributedString = NSAttributedString(text.attributed)
if nsAttributedString.length < maxChars { return nil }
let range = NSRange(location: 0, length: maxChars)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
} }
struct TruncatedText_Previews: PreviewProvider { struct TruncatedText_Previews: PreviewProvider {

View File

@ -23,6 +23,8 @@ struct WebsiteLink: View {
Text(link_text) Text(link_text)
.font(.footnote) .font(.footnote)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.truncationMode(.tail)
.lineLimit(1)
}) })
} }
} }

View File

@ -509,7 +509,7 @@ struct ContentView: View {
}, message: { }, message: {
if let pubkey = self.muting { if let pubkey = self.muting {
let profile = damus_state!.profiles.lookup(id: pubkey) let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.") Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
} else { } else {
Text("User has been muted", comment: "Alert message that informs a user was d.") Text("User has been muted", comment: "Alert message that informs a user was d.")
@ -569,7 +569,7 @@ struct ContentView: View {
}, message: { }, message: {
if let pubkey = muting { if let pubkey = muting {
let profile = damus_state?.profiles.lookup(id: pubkey) let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.") Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
} else { } else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.") Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")

View File

@ -1112,7 +1112,7 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
let profile = profiles.lookup(id: pk) let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount) let formattedSats = format_msats_abbrev(zap.invoice.amount)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name let name = Profile.displayName(profile: profile, pubkey: pk).display_name.truncate(maxLength: 50)
if src.content.isEmpty { if src.content.isEmpty {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale) let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)

View File

@ -0,0 +1,34 @@
//
// StringUtil.swift
// damus
//
// Created by Terry Yiu on 6/4/23.
//
import Foundation
extension String {
/// Returns a copy of the String truncated to maxLength and "..." ellipsis appended to the end,
/// or if the String does not exceed maxLength, the String itself is returned without truncation or added ellipsis.
func truncate(maxLength: Int) -> String {
guard count > maxLength else {
return self
}
return self[...self.index(self.startIndex, offsetBy: maxLength - 1)] + "..."
}
}
extension AttributedString {
/// Returns a copy of the AttributedString truncated to maxLength and "..." ellipsis appended to the end,
/// or if the AttributedString does not exceed maxLength, nil is returned.
func truncateOrNil(maxLength: Int) -> AttributedString? {
let nsAttributedString = NSAttributedString(self)
if nsAttributedString.length < maxLength { return nil }
let range = NSRange(location: 0, length: maxLength)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}

View File

@ -39,7 +39,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.c
let names: [String] = pubkeys.map { let names: [String] = pubkeys.map {
let prof = profiles.lookup(id: $0) let prof = profiles.lookup(id: $0)
return Profile.displayName(profile: prof, pubkey: $0).username return Profile.displayName(profile: prof, pubkey: $0).username.truncate(maxLength: 50)
} }
let uniqueNames = NSOrderedSet(array: names).array as! [String] let uniqueNames = NSOrderedSet(array: names).array as! [String]

View File

@ -231,7 +231,7 @@ func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText {
case .pubkey: case .pubkey:
let pk = m.ref.ref_id let pk = m.ref.ref_id
let profile = profiles.lookup(id: pk) let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk).username let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
var attributedString = AttributedString(stringLiteral: "@\(disp)") var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))") attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = DamusColors.purple

View File

@ -61,7 +61,7 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
func event_author_name(profiles: Profiles, pubkey: String) -> String { func event_author_name(profiles: Profiles, pubkey: String) -> String {
let alice_prof = profiles.lookup(id: pubkey) let alice_prof = profiles.lookup(id: pubkey)
return Profile.displayName(profile: alice_prof, pubkey: pubkey).username return Profile.displayName(profile: alice_prof, pubkey: pubkey).username.truncate(maxLength: 50)
} }
func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String { func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {

View File

@ -92,7 +92,7 @@ struct NotificationsView: View {
var mystery: some View { var mystery: some View {
VStack(spacing: 20) { VStack(spacing: 20) {
Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).display_name)", comment: "Text telling the user to wake up, where the argument is their display name.") Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).display_name.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.")
Text("You are dreaming...", comment: "Text telling the user that they are dreaming.") Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")
} }
.id("what") .id("what")

View File

@ -58,7 +58,7 @@ struct UserSearch: View {
} }
private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString { private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
let name = Profile.displayName(profile: user.profile, pubkey: pk).username let name = Profile.displayName(profile: user.profile, pubkey: pk).username.truncate(maxLength: 50)
let tagString = "@\(name)\u{200B} " let tagString = "@\(name)\u{200B} "
let tagAttributedString = NSMutableAttributedString(string: tagString, let tagAttributedString = NSMutableAttributedString(string: tagString,

View File

@ -65,7 +65,7 @@ struct ProfileName: View {
} }
var name_choice: String { var name_choice: String {
return prefix == "@" ? current_display_name.username : current_display_name.display_name return prefix == "@" ? current_display_name.username.truncate(maxLength: 50) : current_display_name.display_name.truncate(maxLength: 50)
} }
var onlyzapper: Bool { var onlyzapper: Bool {

View File

@ -93,6 +93,7 @@ struct ProfileView: View {
let damus_state: DamusState let damus_state: DamusState
let pfp_size: CGFloat = 90.0 let pfp_size: CGFloat = 90.0
let bannerHeight: CGFloat = 150.0 let bannerHeight: CGFloat = 150.0
let max_about_length = 280
static let markdown = Markdown() static let markdown = Markdown()
@ -103,6 +104,7 @@ struct ProfileView: View {
@State var action_sheet_presented: Bool = false @State var action_sheet_presented: Bool = false
@State var filter_state : FilterState = .posts @State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0 @State var yOffset: CGFloat = 0
@State var show_full_about: Bool = false
@StateObject var profile: ProfileModel @StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel @StateObject var followers: FollowersModel
@ -403,7 +405,23 @@ struct ProfileView: View {
if let about = profile_data?.about { if let about = profile_data?.about {
let blocks = parse_mentions(content: about, tags: []) let blocks = parse_mentions(content: about, tags: [])
let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed let about_string = render_blocks(blocks: blocks, profiles: damus_state.profiles).content.attributed
SelectableText(attributedString: about_string, size: .subheadline) let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
SelectableText(attributedString: truncated_about ?? about_string, size: .subheadline)
if truncated_about != nil {
if show_full_about {
Button(NSLocalizedString("Show less", comment: "Button to show less of a long profile description.")) {
show_full_about = false
}
.font(.footnote)
} else {
Button(NSLocalizedString("Show more", comment: "Button to show more of a long profile description.")) {
show_full_about = true
}
.font(.footnote)
}
}
} else { } else {
Text(verbatim: "") Text(verbatim: "")
.font(.subheadline) .font(.subheadline)

View File

@ -22,7 +22,7 @@ struct ReplyView: View {
.map { pubkey in .map { pubkey in
let pk = pubkey.ref_id let pk = pubkey.ref_id
let prof = damus.profiles.lookup(id: pk) let prof = damus.profiles.lookup(id: pk)
return "@" + Profile.displayName(profile: prof, pubkey: pk).username return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50)
} }
.joined(separator: " ") .joined(separator: " ")
if names.isEmpty { if names.isEmpty {

View File

@ -117,7 +117,7 @@ func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: String) -> String
return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.") return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.")
case .priv: case .priv:
let prof = profiles.lookup(id: pubkey) let prof = profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: prof, pubkey: pubkey).username let name = Profile.displayName(profile: prof, pubkey: pubkey).username.truncate(maxLength: 50)
return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' will see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name) return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' will see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name)
case .non_zap: case .non_zap:
return NSLocalizedString("No zaps will be sent, only a lightning payment.", comment: "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap.") return NSLocalizedString("No zaps will be sent, only a lightning payment.", comment: "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap.")