damus

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

commit c146bab08aff0ce50dac0eecc12d607434f0b60c
parent d1cced8d5424ae893c6a75a324ce6a0720a01c6c
Author: Terry Yiu <git@tyiu.xyz>
Date:   Sat, 29 Mar 2025 10:21:19 -0400

Add notification setting to hide hellthreads

Changelog-Added: Add notification setting to hide hellthreads
Closes: https://github.com/damus-io/damus/issues/2943
Signed-off-by: Terry Yiu <git@tyiu.xyz>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++----
Mdamus/Models/NotificationsManager.swift | 4++++
Mdamus/Models/PushNotificationClient.swift | 18++++++++++--------
Mdamus/Models/UserSettingsStore.swift | 5++++-
Mdamus/Views/Notifications/NotificationsView.swift | 38+++++++++++++++++++++++---------------
Mdamus/Views/Settings/NotificationSettingsView.swift | 6+++++-
Mdamus/en-US.lproj/Localizable.stringsdict | 48++++++++++++++++++++++++++++++++----------------
AdamusTests/LargeEventTests.swift | 50++++++++++++++++++++++++++++++++++++++++++++++++++
MdamusTests/LocalizationUtilTests.swift | 7+++++--
DdamusTests/LongPostTests.swift | 48------------------------------------------------
Mnostrdb/NdbNote.swift | 10++++++++++
11 files changed, 147 insertions(+), 95 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -207,7 +207,7 @@ 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; }; 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; }; 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; }; - 4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LongPostTests.swift */; }; + 4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LargeEventTests.swift */; }; 4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A562A7FFAE6005E6031 /* UrlTests.swift */; }; 4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; }; 4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; }; @@ -2192,7 +2192,7 @@ 4C64305B2A945AFF00B0C0E9 /* MusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicController.swift; sourceTree = "<group>"; }; 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; }; 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; }; - 4C684A542A7E91FE005E6031 /* LongPostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPostTests.swift; sourceTree = "<group>"; }; + 4C684A542A7E91FE005E6031 /* LargeEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeEventTests.swift; sourceTree = "<group>"; }; 4C684A562A7FFAE6005E6031 /* UrlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlTests.swift; sourceTree = "<group>"; }; 4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = "<group>"; }; 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = "<group>"; }; @@ -3743,7 +3743,7 @@ 4C19AE542A5D977400C90DB7 /* HashtagTests.swift */, 3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */, D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */, - 4C684A542A7E91FE005E6031 /* LongPostTests.swift */, + 4C684A542A7E91FE005E6031 /* LargeEventTests.swift */, 4C684A562A7FFAE6005E6031 /* UrlTests.swift */, D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */, D71DC1EB2A9129C3006E207C /* PostViewTests.swift */, @@ -5025,7 +5025,7 @@ D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */, 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */, 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */, - 4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */, + 4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */, E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/damus/Models/NotificationsManager.swift b/damus/Models/NotificationsManager.swift @@ -41,6 +41,10 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent return false } + if !state.settings.hellthread_notification && ev.is_hellthread { + return false + } + // Don't show notifications that match mute list. if state.mutelist_manager.is_event_muted(ev) { return false diff --git a/damus/Models/PushNotificationClient.swift b/damus/Models/PushNotificationClient.swift @@ -175,13 +175,14 @@ extension PushNotificationClient { } struct NotificationSettings: Codable, Equatable { - let zap_notifications_enabled: Bool - let mention_notifications_enabled: Bool - let repost_notifications_enabled: Bool - let reaction_notifications_enabled: Bool - let dm_notifications_enabled: Bool - let only_notifications_from_following_enabled: Bool - + let zap_notifications_enabled: Bool? + let mention_notifications_enabled: Bool? + let repost_notifications_enabled: Bool? + let reaction_notifications_enabled: Bool? + let dm_notifications_enabled: Bool? + let only_notifications_from_following_enabled: Bool? + let hellthread_notifications_enabled: Bool? + static func from(json_data: Data) -> Self? { guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil } return decoded @@ -194,7 +195,8 @@ extension PushNotificationClient { repost_notifications_enabled: settings.repost_notification, reaction_notifications_enabled: settings.like_notification, dm_notifications_enabled: settings.dm_notification, - only_notifications_from_following_enabled: settings.notification_only_from_following + only_notifications_from_following_enabled: settings.notification_only_from_following, + hellthread_notifications_enabled: settings.hellthread_notification ) } diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift @@ -160,7 +160,10 @@ class UserSettingsStore: ObservableObject { @Setting(key: "notification_only_from_following", default_value: false) var notification_only_from_following: Bool - + + @Setting(key: "hellthread_notification", default_value: false) + var hellthread_notification: Bool + @Setting(key: "translate_dms", default_value: false) var translate_dms: Bool diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift @@ -9,15 +9,17 @@ import SwiftUI class NotificationFilter: ObservableObject, Equatable { @Published var state: NotificationFilterState - @Published var fine_filter: FriendFilter - + @Published var friend_filter: FriendFilter + @Published var show_hellthreads: Bool = false + static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool { - return lhs.state == rhs.state && lhs.fine_filter == rhs.fine_filter + return lhs.state == rhs.state && lhs.friend_filter == rhs.friend_filter && lhs.show_hellthreads == rhs.show_hellthreads } - init(state: NotificationFilterState = .all, fine_filter: FriendFilter = .all) { + init(state: NotificationFilterState = .all, friend_filter: FriendFilter = .all, show_hellthreads: Bool = false) { self.state = state - self.fine_filter = fine_filter + self.friend_filter = friend_filter + self.show_hellthreads = show_hellthreads } func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] { @@ -26,8 +28,11 @@ class NotificationFilter: ObservableObject, Equatable { if !self.state.filter(item) { return } - - if let item = item.filter({ self.fine_filter.filter(contacts: contacts, pubkey: $0.pubkey) }) { + + if let item = item.filter({ ev in + self.friend_filter.filter(contacts: contacts, pubkey: ev.pubkey) && + (show_hellthreads || !ev.is_hellthread) + }) { acc.append(item) } } @@ -65,7 +70,8 @@ struct NotificationsView: View { NotificationTab( NotificationFilter( state: .all, - fine_filter: filter.fine_filter + friend_filter: filter.friend_filter, + show_hellthreads: state.settings.hellthread_notification ) ) .tag(NotificationFilterState.all) @@ -73,7 +79,8 @@ struct NotificationsView: View { NotificationTab( NotificationFilter( state: .zaps, - fine_filter: filter.fine_filter + friend_filter: filter.friend_filter, + show_hellthreads: state.settings.hellthread_notification ) ) .tag(NotificationFilterState.zaps) @@ -81,7 +88,8 @@ struct NotificationsView: View { NotificationTab( NotificationFilter( state: .replies, - fine_filter: filter.fine_filter + friend_filter: filter.friend_filter, + show_hellthreads: state.settings.hellthread_notification ) ) .tag(NotificationFilterState.replies) @@ -98,20 +106,20 @@ struct NotificationsView: View { } ToolbarItem(placement: .navigationBarTrailing) { if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) { - FriendsButton(filter: $filter.fine_filter) + FriendsButton(filter: $filter.friend_filter) } } } - .onChange(of: filter.fine_filter) { val in + .onChange(of: filter.friend_filter) { val in state.settings.friend_filter = val - self.subtitle = filter.fine_filter.description() + self.subtitle = filter.friend_filter.description() } .onChange(of: filter_state) { val in filter.state = val } .onAppear { - self.filter.fine_filter = state.settings.friend_filter - self.subtitle = filter.fine_filter.description() + self.filter.friend_filter = state.settings.friend_filter + self.subtitle = filter.friend_filter.description() filter.state = filter_state } .safeAreaInset(edge: .top, spacing: 0) { diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift @@ -16,7 +16,9 @@ struct NotificationSettingsView: View { @State var notification_preferences_sync_state: PreferencesSyncState = .undefined @Environment(\.dismiss) var dismiss - + + let hellthread_notification_settings_text = pluralizedString(key: "hellthread_notification_settings", count: MIN_HELLTHREAD_PUBKEYS - 1) + func indicator_binding(_ val: NewEventsBits) -> Binding<Bool> { return Binding.init(get: { (settings.notification_indicators & val.rawValue) > 0 @@ -175,6 +177,8 @@ struct NotificationSettingsView: View { .toggleStyle(.switch) Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: self.notification_preference_binding($settings.notification_only_from_following)) .toggleStyle(.switch) + Toggle(hellthread_notification_settings_text, isOn: $settings.hellthread_notification) + .toggleStyle(.switch) } Section( diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict @@ -82,6 +82,38 @@ <string>%2$@ and %1$d others reposted</string> </dict> </dict> + <key>hellthread_notification_settings</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@HELLTHREAD_PROFILES@</string> + <key>HELLTHREAD_PROFILES</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>Show notifications that mention more than %d profile</string> + <key>other</key> + <string>Show notifications that mention more than %d profiles</string> + </dict> + </dict> + <key>quoted_reposts_count</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@QUOTE_REPOSTS@</string> + <key>QUOTE_REPOSTS</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>Quote</string> + <key>other</key> + <string>Quotes</string> + </dict> + </dict> <key>reacted_tagged_in_3</key> <dict> <key>NSStringLocalizedFormatKey</key> @@ -242,22 +274,6 @@ <string>Reposts</string> </dict> </dict> - <key>quoted_reposts_count</key> - <dict> - <key>NSStringLocalizedFormatKey</key> - <string>%#@QUOTE_REPOSTS@</string> - <key>QUOTE_REPOSTS</key> - <dict> - <key>NSStringFormatSpecTypeKey</key> - <string>NSStringPluralRuleType</string> - <key>NSStringFormatValueTypeKey</key> - <string>d</string> - <key>one</key> - <string>Quote</string> - <key>other</key> - <string>Quotes</string> - </dict> - </dict> <key>sats</key> <dict> <key>NSStringLocalizedFormatKey</key> diff --git a/damusTests/LargeEventTests.swift b/damusTests/LargeEventTests.swift @@ -0,0 +1,50 @@ +// +// LargeEventTests.swift +// damusTests +// +// Created by William Casarin on 2023-08-05. +// + +import XCTest +@testable import damus + +final class LargeEventTests: XCTestCase { + + func testLongPost() throws { + let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]" + let resp = NostrResponse.owned_from_json(json: json) + + XCTAssertNotNil(resp) + guard let resp, + case .event(let subid, let ev) = resp + else { + XCTAssertFalse(true) + return + } + + XCTAssertEqual(subid, "subid") + XCTAssertTrue(ev.should_show_event) + XCTAssertTrue(!ev.too_big) + XCTAssertTrue(should_show_event(state: test_damus_state, ev: ev)) + XCTAssertTrue(validate_event(ev: ev) == .ok) + } + + func testIsHellthread() throws { + let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]" + let resp = NostrResponse.owned_from_json(json: json) + + XCTAssertNotNil(resp) + guard let resp, + case .event(let subid, let ev) = resp + else { + XCTAssertFalse(true) + return + } + + XCTAssertEqual(subid, "subid") + XCTAssertTrue(ev.should_show_event) + XCTAssertTrue(ev.is_hellthread) + XCTAssertTrue(validate_event(ev: ev) == .ok) + } + +} diff --git a/damusTests/LocalizationUtilTests.swift b/damusTests/LocalizationUtilTests.swift @@ -18,12 +18,15 @@ final class LocalizationUtilTests: XCTestCase { ["followers_count", "Followers", "Follower", "Followers"], ["following_count", "Following", "Following", "Following"], ["imports_count", "Imports", "Import", "Imports"], + ["hellthread_notification_settings", "Show notifications that mention more than 0 profiles", "Show notifications that mention more than 1 profile", "Show notifications that mention more than 2 profiles"], + ["quoted_reposts_count", "Quotes", "Quote", "Quotes"], ["reactions_count", "Reactions", "Reaction", "Reactions"], ["relays_count", "Relays", "Relay", "Relays"], ["reposts_count", "Reposts", "Repost", "Reposts"], ["sats", "sats", "sat", "sats"], - ["zaps_count", "Zaps", "Zap", "Zaps"], - ["word_count", "0 Words", "1 Word", "2 Words"] + ["users_talking_about_it", "0 users talking about it", "1 user talking about it", "2 users talking about it"], + ["word_count", "0 Words", "1 Word", "2 Words"], + ["zaps_count", "Zaps", "Zap", "Zaps"] ] for key in keys { diff --git a/damusTests/LongPostTests.swift b/damusTests/LongPostTests.swift @@ -1,48 +0,0 @@ -// -// LongPostTests.swift -// damusTests -// -// Created by William Casarin on 2023-08-05. -// - -import XCTest -@testable import damus - -final class LongPostTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testLongPost() throws { - let contacts = Contacts(our_pubkey: test_keypair.pubkey) - let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]" - let resp = NostrResponse.owned_from_json(json: json) - - XCTAssertNotNil(resp) - guard let resp, - case .event(let subid, let ev) = resp - else { - XCTAssertFalse(true) - return - } - - XCTAssertEqual(subid, "subid") - XCTAssertTrue(ev.should_show_event) - XCTAssertTrue(!ev.too_big) - XCTAssertTrue(should_show_event(state: test_damus_state, ev: ev)) - XCTAssertTrue(validate_event(ev: ev) == .ok ) - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift @@ -13,6 +13,7 @@ import secp256k1_implementation import CryptoKit let MAX_NOTE_SIZE: Int = 2 << 18 +let MIN_HELLTHREAD_PUBKEYS = 11 struct NdbStr { let note: NdbNote @@ -299,6 +300,15 @@ extension NdbNote { return !too_big } + var is_hellthread: Bool { + switch known_kind { + case .text, .boost, .like, .zap: + Set(referenced_pubkeys).count >= MIN_HELLTHREAD_PUBKEYS + default: + false + } + } + func get_blocks(keypair: Keypair) -> Blocks { return parse_note_content(content: .init(note: self, keypair: keypair)) }