commit 8ca377bec9b4f7e478c9031ab65325aa36af59ca
parent 952d6746d55eaf573fe2aa44e31aba24141323b2
Author: Terry Yiu <963907+tyiu@users.noreply.github.com>
Date: Sun, 4 Jun 2023 17:49:37 -0400
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
Diffstat:
15 files changed, 71 insertions(+), 23 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -19,6 +19,7 @@
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
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 */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.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>"; };
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>"; };
+ 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>"; };
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>"; };
@@ -1140,6 +1142,7 @@
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
50B5685229F97CB400A23243 /* CredentialHandler.swift */,
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
+ 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1670,6 +1673,7 @@
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
+ 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
diff --git a/damus/Components/TruncatedText.swift b/damus/Components/TruncatedText.swift
@@ -12,7 +12,7 @@ struct TruncatedText: View {
let maxChars: Int = 280
var body: some View {
- let truncatedAttributedString: AttributedString? = getTruncatedString()
+ let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
if let truncatedAttributedString {
Text(truncatedAttributedString)
@@ -28,16 +28,6 @@ struct TruncatedText: View {
.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 {
diff --git a/damus/Components/WebsiteLink.swift b/damus/Components/WebsiteLink.swift
@@ -23,6 +23,8 @@ struct WebsiteLink: View {
Text(link_text)
.font(.footnote)
.foregroundColor(.accentColor)
+ .truncationMode(.tail)
+ .lineLimit(1)
})
}
}
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -509,7 +509,7 @@ struct ContentView: View {
}, message: {
if let pubkey = self.muting {
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.")
} else {
Text("User has been muted", comment: "Alert message that informs a user was d.")
@@ -569,7 +569,7 @@ struct ContentView: View {
}, message: {
if let pubkey = muting {
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.")
} else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
@@ -1112,7 +1112,7 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
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 {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
diff --git a/damus/Util/StringUtil.swift b/damus/Util/StringUtil.swift
@@ -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) + "..."
+ }
+}
diff --git a/damus/Views/Events/ReplyDescription.swift b/damus/Views/Events/ReplyDescription.swift
@@ -39,7 +39,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.c
let names: [String] = pubkeys.map {
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]
diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift
@@ -231,7 +231,7 @@ func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText {
case .pubkey:
let pk = m.ref.ref_id
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)")
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple
diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift
@@ -61,7 +61,7 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
func event_author_name(profiles: Profiles, pubkey: String) -> String {
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 {
diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift
@@ -92,7 +92,7 @@ struct NotificationsView: View {
var mystery: some View {
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.")
}
.id("what")
diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift
@@ -58,7 +58,7 @@ struct UserSearch: View {
}
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 tagAttributedString = NSMutableAttributedString(string: tagString,
diff --git a/damus/Views/Profile/ProfileName.swift b/damus/Views/Profile/ProfileName.swift
@@ -65,7 +65,7 @@ struct ProfileName: View {
}
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 {
diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift
@@ -93,6 +93,7 @@ struct ProfileView: View {
let damus_state: DamusState
let pfp_size: CGFloat = 90.0
let bannerHeight: CGFloat = 150.0
+ let max_about_length = 280
static let markdown = Markdown()
@@ -103,6 +104,7 @@ struct ProfileView: View {
@State var action_sheet_presented: Bool = false
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
+ @State var show_full_about: Bool = false
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
@@ -403,7 +405,23 @@ struct ProfileView: View {
if let about = profile_data?.about {
let blocks = parse_mentions(content: about, tags: [])
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 {
Text(verbatim: "")
.font(.subheadline)
diff --git a/damus/Views/ReplyView.swift b/damus/Views/ReplyView.swift
@@ -22,7 +22,7 @@ struct ReplyView: View {
.map { pubkey in
let pk = pubkey.ref_id
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: " ")
if names.isEmpty {
diff --git a/damus/Views/Zaps/ZapTypePicker.swift b/damus/Views/Zaps/ZapTypePicker.swift
@@ -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.")
case .priv:
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)
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.")