From bebaffd247d383bc7ae4bebd1167b60ca1735db3 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 13 Jul 2023 09:30:26 -0700 Subject: [PATCH 1/5] contacts: unify following logic We are about to add hashtag following, so let's prepare handle_follow for this. Generalize pubkey following to ReferenceId follows in the handle_{follow,unfollow} functions. We also split out the notification part into its own function. --- damus/ContentView.swift | 78 ++++++++++++++++++------------ damus/Views/FollowButtonView.swift | 8 +-- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/damus/ContentView.swift b/damus/ContentView.swift index e676fc30..36664959 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -395,16 +395,12 @@ struct ContentView: View { } } .onReceive(handle_notify(.unfollow)) { notif in - guard let state = self.damus_state else { - return - } - handle_unfollow(state: state, notif: notif) + guard let state = self.damus_state else { return } + handle_unfollow_notif(state: state, notif: notif) } .onReceive(handle_notify(.follow)) { notif in - guard let state = self.damus_state else { - return - } - handle_follow(state: state, notif: notif) + guard let state = self.damus_state else { return } + handle_follow_notif(state: state, notif: notif) } .onReceive(handle_notify(.post)) { notif in guard let state = self.damus_state, @@ -879,47 +875,67 @@ func timeline_name(_ timeline: Timeline?) -> String { } } -func handle_unfollow(state: DamusState, notif: Notification) { +func handle_unfollow(state: DamusState, unfollow: ReferencedId) { guard let keypair = state.keypair.to_full() else { return } - - let target = notif.object as! FollowTarget - let pk = target.pubkey + let old_contacts = state.contacts.event - guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: .p(pk)) - else { return } - - notify(.unfollowed, pk) - - state.contacts.event = ev - state.contacts.remove_friend(pk) - state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev) -} - -func handle_follow(state: DamusState, notif: Notification) { - guard let keypair = state.keypair.to_full() else { - return - } - - let fnotify = notif.object as! FollowTarget - - guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: .p(fnotify.pubkey)) + guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow) else { return } - notify(.followed, fnotify.pubkey) + notify(.unfollowed, unfollow) state.contacts.event = ev + if unfollow.key == "p" { + state.contacts.remove_friend(unfollow.ref_id) + state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev) + } +} + +func handle_unfollow_notif(state: DamusState, notif: Notification) { + let target = notif.object as! FollowTarget + let pk = target.pubkey + + handle_unfollow(state: state, unfollow: .p(pk)) +} + +@discardableResult +func handle_follow(state: DamusState, follow: ReferencedId) -> Bool { + guard let keypair = state.keypair.to_full() else { + return false + } + + guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow) + else { + return false + } + + notify(.followed, follow) + + state.contacts.event = ev + if follow.key == "p" { + state.contacts.add_friend_pubkey(follow.ref_id) + } + + return true +} + +@discardableResult +func handle_follow_notif(state: DamusState, notif: Notification) -> Bool { + let fnotify = notif.object as! FollowTarget switch fnotify { case .pubkey(let pk): state.contacts.add_friend_pubkey(pk) case .contact(let ev): state.contacts.add_friend_contact(ev) } + + return handle_follow(state: state, follow: .p(fnotify.pubkey)) } func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, notif: Notification) -> Bool { diff --git a/damus/Views/FollowButtonView.swift b/damus/Views/FollowButtonView.swift index 1ccd0820..574121d4 100644 --- a/damus/Views/FollowButtonView.swift +++ b/damus/Views/FollowButtonView.swift @@ -32,16 +32,16 @@ struct FollowButtonView: View { } } .onReceive(handle_notify(.followed)) { notif in - let pk = notif.object as! String - if pk != target.pubkey { + let pk = notif.object as! ReferencedId + if pk.key == "p", pk.ref_id != target.pubkey { return } self.follow_state = .follows } .onReceive(handle_notify(.unfollowed)) { notif in - let pk = notif.object as! String - if pk != target.pubkey { + let pk = notif.object as! ReferencedId + if pk.key == "p", pk.ref_id != target.pubkey { return } From 17df2972d9ed81bcbec28927526e198d3efc3a07 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 13 Jul 2023 07:05:53 -0700 Subject: [PATCH 2/5] ui: add follow hashtag ui on search view --- damus.xcodeproj/project.pbxproj | 12 ++ .../Components/Search/SearchHeaderView.swift | 134 ++++++++++++++++++ damus/Views/SearchView.swift | 76 +++++++--- 3 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 damus/Components/Search/SearchHeaderView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 26d825a1..11caf01e 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -144,6 +144,7 @@ 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; }; 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; }; 4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; }; + 4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; }; 4C687C272A6039500092C550 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C262A6039500092C550 /* TestData.swift */; }; 4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; }; 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; @@ -619,6 +620,7 @@ 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = ""; }; 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = ""; }; 4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = ""; }; + 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = ""; }; 4C687C262A6039500092C550 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = ""; }; 4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = ""; }; @@ -1099,6 +1101,14 @@ path = Notifications; sourceTree = ""; }; + 4C687C2A2A6058450092C550 /* Search */ = { + isa = PBXGroup; + children = ( + 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */, + ); + path = Search; + sourceTree = ""; + }; 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( @@ -1404,6 +1414,7 @@ 4CE4F9DF285287A000C00DD9 /* Components */ = { isa = PBXGroup; children = ( + 4C687C2A2A6058450092C550 /* Search */, 4C7D09702A0AEF4C00943473 /* Gradients */, 31D2E846295218AF006D67F8 /* Shimmer.swift */, 4CD7641A28A1641400B6928F /* EndBlock.swift */, @@ -1946,6 +1957,7 @@ 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */, 4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */, 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */, + 4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */, 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */, 4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */, 4C3EA64928FF597700C48A62 /* bech32.c in Sources */, diff --git a/damus/Components/Search/SearchHeaderView.swift b/damus/Components/Search/SearchHeaderView.swift new file mode 100644 index 00000000..fb88b56c --- /dev/null +++ b/damus/Components/Search/SearchHeaderView.swift @@ -0,0 +1,134 @@ +// +// SearchIconView.swift +// damus +// +// Created by William Casarin on 2023-07-12. +// + +import SwiftUI + +struct SearchHeaderView: View { + let state: DamusState + let described: DescribedSearch + @State var is_following: Bool + + init(state: DamusState, described: DescribedSearch) { + self.state = state + self.described = described + + let is_following = (described.is_hashtag.map { + ht in is_following_hashtag(contacts: state.contacts.event, hashtag: ht) + }) ?? false + + self._is_following = State(wrappedValue: is_following) + } + + var Icon: some View { + ZStack { + Circle() + .fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0)) + .frame(width: 54, height: 54) + + switch described { + case .hashtag: + Text("#") + .font(.largeTitle.bold()) + .foregroundStyle(PinkGradient) + .mask(Text("#") + .font(.largeTitle.bold())) + + case .unknown: + Image(systemName: "magnifyingglass") + .font(.title.bold()) + .foregroundStyle(PinkGradient) + } + } + } + + var SearchText: Text { + switch described { + case .hashtag(let ht): + Text(verbatim: "#" + ht) + case .unknown: + Text("Search") + } + } + + func unfollow(_ hashtag: String) { + is_following = false + handle_unfollow(state: state, unfollow: .t(hashtag)) + } + + func follow(_ hashtag: String) { + is_following = true + handle_follow(state: state, follow: .t(hashtag)) + } + + func FollowButton(_ ht: String) -> some View { + return Button(action: { follow(ht) }) { + Text("Follow hashtag") + .font(.footnote.bold()) + } + .buttonStyle(GradientButtonStyle(padding: 10)) + } + + func UnfollowButton(_ ht: String) -> some View { + return Button(action: { unfollow(ht) }) { + Text("Unfollow hashtag") + .font(.footnote.bold()) + } + .buttonStyle(GradientButtonStyle(padding: 10)) + } + + var body: some View { + HStack(alignment: .center, spacing: 30) { + Icon + + VStack(alignment: .leading, spacing: 10.0) { + SearchText + .foregroundStyle(DamusLogoGradient.gradient) + .font(.title.bold()) + + if state.is_privkey_user, case .hashtag(let ht) = described { + if is_following { + UnfollowButton(ht) + } else { + FollowButton(ht) + } + } + } + } + .onReceive(handle_notify(.followed)) { notif in + let ref = notif.object as! ReferencedId + guard hashtag_matches_search(desc: self.described, ref: ref) else { return } + self.is_following = true + } + .onReceive(handle_notify(.unfollowed)) { notif in + let ref = notif.object as! ReferencedId + guard hashtag_matches_search(desc: self.described, ref: ref) else { return } + self.is_following = false + } + } +} + +func hashtag_matches_search(desc: DescribedSearch, ref: ReferencedId) -> Bool { + guard let ht = desc.is_hashtag, ref.key == "t" && ref.ref_id == ht + else { return false } + return true +} + +func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool { + guard let contacts else { return false } + return is_already_following(contacts: contacts, follow: .t(hashtag)) +} + + +struct SearchHeaderView_Previews: PreviewProvider { + static var previews: some View { + VStack(alignment: .leading) { + SearchHeaderView(state: test_damus_state(), described: .hashtag("damus")) + + SearchHeaderView(state: test_damus_state(), described: .unknown) + } + } +} diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift index dd328372..818861b8 100644 --- a/damus/Views/SearchView.swift +++ b/damus/Views/SearchView.swift @@ -11,32 +11,70 @@ struct SearchView: View { let appstate: DamusState @ObservedObject var search: SearchModel @Environment(\.dismiss) var dismiss - + + let height: CGFloat = 250.0 + var body: some View { - TimelineView(events: search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true }) - .navigationBarTitle(describe_search(search.search)) - .onReceive(handle_notify(.switched_timeline)) { obj in - dismiss() - } - .onAppear() { - search.subscribe() - } - .onDisappear() { - search.unsubscribe() - } - .onReceive(handle_notify(.new_mutes)) { notif in - search.filter_muted() + TimelineView(events: search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true }) { + ZStack(alignment: .leading) { + DamusBackground(maxHeight: height) + .mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom)) + SearchHeaderView(state: appstate, described: described_search) + .padding(.leading, 30) + .padding(.top, 100) } + } + .ignoresSafeArea() + .onReceive(handle_notify(.switched_timeline)) { obj in + dismiss() + } + .onAppear() { + search.subscribe() + } + .onDisappear() { + search.unsubscribe() + } + .onReceive(handle_notify(.new_mutes)) { notif in + search.filter_muted() + } + } + + var described_search: DescribedSearch { + return describe_search(search.search) } } -func describe_search(_ filter: NostrFilter) -> String { - if let hashtags = filter.hashtag { - if hashtags.count >= 1 { - return "#" + hashtags[0] +enum DescribedSearch { + case hashtag(String) + case unknown + + var is_hashtag: String? { + switch self { + case .hashtag(let ht): + return ht + case .unknown: + return nil } } - return "Search" + + var description: String { + switch self { + case .hashtag(let s): + return "#" + s + case .unknown: + return "Search" + } + } +} + +func describe_search(_ filter: NostrFilter) -> DescribedSearch { + if let hashtags = filter.hashtag { + if hashtags.count >= 1 { + return .hashtag(hashtags[0]) + } + } + + return .unknown } struct SearchView_Previews: PreviewProvider { From 9a714943fdae28538f2e9824c7ee97ca2b6dd7fb Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 13 Jul 2023 11:02:13 -0700 Subject: [PATCH 3/5] contacts: get followed hashtags function todo: cache these --- damus/Models/Contacts.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift index cce25c4b..f64c7e01 100644 --- a/damus/Models/Contacts.swift +++ b/damus/Models/Contacts.swift @@ -69,7 +69,16 @@ class Contacts { func get_friend_list() -> [String] { return Array(friends) } - + + func get_followed_hashtags() -> Set { + guard let ev = self.event else { return Set() } + return ev.tags.reduce(into: Set(), { htags, tag in + if tag.count >= 2 && tag[0] == "t" && tag[1] != "" { + htags.insert(tag[1]) + } + }) + } + func add_friend_pubkey(_ pubkey: String) { friends.insert(pubkey) } From 122655bea340fb18b029f7ea76bca65b78111e98 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 13 Jul 2023 11:04:20 -0700 Subject: [PATCH 4/5] home: separate home filters we will want to resubscribe to these, so pull them out --- damus/Models/Contacts.swift | 4 +- damus/Models/HomeModel.swift | 85 +++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift index f64c7e01..24135907 100644 --- a/damus/Models/Contacts.swift +++ b/damus/Models/Contacts.swift @@ -66,8 +66,8 @@ class Contacts { } } - func get_friend_list() -> [String] { - return Array(friends) + func get_friend_list() -> Set { + return friends } func get_followed_hashtags() -> Set { diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 14cd9798..47e49fe7 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -382,8 +382,7 @@ class HomeModel { // TODO: since times should be based on events from a specific relay // perhaps we could mark this in the relay pool somehow - var friends = damus_state.contacts.get_friend_list() - friends.append(damus_state.pubkey) + let friends = get_friends() var contacts_filter = NostrFilter(kinds: [.metadata]) contacts_filter.authors = friends @@ -405,18 +404,6 @@ class HomeModel { dms_filter.pubkeys = [ damus_state.pubkey ] our_dms_filter.authors = [ damus_state.pubkey ] - // TODO: separate likes? - var home_filter_kinds: [NostrKind] = [ - .text, .longform, .boost - ] - if !damus_state.settings.onlyzaps_mode { - home_filter_kinds.append(.like) - } - var home_filter = NostrFilter(kinds: home_filter_kinds) - // include our pubkey as well even if we're not technically a friend - home_filter.authors = friends - home_filter.limit = 500 - var notifications_filter_kinds: [NostrKind] = [ .text, .boost, @@ -429,33 +416,71 @@ class HomeModel { notifications_filter.pubkeys = [damus_state.pubkey] notifications_filter.limit = 500 - var home_filters = [home_filter] var notifications_filters = [notifications_filter] var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter] var dms_filters = [dms_filter, our_dms_filter] + let last_of_kind = get_last_of_kind(relay_id: relay_id) - let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:] - - home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters) contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters) notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters) dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters) //print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters]) - if let relay_id { - pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id]) - pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id]) - pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id]) - pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: [relay_id]) - } else { - pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid))) - pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid))) - pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid))) - pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid))) - } + subscribe_to_home_filters(relay_id: relay_id) + + let relay_ids = relay_id.map { [$0] } + + pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: relay_ids) + pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: relay_ids) + pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids) } - + + func get_last_of_kind(relay_id: String?) -> [Int: NostrEvent] { + return relay_id.flatMap { last_event_of_kind[$0] } ?? [:] + } + + func unsubscribe_to_home_filters() { + pool.send(.unsubscribe(home_subid)) + } + + func get_friends() -> [String] { + var friends = damus_state.contacts.get_friend_list() + friends.insert(damus_state.pubkey) + return Array(friends) + } + + func subscribe_to_home_filters(friends fs: [String]? = nil, relay_id: String? = nil) { + // TODO: separate likes? + let home_filter_kinds: [NostrKind] = [ + .text, .longform, .boost + ] + //if !damus_state.settings.onlyzaps_mode { + //home_filter_kinds.append(.like) + //} + + let friends = fs ?? get_friends() + var home_filter = NostrFilter(kinds: home_filter_kinds) + // include our pubkey as well even if we're not technically a friend + home_filter.authors = friends + home_filter.limit = 500 + + var home_filters = [home_filter] + + let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags()) + if followed_hashtags.count != 0 { + var hashtag_filter = NostrFilter.filter_hashtag(followed_hashtags) + hashtag_filter.limit = 100 + home_filters.append(hashtag_filter) + } + + let relay_ids = relay_id.map { [$0] } + home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters) + let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid) + + pool.send(.subscribe(sub), to: relay_ids) + } + func handle_list_event(_ ev: NostrEvent) { // we only care about our lists guard ev.pubkey == damus_state.pubkey else { From 31fa63debf98ac0ccdf8df4ade726872b66c4cf3 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 13 Jul 2023 11:06:07 -0700 Subject: [PATCH 5/5] home: hide users and hashtags from home timeline when you unfollow Add the ability to resubscribe to home filters so that it will be updated when you follow and unfollow people Changelog-Fixed: Hide users and hashtags from home timeline when you unfollow --- damus/ContentView.swift | 32 ++++++++++--- damus/Models/HomeModel.swift | 92 ++++++++++++++++++++++++++++++------ 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 36664959..2d77d57e 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -396,11 +396,21 @@ struct ContentView: View { } .onReceive(handle_notify(.unfollow)) { notif in guard let state = self.damus_state else { return } - handle_unfollow_notif(state: state, notif: notif) + guard let unfollow = handle_unfollow_notif(state: state, notif: notif) else { return } + } + .onReceive(handle_notify(.unfollowed)) { notif in + guard let state = self.damus_state else { return } + let unfollow = notif.object as! ReferencedId + home.resubscribe(.unfollowing(unfollow)) } .onReceive(handle_notify(.follow)) { notif in guard let state = self.damus_state else { return } - handle_follow_notif(state: state, notif: notif) + guard handle_follow_notif(state: state, notif: notif) else { return } + } + .onReceive(handle_notify(.followed)) { notif in + guard let state = self.damus_state else { return } + let follow = notif.object as! ReferencedId + home.resubscribe(.following) } .onReceive(handle_notify(.post)) { notif in guard let state = self.damus_state, @@ -875,16 +885,17 @@ func timeline_name(_ timeline: Timeline?) -> String { } } -func handle_unfollow(state: DamusState, unfollow: ReferencedId) { +@discardableResult +func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool { guard let keypair = state.keypair.to_full() else { - return + return false } let old_contacts = state.contacts.event guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow) else { - return + return false } notify(.unfollowed, unfollow) @@ -895,13 +906,20 @@ func handle_unfollow(state: DamusState, unfollow: ReferencedId) { state.contacts.remove_friend(unfollow.ref_id) state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev) } + + return true } -func handle_unfollow_notif(state: DamusState, notif: Notification) { +func handle_unfollow_notif(state: DamusState, notif: Notification) -> ReferencedId? { let target = notif.object as! FollowTarget let pk = target.pubkey - handle_unfollow(state: state, unfollow: .p(pk)) + let ref = ReferencedId.p(pk) + if handle_unfollow(state: state, unfollow: ref) { + return ref + } + + return nil } @discardableResult diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 47e49fe7..b3247ccb 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -23,6 +23,40 @@ struct NewEventsBits: OptionSet { static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions] } +enum Resubscribe { + case following + case unfollowing(ReferencedId) +} + +enum HomeResubFilter { + case pubkey(String) + case hashtag(String) + + init?(from: ReferencedId) { + if from.key == "p" { + self = .pubkey(from.ref_id) + return + } else if from.key == "t" { + self = .hashtag(from.ref_id) + return + } + + return nil + } + + func filter(contacts: Contacts, ev: NostrEvent) -> Bool { + switch self { + case .pubkey(let pk): + return ev.pubkey == pk + case .hashtag(let ht): + if contacts.is_friend(ev.pubkey) { + return false + } + return ev.references(id: ht, key: "t") + } + } +} + class HomeModel { // Don't trigger a user notification for events older than a certain age static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60 @@ -36,6 +70,7 @@ class HomeModel { var done_init: Bool = false var incoming_dms: [NostrEvent] = [] let dm_debouncer = Debouncer(interval: 0.5) + let resub_debouncer = Debouncer(interval: 3.0) var should_debounce_dms = true let home_subid = UUID().description @@ -90,6 +125,31 @@ class HomeModel { } } + func resubscribe(_ resubbing: Resubscribe) { + if self.should_debounce_dms { + // don't resub on initial load + return + } + + print("hit resub debouncer") + + resub_debouncer.debounce { + print("resub") + self.unsubscribe_to_home_filters() + + switch resubbing { + case .following: + break + case .unfollowing(let r): + if let filter = HomeResubFilter(from: r) { + self.events.filter { ev in !filter.filter(contacts: self.damus_state.contacts, ev: ev) } + } + } + + self.subscribe_to_home_filters() + } + } + func process_event(sub_id: String, relay_id: String, ev: NostrEvent) { if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) { return @@ -639,32 +699,34 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) { func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { let contacts = state.contacts - var new_pks = Set() + var new_refs = Set() // our contacts for tag in ev.tags { - if tag.count >= 2 && tag[0] == "p" { - new_pks.insert(tag[1]) - } + guard let ref = tag_to_refid(tag) else { continue } + new_refs.insert(ref) } - var old_pks = Set() + var old_refs = Set() // find removed contacts if let old_ev = m_old_ev { for tag in old_ev.tags { - if tag.count >= 2 && tag[0] == "p" { - old_pks.insert(tag[1]) - } + guard let ref = tag_to_refid(tag) else { continue } + old_refs.insert(ref) } } - let diff = new_pks.symmetricDifference(old_pks) - for pk in diff { - if new_pks.contains(pk) { - notify(.followed, pk) - contacts.add_friend_pubkey(pk) + let diff = new_refs.symmetricDifference(old_refs) + for ref in diff { + if new_refs.contains(ref) { + notify(.followed, ref) + if ref.key == "p" { + contacts.add_friend_pubkey(ref.ref_id) + } } else { - notify(.unfollowed, pk) - contacts.remove_friend(pk) + notify(.unfollowed, ref) + if ref.key == "p" { + contacts.remove_friend(ref.ref_id) + } } }