damus

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

commit 9bf8349db60a9f247aa02e29a1205c8bb7ec9716
parent 4c44de927675a095881f07217c2ce28af25a050f
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 21 Apr 2023 14:17:37 -0700

Friends filter for notifications

Changelog-Added: Friends filter for notifications

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 12++++++++++++
Mdamus/Models/Notifications/EventGroup.swift | 18++++++++++++++++++
Mdamus/Models/Notifications/ZapGroup.swift | 25+++++++++++++++++++++----
Mdamus/Models/NotificationsModel.swift | 31+++++++++++++++++++++++++++++++
Mdamus/Util/Zap.swift | 4++++
Adamus/Views/Buttons/FriendsButton.swift | 39+++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Notifications/NotificationsView.swift | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
7 files changed, 264 insertions(+), 27 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -143,6 +143,7 @@ 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; }; 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; }; 4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */; }; + 4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; }; 4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; }; 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; }; 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; }; @@ -550,6 +551,7 @@ 4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; }; 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; }; 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendIcon.swift; sourceTree = "<group>"; }; + 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendsButton.swift; sourceTree = "<group>"; }; 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; }; 4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; }; 4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; }; @@ -881,6 +883,7 @@ 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4C8D1A6D29F31E4100ACDF75 /* Buttons */, 4C1A9A1B29DDCF8B00516EAC /* Settings */, 4CFF8F6129CC9A80008DB934 /* Images */, 4CCEB7AC29B53D180078AA28 /* Search */, @@ -1009,6 +1012,14 @@ path = Util; sourceTree = "<group>"; }; + 4C8D1A6D29F31E4100ACDF75 /* Buttons */ = { + isa = PBXGroup; + children = ( + 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */, + ); + path = Buttons; + sourceTree = "<group>"; + }; 4CAAD8AE29888A9B00060CEA /* Relays */ = { isa = PBXGroup; children = ( @@ -1535,6 +1546,7 @@ 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, 4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */, 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, + 4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */, 4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */, diff --git a/damus/Models/Notifications/EventGroup.swift b/damus/Models/Notifications/EventGroup.swift @@ -29,4 +29,22 @@ class EventGroup { func insert(_ ev: NostrEvent) -> Bool { return insert_uniq_sorted_event_created(events: &events, new_ev: ev) } + + func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { + for ev in events { + if !isIncluded(ev) { + return true + } + } + + return false + } + + func filter(_ isIncluded: (NostrEvent) -> Bool) -> EventGroup? { + let new_evs = events.filter(isIncluded) + guard new_evs.count > 0 else { + return nil + } + return EventGroup(events: new_evs) + } } diff --git a/damus/Models/Notifications/ZapGroup.swift b/damus/Models/Notifications/ZapGroup.swift @@ -30,10 +30,26 @@ class ZapGroup { } } - init(zaps: [Zap]) { - self.zaps = zaps - self.msat_total = 0 - self.zappers = Set() + func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { + for zap in zaps { + if !isIncluded(zap.request_ev) { + return true + } + } + + return false + } + + func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? { + let new_zaps = zaps.filter { isIncluded($0.request_ev) } + guard new_zaps.count > 0 else { + return nil + } + let grp = ZapGroup() + for zap in new_zaps { + grp.insert(zap) + } + return grp } init() { @@ -42,6 +58,7 @@ class ZapGroup { self.zappers = Set() } + @discardableResult func insert(_ zap: Zap) -> Bool { if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) { return false diff --git a/damus/Models/NotificationsModel.swift b/damus/Models/NotificationsModel.swift @@ -65,6 +65,37 @@ enum NotificationItem { return reply.created_at } } + + func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { + switch self { + case .repost(_, let evgrp): + return evgrp.would_filter(isIncluded) + case .reaction(_, let evgrp): + return evgrp.would_filter(isIncluded) + case .profile_zap(let zapgrp): + return zapgrp.would_filter(isIncluded) + case .event_zap(_, let zapgrp): + return zapgrp.would_filter(isIncluded) + case .reply(let ev): + return !isIncluded(ev) + } + } + + func filter(_ isIncluded: (NostrEvent) -> Bool) -> NotificationItem? { + switch self { + case .repost(let evid, let evgrp): + return evgrp.filter(isIncluded).map { .repost(evid, $0) } + case .reaction(let evid, let evgrp): + return evgrp.filter(isIncluded).map { .reaction(evid, $0) } + case .profile_zap(let zapgrp): + return zapgrp.filter(isIncluded).map { .profile_zap($0) } + case .event_zap(let evid, let zapgrp): + return zapgrp.filter(isIncluded).map { .event_zap(evid, $0) } + case .reply(let ev): + if isIncluded(ev) { return .reply(ev) } + return nil + } + } } class NotificationsModel: ObservableObject, ScrollQueue { diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift @@ -52,6 +52,10 @@ struct Zap { public let is_anon: Bool public let private_request: NostrEvent? + var request_ev: NostrEvent { + return private_request ?? self.request.ev + } + public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? { /// Make sure that we only create a zap event if it is authorized by the profile or event guard zapper == zap_ev.pubkey else { diff --git a/damus/Views/Buttons/FriendsButton.swift b/damus/Views/Buttons/FriendsButton.swift @@ -0,0 +1,39 @@ +// +// FriendsButton.swift +// damus +// +// Created by William Casarin on 2023-04-21. +// + +import SwiftUI + +struct FriendsButton: View { + @Binding var enabled: Bool + + var body: some View { + Button(action: { + self.enabled.toggle() + }) { + if enabled { + LINEAR_GRADIENT + .mask(Image(systemName: "person.2.fill") + .resizable() + ).frame(width: 30, height: 20) + } else { + Image(systemName: "person.2.fill") + .resizable() + .frame(width: 30, height: 20) + .foregroundColor(DamusColors.adaptableGrey) + } + } + .buttonStyle(.plain) + } +} + +struct FriendsButton_Previews: PreviewProvider { + @State static var enabled: Bool = false + + static var previews: some View { + FriendsButton(enabled: $enabled) + } +} diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift @@ -7,6 +7,69 @@ import SwiftUI +enum FineNotificationFilter: String { + case all + case friends + + func filter(contacts: Contacts, pubkey: String) -> Bool { + switch self { + case .all: + return true + case .friends: + return contacts.is_in_friendosphere(pubkey) + } + } +} + +class NotificationFilter: ObservableObject, Equatable { + @Published var state: NotificationFilterState + @Published var fine_filter: FineNotificationFilter + + static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool { + return lhs.state == rhs.state && lhs.fine_filter == rhs.fine_filter + } + + init() { + self.state = .all + self.fine_filter = .all + } + + init(state: NotificationFilterState, fine_filter: FineNotificationFilter) { + self.state = state + self.fine_filter = fine_filter + } + + func toggle_fine_filter() { + switch self.fine_filter { + case .all: + self.fine_filter = .friends + case .friends: + self.fine_filter = .all + } + } + + var fine_filter_binding: Binding<Bool> { + Binding(get: { + return self.fine_filter == .friends + }, set: { v in + self.fine_filter = v ? .friends : .all + }) + } + + func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] { + + return items.reduce(into: []) { acc, item in + if !self.state.filter(item) { + return + } + + if let item = item.filter({ self.fine_filter.filter(contacts: contacts, pubkey: $0.pubkey) }) { + acc.append(item) + } + } + } +} + enum NotificationFilterState: String { case all case zaps @@ -31,7 +94,7 @@ enum NotificationFilterState: String { struct NotificationsView: View { let state: DamusState @ObservedObject var notifications: NotificationsModel - @State var filter_state: NotificationFilterState = .all + @StateObject var filter_state: NotificationFilter = NotificationFilter() @Environment(\.colorScheme) var colorScheme @@ -44,28 +107,55 @@ struct NotificationsView: View { } var body: some View { - TabView(selection: $filter_state) { + TabView(selection: $filter_state.state) { + // This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why. mystery - // This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why. - NotificationTab(NotificationFilterState.all) - .tag(NotificationFilterState.all) + NotificationTab( + NotificationFilter( + state: .all, + fine_filter: filter_state.fine_filter + ) + ) + .tag(NotificationFilterState.all) - NotificationTab(NotificationFilterState.zaps) - .tag(NotificationFilterState.zaps) + NotificationTab( + NotificationFilter( + state: .zaps, + fine_filter: filter_state.fine_filter + ) + ) + .tag(NotificationFilterState.zaps) - NotificationTab(NotificationFilterState.replies) - .tag(NotificationFilterState.replies) + NotificationTab( + NotificationFilter( + state: .replies, + fine_filter: filter_state.fine_filter + ) + ) + .tag(NotificationFilterState.replies) } - .onChange(of: filter_state) { val in + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if would_filter_non_friends_from_notifications(contacts: state.contacts, state: self.filter_state.state, items: self.notifications.notifications) { + FriendsButton(enabled: self.filter_state.fine_filter_binding) + } + } + } + .onChange(of: filter_state.fine_filter) { val in + save_friend_filter(pubkey: state.pubkey, filter: val) + } + .onChange(of: filter_state.state) { val in save_notification_filter_state(pubkey: state.pubkey, state: val) } .onAppear { - self.filter_state = load_notification_filter_state(pubkey: state.pubkey) + let state = load_notification_filter_state(pubkey: state.pubkey) + self.filter_state.fine_filter = state.fine_filter + self.filter_state.state = state.state } .safeAreaInset(edge: .top, spacing: 0) { VStack(spacing: 0) { - CustomPicker(selection: $filter_state, content: { + CustomPicker(selection: $filter_state.state, content: { Text("All", comment: "Label for filter for all notifications.") .tag(NotificationFilterState.all) @@ -83,14 +173,14 @@ struct NotificationsView: View { } } - func NotificationTab(_ filter: NotificationFilterState) -> some View { + func NotificationTab(_ filter: NotificationFilter) -> some View { ScrollViewReader { scroller in ScrollView { LazyVStack(alignment: .leading) { Color.white.opacity(0) .id("startblock") .frame(height: 5) - ForEach(notifications.notifications.filter(filter.filter), id: \.id) { item in + ForEach(filter.filter(contacts: state.contacts, items: notifications.notifications), id: \.id) { item in NotificationItemView(state: state, item: item) } } @@ -116,7 +206,7 @@ struct NotificationsView: View { struct NotificationsView_Previews: PreviewProvider { static var previews: some View { - NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilterState.all) + NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilter()) } } @@ -124,22 +214,48 @@ func notification_filter_state_key(pubkey: String) -> String { return pk_setting_key(pubkey, key: "notification_filter_state") } -func load_notification_filter_state(pubkey: String) -> NotificationFilterState { +func friend_filter_key(pubkey: String) -> String { + return pk_setting_key(pubkey, key: "friend_filter") +} + +func load_notification_filter_state(pubkey: String) -> NotificationFilter { let key = notification_filter_state_key(pubkey: pubkey) + let fine_key = friend_filter_key(pubkey: pubkey) - guard let state_str = UserDefaults.standard.string(forKey: key) else { - return .all - } + let state_str = UserDefaults.standard.string(forKey: key) + let state = (state_str.flatMap { NotificationFilterState(rawValue: $0) }) ?? .all - guard let state = NotificationFilterState(rawValue: state_str) else { - return .all - } + let filter_str = UserDefaults.standard.string(forKey: fine_key) + let filter = (filter_str.flatMap { FineNotificationFilter(rawValue: $0) } ) ?? .all - return state + return NotificationFilter(state: state, fine_filter: filter) } func save_notification_filter_state(pubkey: String, state: NotificationFilterState) { let key = notification_filter_state_key(pubkey: pubkey) + UserDefaults.standard.set(state.rawValue, forKey: key) } + +func save_friend_filter(pubkey: String, filter: FineNotificationFilter) { + let key = friend_filter_key(pubkey: pubkey) + + UserDefaults.standard.set(filter.rawValue, forKey: key) +} + +func would_filter_non_friends_from_notifications(contacts: Contacts, state: NotificationFilterState, items: [NotificationItem]) -> Bool { + for item in items { + // this is only valid depending on which tab we're looking at + if !state.filter(item) { + continue + } + + if item.would_filter({ ev in FineNotificationFilter.friends.filter(contacts: contacts, pubkey: ev.pubkey) }) { + return true + } + } + + return false +} +