damus

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

commit 10596ddb09690cf2ab5b7c5917dff6b38075fec9
parent 989684cd375b3782aceefa1225574f4da59ca334
Author: William Casarin <jb55@jb55.com>
Date:   Wed,  8 Feb 2023 11:07:58 -0800

Relay Filters

wip

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 12++++++++----
Mdamus/ContentView.swift | 31+++++++++++++++++++++++++++++--
Mdamus/Models/DamusState.swift | 3++-
Mdamus/Models/SearchHomeModel.swift | 3++-
Mdamus/Models/SearchModel.swift | 15+++++++++++++++
Mdamus/Nostr/RelayPool.swift | 23+++++++++++++++++++++--
Adamus/Util/RelayFilters.swift | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/EventDetailView.swift | 267+------------------------------------------------------------------------------
Mdamus/Views/MainTabView.swift | 2+-
Adamus/Views/RelayFilterView.swift | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddamus/Views/ThreadView.swift | 99-------------------------------------------------------------------------------
Mdamus/damusApp.swift | 1-
MdamusTests/damusTests.swift | 18++++++++++++++++++
13 files changed, 237 insertions(+), 376 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -31,7 +31,6 @@ 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; }; 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; }; 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; - 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; }; 4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; }; 4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; }; 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; }; @@ -168,6 +167,7 @@ 4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; }; 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; }; 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; }; + 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; }; 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; }; 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; }; 4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; }; @@ -193,6 +193,7 @@ 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; }; + 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; }; 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; }; 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; }; 6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; }; @@ -289,7 +290,6 @@ 4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; }; 4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; - 4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; }; 4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; }; 4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; }; 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; }; @@ -458,6 +458,7 @@ 4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; }; 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; }; 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; }; + 4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; }; 4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsecp256k1.a; sourceTree = "<group>"; }; 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRequest.swift; sourceTree = "<group>"; }; 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; }; @@ -484,6 +485,7 @@ 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; }; 5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; }; 6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; }; + 643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; }; 647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; }; 64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; }; 7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; }; @@ -692,7 +694,6 @@ BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */, 4C3AC7A02835A81400E1F516 /* SetupView.swift */, E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */, - 4C0A3F96280F8E02000448DE /* ThreadView.swift */, 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */, 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */, 647D9A8C2968520300A295DE /* SideMenuView.swift */, @@ -703,6 +704,7 @@ 4CF0ABE42981EE0C00D66079 /* EULAView.swift */, 3AA247FE297E3D900090C62D /* RepostsView.swift */, 5C513FCB2984ACA60072348F /* QRCodeView.swift */, + 643EA5C7296B764E005081BB /* RelayFilterView.swift */, ); path = Views; sourceTree = "<group>"; @@ -755,6 +757,7 @@ 4CB883A72975FC1800DC99E7 /* Zaps.swift */, 4CB883B5297730E400DC99E7 /* LNUrls.swift */, 3AB72AB8298ECF30004BB58C /* Translator.swift */, + 4CE8794729941DA700F758CC /* RelayFilters.swift */, ); path = Util; sourceTree = "<group>"; @@ -1187,6 +1190,7 @@ F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */, 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */, 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */, + 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */, 7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */, @@ -1269,6 +1273,7 @@ 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, + 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */, 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */, 4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */, 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */, @@ -1278,7 +1283,6 @@ F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, 4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */, 4CF0ABD42980996B00D66079 /* Report.swift in Sources */, - 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */, 4C06670B28FDE64700038D2A /* damus.c in Sources */, 3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */, 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -27,12 +27,16 @@ enum Sheets: Identifiable { case post case report(ReportTarget) case reply(NostrEvent) + case event(NostrEvent) + case filter var id: String { switch self { case .report: return "report" case .post: return "post" case .reply(let ev): return "reply-" + ev.id + case .event(let ev): return "event-" + ev.id + case .filter: return "filter" } } } @@ -282,7 +286,16 @@ struct ContentView: View { .foregroundColor(.gray) } } - + + Button(action: { + //isFilterVisible.toggle() + self.active_sheet = .filter + }) { + // checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease + Label("Filter", systemImage: "line.3.horizontal.decrease") + .foregroundColor(.gray) + //.contentShape(Rectangle()) + } } } } @@ -311,6 +324,17 @@ struct ContentView: View { PostView(replying_to: nil, references: [], damus_state: damus_state!) case .reply(let event): ReplyView(replying_to: event, damus: damus_state!) + case .event(let event): + EventDetailView() + case .filter: + let timeline = selected_timeline ?? .home + if #available(iOS 16.0, *) { + RelayFilterView(state: damus_state!, timeline: timeline) + .presentationDetents([.height(550)]) + .presentationDragIndicator(.visible) + } else { + RelayFilterView(state: damus_state!, timeline: timeline) + } } } .onOpenURL { url in @@ -429,6 +453,8 @@ struct ContentView: View { let post_res = obj.object as! NostrPostResult switch post_res { case .post(let post): + //let post = tup.0 + //let to_relays = tup.1 print("post \(post.content)") let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey) self.damus_state?.pool.send(.event(new_ev)) @@ -576,7 +602,8 @@ struct ContentView: View { previews: PreviewCache(), zaps: Zaps(our_pubkey: pubkey), lnurls: LNUrls(), - settings: UserSettingsStore() + settings: UserSettingsStore(), + relay_filters: RelayFilters(our_pubkey: pubkey) ) home.damus_state = self.damus_state! diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -21,6 +21,7 @@ struct DamusState { let zaps: Zaps let lnurls: LNUrls let settings: UserSettingsStore + let relay_filters: RelayFilters var pubkey: String { return keypair.pubkey @@ -32,6 +33,6 @@ struct DamusState { static var empty: DamusState { - return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore()) + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: "")) } } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -36,7 +36,8 @@ class SearchHomeModel: ObservableObject { func subscribe() { loading = true - damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event) + let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters) + damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays) } func unsubscribe(to: String? = nil) { diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift @@ -98,6 +98,21 @@ func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool { return false } +func tag_is_hashtag(_ tag: [String]) -> Bool { + // "hashtag" is deprecated, will remove in the future + return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t") +} + +func has_hashtag(_ tags: [[String]], hashtag: String) -> Bool { + for tag in tags { + if tag_is_hashtag(tag) && tag[1] == hashtag { + return true + } + } + + return false +} + func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool { if let hashtags = filter.hashtag { return event_matches_hashtag(ev, hashtags: hashtags) diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -42,6 +42,8 @@ class RelayPool { var relays: [Relay] = [] var handlers: [RelayHandler] = [] var request_queue: [QueuedRequest] = [] + var seen: Set<String> = Set() + var counts: [String: UInt64] = [:] var descriptors: [RelayDescriptor] { relays.map { $0.descriptor } @@ -149,9 +151,9 @@ class RelayPool { self.send(.unsubscribe(sub_id), to: to) } - func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> ()) { + func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) { register_handler(sub_id: sub_id, handler: handler) - send(.subscribe(.init(filters: filters, sub_id: sub_id))) + send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to) } func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) { @@ -241,8 +243,25 @@ class RelayPool { } } + func record_seen(relay_id: String, event: NostrConnectionEvent) { + if case .nostr_event(let ev) = event { + if case .event(_, let nev) = ev { + let k = relay_id + nev.id + if !seen.contains(k) { + seen.insert(k) + if counts[relay_id] == nil { + counts[relay_id] = 1 + } else { + counts[relay_id] = (counts[relay_id] ?? 0) + 1 + } + } + } + } + } + func handle_event(relay_id: String, event: NostrConnectionEvent) { record_last_pong(relay_id: relay_id, event: event) + record_seen(relay_id: relay_id, event: event) // run req queue when we reconnect if case .ws_event(let ws) = event { diff --git a/damus/Util/RelayFilters.swift b/damus/Util/RelayFilters.swift @@ -0,0 +1,80 @@ +// +// RelayFilters.swift +// damus +// +// Created by William Casarin on 2023-02-08. +// + +import Foundation + +struct RelayFilter: Hashable { + let timeline: Timeline + let relay_id: String +} + +class RelayFilters { + private let our_pubkey: String + private var disabled: Set<RelayFilter> + + func is_filtered(timeline: Timeline, relay_id: String) -> Bool { + let filter = RelayFilter(timeline: timeline, relay_id: relay_id) + let contains = disabled.contains(filter) + return contains + } + + func remove(timeline: Timeline, relay_id: String) { + let filter = RelayFilter(timeline: timeline, relay_id: relay_id) + if !disabled.contains(filter) { + return + } + + disabled.remove(filter) + save_relay_filters(our_pubkey, filters: disabled) + } + + func insert(timeline: Timeline, relay_id: String) { + let filter = RelayFilter(timeline: timeline, relay_id: relay_id) + if disabled.contains(filter) { + return + } + + disabled.insert(filter) + save_relay_filters(our_pubkey, filters: disabled) + } + + init(our_pubkey: String) { + self.our_pubkey = our_pubkey + disabled = load_relay_filters(our_pubkey) + } +} + +func save_relay_filters(_ pubkey: String, filters: Set<RelayFilter>) { + let key = pk_setting_key(pubkey, key: "relay_filters") + let arr = Array(filters.map { filter in "\(filter.timeline)\t\(filter.relay_id)" }) + UserDefaults.standard.set(arr, forKey: key) +} + +func load_relay_filters(_ pubkey: String) -> Set<RelayFilter> { + let key = pk_setting_key(pubkey, key: "relay_filters") + guard let filters = UserDefaults.standard.stringArray(forKey: key) else { + return Set() + } + + return filters.reduce(into: Set()) { s, str in + let parts = str.components(separatedBy: "\t") + guard parts.count == 2 else { + return + } + guard let timeline = Timeline.init(rawValue: parts[0]) else { + return + } + let filter = RelayFilter(timeline: timeline, relay_id: parts[1]) + s.insert(filter) + } +} + +func determine_to_relays(pool: RelayPool, filters: RelayFilters) -> [String] { + return pool.descriptors + .map { $0.url.absoluteString } + .filter { !filters.is_filtered(timeline: .search, relay_id: $0) } +} diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -7,119 +7,10 @@ import SwiftUI -struct CollapsedEvents: Identifiable { - let count: Int - let start: Int - let end: Int - - var id: String = UUID().description -} - -enum CollapsedEvent: Identifiable { - case event(NostrEvent, Highlight) - case collapsed(CollapsedEvents) - - var id: String { - switch self { - case .event(let ev, _): - return ev.id - case .collapsed(let c): - return c.id - } - } -} - struct EventDetailView: View { - let sub_id = UUID().description - let damus: DamusState - - @StateObject var thread: ThreadModel - @State var collapsed: Bool = true - - func toggle_collapse_thread(scroller: ScrollViewProxy, id mid: String?, animate: Bool = true) { - self.collapsed = !self.collapsed - if let id = mid { - if !self.collapsed { - scroll_to_event(scroller: scroller, id: id, delay: 0.1, animate: animate) - } - } - } - - func uncollapse_section(scroller: ScrollViewProxy, c: CollapsedEvents) - { - let ev = thread.events[c.start] - print("uncollapsing section at \(c.start) '\(ev.content.prefix(12))...'") - let start_id = ev.id - - toggle_collapse_thread(scroller: scroller, id: start_id, animate: false) - } - - func CollapsedEventView(_ cev: CollapsedEvent, scroller: ScrollViewProxy) -> some View { - Group { - switch cev { - case .collapsed(let c): - Text(String(format: NSLocalizedString("collapsed_event_view_other_notes", comment: "Text to indicate that the thread was collapsed and that there are other notes to view if tapped."), c.count)) - .padding([.top,.bottom], 8) - .font(.footnote) - .foregroundColor(.gray) - .onTapGesture { - //self.uncollapse_section(scroller: proxy, c: c) - //self.toggle_collapse_thread(scroller: proxy, id: nil) - if let ev = thread.events[safe: c.start] { - thread.set_active_event(ev, privkey: damus.keypair.privkey) - } - toggle_thread_view() - } - case .event(let ev, _): - EventView(damus: damus, event: ev, has_action_bar: true) - .onTapGesture { - if thread.initial_event.id == ev.id { - toggle_thread_view() - } else { - thread.set_active_event(ev, privkey: damus.keypair.privkey) - } - } - } - } - } - var body: some View { - ScrollViewReader { proxy in - if thread.loading { - ProgressView().progressViewStyle(.circular) - } - - ScrollView(.vertical) { - LazyVStack { - let collapsed_events = calculated_collapsed_events( - privkey: damus.keypair.privkey, - collapsed: self.collapsed, - active: thread.event, - events: thread.events - ) - ForEach(collapsed_events, id: \.id) { cev in - CollapsedEventView(cev, scroller: proxy) - } - } - .padding(.horizontal) - .padding(.top) - - EndBlock() - } - .onChange(of: thread.loading) { val in - scroll_after_load(thread: thread, proxy: proxy) - } - .onAppear() { - scroll_after_load(thread: thread, proxy: proxy) - } - } - .navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread.")) - - } - - func toggle_thread_view() { - NotificationCenter.default.post(name: .toggle_thread_view, object: nil) + Text("EventDetailView") } } @@ -130,165 +21,11 @@ func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) { } } - struct EventDetailView_Previews: PreviewProvider { static var previews: some View { let state = test_damus_state() - let tm = ThreadModel(evid: "4da698ceac09a16cdb439276fa3d13ef8f6620ffb45d11b76b3f103483c2d0b0", damus_state: state) - EventDetailView(damus: state, thread: tm) - } -} - -/// Find the entire reply path for the active event -func make_reply_map(active: NostrEvent, events: [NostrEvent], privkey: String?) -> [String: ()] -{ - let event_map: [String: Int] = zip(events,0...events.count).reduce(into: [:]) { (acc, arg1) in - let (ev, i) = arg1 - acc[ev.id] = i - } - var is_reply: [String: ()] = [:] - var i: Int = 0 - var start: Int = 0 - var iterations: Int = 0 - - if events.count == 0 { - return is_reply - } - - for ev in events { - /// does this event reply to the active event? - let ev_refs = ev.event_refs(privkey) - for ev_ref in ev_refs { - if let reply = ev_ref.is_reply { - if reply.ref_id == active.id { - is_reply[ev.id] = () - start = i - } - } - } - - /// does the active event reply to this event? - let active_refs = active.event_refs(privkey) - for active_ref in active_refs { - if let reply = active_ref.is_reply { - if reply.ref_id == ev.id { - is_reply[ev.id] = () - start = i - } - } - } - - i += 1 - } - - i = start - - while true { - if iterations > 1024 { - // infinite loop? or super large thread - print("breaking from large reply_map... big thread??") - break - } - - let ev = events[i] - - let ref_ids = ev.referenced_ids - if ref_ids.count == 0 { - break - } - - let ref_id = ref_ids[ref_ids.count-1] - let pubkey = ref_id.ref_id - is_reply[pubkey] = () - - if let mi = event_map[pubkey] { - i = mi - } else { - break - } - - iterations += 1 - } - - return is_reply -} - -func determine_highlight(reply_map: [String: ()], current: NostrEvent, active: NostrEvent) -> Highlight -{ - if current.id == active.id { - return .main - } else if reply_map[current.id] != nil { - return .reply - } else { - return .none - } -} - -func calculated_collapsed_events(privkey: String?, collapsed: Bool, active: NostrEvent?, events: [NostrEvent]) -> [CollapsedEvent] { - var count: Int = 0 - - guard let active = active else { - return [] - } - - let reply_map = make_reply_map(active: active, events: events, privkey: privkey) - - if !collapsed { - return events.reduce(into: []) { acc, ev in - let highlight = determine_highlight(reply_map: reply_map, current: ev, active: active) - return acc.append(.event(ev, highlight)) - } - } - - let nevents = events.count - var start: Int = 0 - var i: Int = 0 - - return events.reduce(into: []) { (acc, ev) in - let highlight = determine_highlight(reply_map: reply_map, current: ev, active: active) - - switch highlight { - case .none: - if i == 0 { - start = 1 - } - count += 1 - case .main: fallthrough - case .custom: fallthrough - case .reply: - if count != 0 { - let c = CollapsedEvents(count: count, start: start, end: i) - acc.append(.collapsed(c)) - start = i - count = 0 - } - acc.append(.event(ev, highlight)) - } - - if i == nevents-1 { - if count != 0 { - let c = CollapsedEvents(count: count, start: i-count, end: i) - acc.append(.collapsed(c)) - count = 0 - } - } - - i += 1 - } -} - - - -func any_collapsed(_ evs: [CollapsedEvent]) -> Bool { - for ev in evs { - switch ev { - case .collapsed: - return true - case .event: - continue - } + EventDetailView() } - return false } diff --git a/damus/Views/MainTabView.swift b/damus/Views/MainTabView.swift @@ -7,7 +7,7 @@ import SwiftUI -enum Timeline: String, CustomStringConvertible { +enum Timeline: String, CustomStringConvertible, Hashable { case home case notifications case search diff --git a/damus/Views/RelayFilterView.swift b/damus/Views/RelayFilterView.swift @@ -0,0 +1,59 @@ +// +// RelayFilterView.swift +// damus +// +// Created by Ben Weeks on 1/8/23. +// + +import SwiftUI + +struct RelayFilterView: View { + let state: DamusState + let timeline: Timeline + //@State var relays: [RelayDescriptor] + //@EnvironmentObject var user_settings: UserSettingsStore + //@State var relays: [RelayDescriptor] + + init(state: DamusState, timeline: Timeline) { + self.state = state + self.timeline = timeline + + //_relays = State(initialValue: state.pool.descriptors) + } + + var relays: [RelayDescriptor] { + return state.pool.descriptors + } + + func toggle_binding(relay_id: String) -> Binding<Bool> { + return Binding(get: { + !state.relay_filters.is_filtered(timeline: timeline, relay_id: relay_id) + }, set: { on in + if !on { + state.relay_filters.insert(timeline: timeline, relay_id: relay_id) + } else { + state.relay_filters.remove(timeline: timeline, relay_id: relay_id) + } + }) + } + + var body: some View { + Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:") + .padding() + .padding(.top, 20) + .padding(.bottom, 0) + + List(Array(relays), id: \.url) { relay in + //RelayView(state: state, relay: relay.url.absoluteString) + let relay_id = relay.url.absoluteString + Toggle(relay_id, isOn: toggle_binding(relay_id: relay_id)) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + } +} + +struct RelayFilterView_Previews: PreviewProvider { + static var previews: some View { + RelayFilterView(state: test_damus_state(), timeline: .search) + } +} diff --git a/damus/Views/ThreadView.swift b/damus/Views/ThreadView.swift @@ -1,99 +0,0 @@ -// -// ThreadView.swift -// damus -// -// Created by William Casarin on 2022-04-19. -// - -import SwiftUI - - -struct ThreadView: View { - @StateObject var thread: ThreadModel - let damus: DamusState - @State var is_chatroom: Bool - @State var metadata: ChatroomMetadata? = nil - @State var seen_first: Bool = false - - @Environment(\.dismiss) var dismiss - - var body: some View { - Group { - if is_chatroom { - ChatroomView(damus: damus) - .navigationBarTitle(metadata?.name ?? NSLocalizedString("Chat", comment: "Navigation bar title for Chatroom view.")) - .environmentObject(thread) - } else { - EventDetailView(damus: damus, thread: thread) - .navigationBarTitle(metadata?.name ?? NSLocalizedString("Thread", comment: "Navigation bar title for threaded event detail view.")) - .environmentObject(thread) - } - - /* - NavigationLink(destination: edv, isActive: $is_chatroom) { - EmptyView() - } - */ - } - .onReceive(handle_notify(.switched_timeline)) { n in - dismiss() - } - .onReceive(handle_notify(.toggle_thread_view)) { _ in - is_chatroom = !is_chatroom - //print("is_chatroom: \(is_chatroom)") - } - .onReceive(handle_notify(.chatroom_meta)) { n in - let meta = n.object as! ChatroomMetadata - self.metadata = meta - } - .onChange(of: thread.events) { val in - if seen_first { - return - } - if let ev = thread.events.first { - guard ev.is_root_event() else { - return - } - seen_first = true - is_chatroom = should_show_chatroom(ev) - } - } - .onAppear() { - thread.subscribe() - } - .onDisappear() { - thread.unsubscribe() - } - } -} - -/* -struct ThreadView_Previews: PreviewProvider { - static var previews: some View { - ThreadView() - } -} -*/ - -func should_show_chatroom(_ ev: NostrEvent) -> Bool { - if ev.known_kind == .chat || ev.known_kind == .channel_create { - return true - } - - return has_hashtag(ev.tags, hashtag: "chat") -} - -func tag_is_hashtag(_ tag: [String]) -> Bool { - // "hashtag" is deprecated, will remove in the future - return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t") -} - -func has_hashtag(_ tags: [[String]], hashtag: String) -> Bool { - for tag in tags { - if tag_is_hashtag(tag) && tag[1] == hashtag { - return true - } - } - - return false -} diff --git a/damus/damusApp.swift b/damus/damusApp.swift @@ -7,7 +7,6 @@ import SwiftUI - @main struct damusApp: App { var body: some Scene { diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift @@ -88,6 +88,24 @@ class damusTests: XCTestCase { XCTAssertEqual(parsed, expected) } + func testSaveRelayFilters() { + var filters = Set<RelayFilter>() + + let filter1 = RelayFilter(timeline: .search, relay_id: "wss://abc.com") + let filter2 = RelayFilter(timeline: .home, relay_id: "wss://abc.com") + filters.insert(filter1) + filters.insert(filter2) + + let pubkey = "test_pubkey" + save_relay_filters(pubkey, filters: filters) + let loaded_filters = load_relay_filters(pubkey) + + XCTAssertEqual(loaded_filters.count, 2) + XCTAssertTrue(loaded_filters.contains(filter1)) + XCTAssertTrue(loaded_filters.contains(filter2)) + XCTAssertEqual(filters, loaded_filters) + } + func testParseUrl() { let parsed = parse_mentions(content: "a https://jb55.com b", tags: [])