diff --git a/damus/Models/ContentFilters.swift b/damus/Models/ContentFilters.swift index 425d2b35..12bb03d5 100644 --- a/damus/Models/ContentFilters.swift +++ b/damus/Models/ContentFilters.swift @@ -10,8 +10,9 @@ import Foundation /// Simple filter to determine whether to show posts or all posts and replies. enum FilterState : Int { - case posts_and_replies = 1 case posts = 0 + case posts_and_replies = 1 + case conversations = 2 func filter(ev: NostrEvent) -> Bool { switch self { @@ -19,6 +20,8 @@ enum FilterState : Int { return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply() case .posts_and_replies: return true + case .conversations: + return true } } } diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index 90376383..4b30d0e2 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -22,8 +22,10 @@ class ProfileModel: ObservableObject, Equatable { var seen_event: Set = Set() var sub_id = UUID().description var prof_subid = UUID().description + var conversations_subid = UUID().description var findRelay_subid = UUID().description - + var conversation_events: Set = Set() + init(pubkey: Pubkey, damus: DamusState) { self.pubkey = pubkey self.damus = damus @@ -59,6 +61,9 @@ class ProfileModel: ObservableObject, Equatable { print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)") damus.pool.unsubscribe(sub_id: sub_id) damus.pool.unsubscribe(sub_id: prof_subid) + if pubkey != damus.pubkey { + damus.pool.unsubscribe(sub_id: conversations_subid) + } } func subscribe() { @@ -69,13 +74,29 @@ class ProfileModel: ObservableObject, Equatable { text_filter.authors = [pubkey] text_filter.limit = 500 - - print("subscribing to profile \(pubkey) with sub_id \(sub_id)") + + print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)") //print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]]) damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event) damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event) + + subscribe_to_conversations() } - + + private func subscribe_to_conversations() { + // Only subscribe to conversation events if the profile is not us. + guard pubkey != damus.pubkey else { + return + } + + let conversation_kinds: [NostrKind] = [.text, .longform, .highlight] + let limit: UInt32 = 500 + let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey]) + let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey]) + print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)") + damus.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event) + } + func handle_profile_contact_event(_ ev: NostrEvent) { process_contact_event(state: damus, ev: ev) @@ -90,15 +111,8 @@ class ProfileModel: ObservableObject, Equatable { self.following = count_pubkeys(ev.tags) self.relays = decode_json_relays(ev.content) } - - func add_event(_ ev: NostrEvent) { - guard ev.should_show_event else { - return - } - if seen_event.contains(ev.id) { - return - } + private func add_event(_ ev: NostrEvent) { if ev.is_textlike || ev.known_kind == .boost { if self.events.insert(ev) { self.objectWillChange.send() @@ -109,24 +123,57 @@ class ProfileModel: ObservableObject, Equatable { seen_event.insert(ev.id) } + // Ensure the event public key matches the public key(s) we are querying. + // This is done to protect against a relay not properly filtering events by the pubkey + // See https://github.com/damus-io/damus/issues/1846 for more information + private func relay_filtered_correctly(_ ev: NostrEvent, subid: String?) -> Bool { + if subid == self.conversations_subid { + switch ev.pubkey { + case self.pubkey: + return ev.referenced_pubkeys.contains(damus.pubkey) + case damus.pubkey: + return ev.referenced_pubkeys.contains(self.pubkey) + default: + return false + } + } + + return self.pubkey == ev.pubkey + } + private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { switch ev { case .ws_event: return case .nostr_event(let resp): - guard resp.subid == self.sub_id || resp.subid == self.prof_subid else { + guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else { return } switch resp { case .ok: break case .event(_, let ev): - // Ensure the event public key matches this profiles public key - // This is done to protect against a relay not properly filtering events by the pubkey - // See https://github.com/damus-io/damus/issues/1846 for more information - guard self.pubkey == ev.pubkey else { break } + guard ev.should_show_event else { + break + } - add_event(ev) + if !seen_event.contains(ev.id) { + guard relay_filtered_correctly(ev, subid: resp.subid) else { + break + } + + add_event(ev) + + if resp.subid == self.conversations_subid { + conversation_events.insert(ev.id) + } + } else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) { + guard relay_filtered_correctly(ev, subid: resp.subid) else { + break + } + + conversation_events.insert(ev.id) + } case .notice: break //notify(.notice, notice) diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index a6b310eb..91a21b2e 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -122,6 +122,9 @@ struct ProfileView: View { func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) { var filters = ContentFilters.defaults(damus_state: damus_state) filters.append(fstate.filter) + if fstate == .conversations { + filters.append({ profile.conversation_events.contains($0.id) } ) + } return ContentFilters(filters: filters).filter } @@ -429,6 +432,17 @@ struct ProfileView: View { .padding(.horizontal) } + var tabs: [(String, FilterState)] { + var tabs = [ + (NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts), + (NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies) + ] + if profile.pubkey != damus_state.pubkey && !profile.conversation_events.isEmpty { + tabs.append((NSLocalizedString("Conversations", comment: "Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile."), FilterState.conversations)) + } + return tabs + } + var body: some View { ZStack { ScrollView(.vertical) { @@ -440,10 +454,7 @@ struct ProfileView: View { aboutSection VStack(spacing: 0) { - CustomPicker(tabs: [ - (NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts), - (NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies) - ], selection: $filter_state) + CustomPicker(tabs: tabs, selection: $filter_state) Divider() .frame(height: 1) } @@ -455,6 +466,9 @@ struct ProfileView: View { if filter_state == FilterState.posts_and_replies { InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies)) } + if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty { + InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations)) + } } .padding(.horizontal, Theme.safeAreaInsets?.left) .zIndex(-yOffset > navbarHeight ? 0 : 1)