damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Components/TruncatedText.swift | 12+-----------
Mdamus/Components/WebsiteLink.swift | 2++
Mdamus/ContentView.swift | 4++--
Mdamus/Models/HomeModel.swift | 2+-
Adamus/Util/StringUtil.swift | 34++++++++++++++++++++++++++++++++++
Mdamus/Views/Events/ReplyDescription.swift | 2+-
Mdamus/Views/NoteContentView.swift | 2+-
Mdamus/Views/Notifications/EventGroupView.swift | 2+-
Mdamus/Views/Notifications/NotificationsView.swift | 2+-
Mdamus/Views/Posting/UserSearch.swift | 2+-
Mdamus/Views/Profile/ProfileName.swift | 2+-
Mdamus/Views/Profile/ProfileView.swift | 20+++++++++++++++++++-
Mdamus/Views/ReplyView.swift | 2+-
Mdamus/Views/Zaps/ZapTypePicker.swift | 2+-
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.")