commit 10596ddb09690cf2ab5b7c5917dff6b38075fec9
parent 989684cd375b3782aceefa1225574f4da59ca334
Author: William Casarin <jb55@jb55.com>
Date: Wed, 8 Feb 2023 11:07:58 -0800
Relay Filters
wip
Diffstat:
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: [])