damus

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

commit b6421bb5e4c1ee24ea735a5b7d0ed78a45336b2a
parent 2676dea1409957d3312b52d48dbcae3d876b2c09
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 16 Apr 2022 15:07:26 -0700

threads working

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus/ContentView.swift | 37++++++++++++++++++++++++++-----------
Mdamus/Nostr/NostrEvent.swift | 33+++++++++++++++++++++++++++++++++
Mdamus/Nostr/NostrRequest.swift | 1+
Mdamus/Nostr/Relay.swift | 8++++++--
Mdamus/Nostr/RelayConnection.swift | 9++++++++-
Mdamus/Nostr/RelayPool.swift | 49++++++++++++++++++++++++++++++++++++++++++++-----
Mdamus/Views/EventDetailView.swift | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mdamus/Views/EventView.swift | 3++-
Mdamus/Views/ProfileName.swift | 3---
Mdamus/Views/ProfilePicView.swift | 9+++++++--
10 files changed, 213 insertions(+), 31 deletions(-)

diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -43,15 +43,18 @@ struct ContentView: View { @State var timeline: Timeline = .friends @State var pool: RelayPool? = nil + let sub_id = UUID().description let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" - var MainContent: some View { + func MainContent(pool: RelayPool) -> some View { ScrollView { - ForEach(events, id: \.id) { ev in + ForEach(self.events, id: \.id) { (ev: NostrEvent) in if ev.is_local && timeline == .debug || (timeline == .global && !ev.is_local) || (timeline == .friends && is_friend(ev.pubkey)) { let profile: Profile? = profiles[ev.pubkey]?.profile - NavigationLink(destination: EventDetailView(event: ev, profile: profile).navigationBarTitle("Note")) { - EventView(event: ev, profile: profile) + let evdet = EventDetailView(event: ev, pool: pool, profiles: profiles) + .navigationBarTitle("Note") + NavigationLink(destination: evdet) { + EventView(event: ev, profile: profile, highlighted: false) } .buttonStyle(PlainButtonStyle()) } @@ -93,8 +96,10 @@ struct ContentView: View { VStack { TopBar(selected: self.timeline) ZStack { - MainContent - .padding() + if let pool = self.pool { + MainContent(pool: pool) + .padding() + } PostButtonContainer } } @@ -137,13 +142,15 @@ struct ContentView: View { } func connect() { - let pool = RelayPool(handle_event: handle_event) + let pool = RelayPool() - add_relay(pool, "nostr-relay.wlvs.space") + add_relay(pool, "wss://nostr.onsats.org") add_relay(pool, "nostr.bitcoiner.social") add_relay(pool, "nostr-relay.freeberty.net") add_relay(pool, "nostr-relay.untethr.me") + pool.register_handler(sub_id: sub_id, handler: handle_event) + self.pool = pool pool.connect() } @@ -194,8 +201,6 @@ struct ContentView: View { let filters = [since_filter, profile_filter, contacts_filter] print("connected to \(relay_id), refreshing from \(since)") - let sub_id = UUID().description - print("subscribing to \(sub_id)") self.pool?.send(.subscribe(.init(filters: filters, sub_id: sub_id))) } @@ -211,6 +216,11 @@ struct ContentView: View { switch ev { case .connected: send_filters(relay_id: relay_id) + case .error(let merr): + let desc = merr.debugDescription + if desc.contains("Software caused connection abort") { + self.pool?.connect(to: [relay_id]) + } case .disconnected: self.pool?.connect(to: [relay_id]) case .cancelled: @@ -227,7 +237,12 @@ struct ContentView: View { case .nostr_event(let ev): switch ev { - case .event(_, let ev): + case .event(let sub_id, let ev): + if sub_id != self.sub_id { + // TODO: other views like threads might have their own sub ids, so ignore those events... or should we? + return + } + if self.loading { self.loading = false } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -19,6 +19,19 @@ struct KeyEvent { let relay_url: String } +struct ReferencedId { + let ref_id: String + let relay_id: String? +} + +struct EventId: Identifiable, CustomStringConvertible { + let id: String + + var description: String { + id + } +} + class NostrEvent: Codable, Identifiable { var id: String var sig: String @@ -39,6 +52,26 @@ class NostrEvent: Codable, Identifiable { case id, sig, tags, pubkey, created_at, kind, content } + private func get_referenced_ids(key: String) -> [ReferencedId] { + return tags.reduce(into: []) { (acc, tag) in + if tag.count >= 2 && tag[0] == key { + var relay_id: String? = nil + if tag.count >= 3 { + relay_id = tag[2] + } + acc.append(ReferencedId(ref_id: tag[1], relay_id: relay_id)) + } + } + } + + public var referenced_ids: [ReferencedId] { + return get_referenced_ids(key: "e") + } + + public var referenced_pubkeys: [ReferencedId] { + return get_referenced_ids(key: "p") + } + /// Make a local event public static func local(content: String, pubkey: String) -> NostrEvent { let ev = NostrEvent(content: content, pubkey: pubkey) diff --git a/damus/Nostr/NostrRequest.swift b/damus/Nostr/NostrRequest.swift @@ -14,5 +14,6 @@ struct NostrSubscribe { enum NostrRequest { case subscribe(NostrSubscribe) + case unsubscribe(String) case event(NostrEvent) } diff --git a/damus/Nostr/Relay.swift b/damus/Nostr/Relay.swift @@ -14,13 +14,17 @@ struct RelayInfo { static let rw = RelayInfo(read: true, write: true) } -struct Relay: Identifiable { +struct RelayDescriptor { let url: URL let info: RelayInfo +} + +struct Relay: Identifiable { + let descriptor: RelayDescriptor let connection: RelayConnection var id: String { - return get_relay_id(url) + return get_relay_id(descriptor.url) } } diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift @@ -40,6 +40,8 @@ class RelayConnection: WebSocketDelegate { print("failed to encode nostr req: \(req)") return } + print("req: \(req)") + socket.write(string: req) } @@ -75,6 +77,8 @@ func make_nostr_req(_ req: NostrRequest) -> String? { switch req { case .subscribe(let sub): return make_nostr_subscription_req(sub.filters, sub_id: sub.sub_id) + case .unsubscribe(let sub_id): + return make_nostr_unsubscribe_req(sub_id) case .event(let ev): return make_nostr_push_event(ev: ev) } @@ -89,6 +93,10 @@ func make_nostr_push_event(ev: NostrEvent) -> String? { return encoded } +func make_nostr_unsubscribe_req(_ sub_id: String) -> String? { + return "[\"CLOSE\",\"\(sub_id)\"]" +} + func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> String? { let encoder = JSONEncoder() var req = "[\"REQ\",\"\(sub_id)\"" @@ -101,7 +109,6 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St req += filter_json_str } req += "]" - print("req: \(req)") return req } diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -7,12 +7,41 @@ import Foundation +struct SubscriptionId: Identifiable, CustomStringConvertible { + let id: String + + var description: String { + id + } +} + +struct RelayId: Identifiable, CustomStringConvertible { + let id: String + + var description: String { + id + } +} + +struct RelayHandler { + let sub_id: String + let callback: (String, NostrConnectionEvent) -> () +} + class RelayPool { var relays: [Relay] = [] - let custom_handle_event: (String, NostrConnectionEvent) -> () + var handlers: [RelayHandler] = [] - init(handle_event: @escaping (String, NostrConnectionEvent) -> ()) { - self.custom_handle_event = handle_event + var descriptors: [RelayDescriptor] { + relays.map { $0.descriptor } + } + + func remove_handler(sub_id: String) { + handlers = handlers.filter { $0.sub_id != sub_id } + } + + func register_handler(sub_id: String, handler: @escaping (String, NostrConnectionEvent) -> ()) { + self.handlers.append(RelayHandler(sub_id: sub_id, callback: handler)) } func add_relay(_ url: URL, info: RelayInfo) throws { @@ -23,7 +52,8 @@ class RelayPool { let conn = RelayConnection(url: url) { event in self.handle_event(relay_id: relay_id, event: event) } - let relay = Relay(url: url, info: info, connection: conn) + let descriptor = RelayDescriptor(url: url, info: info) + let relay = Relay(descriptor: descriptor, connection: conn) self.relays.append(relay) } @@ -34,6 +64,13 @@ class RelayPool { } } + func disconnect(to: [String]? = nil) { + let relays = to.map{ get_relays($0) } ?? self.relays + for relay in relays { + relay.connection.disconnect() + } + } + func send(_ req: NostrRequest, to: [String]? = nil) { let relays = to.map{ get_relays($0) } ?? self.relays @@ -68,7 +105,9 @@ class RelayPool { func handle_event(relay_id: String, event: NostrConnectionEvent) { // handle reconnect logic, etc? - custom_handle_event(relay_id, event) + for handler in handlers { + handler.callback(relay_id, event) + } } } diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -9,12 +9,62 @@ import SwiftUI struct EventDetailView: View { let event: NostrEvent - let profile: Profile? - var body: some View { + let sub_id = UUID().description + + @State var events: [NostrEvent] = [] + @State var has_event: [String: ()] = [:] + + let pool: RelayPool + let profiles: [String: TimestampedProfile] + + func unsubscribe_to_thread() { + print("unsubscribing from thread \(event.id) with sub_id \(sub_id)") + self.pool.send(.unsubscribe(sub_id)) + self.pool.remove_handler(sub_id: sub_id) + } + + func subscribe_to_thread() { + var ref_events = NostrFilter.filter_text + var events = NostrFilter.filter_text + + // TODO: add referenced relays + ref_events.referenced_ids = event.referenced_ids.map { $0.ref_id } + ref_events.referenced_ids!.append(event.id) + + events.ids = ref_events.referenced_ids! + + print("subscribing to thread \(event.id) with sub_id \(sub_id)") + pool.register_handler(sub_id: sub_id, handler: handle_event) + pool.send(.subscribe(.init(filters: [ref_events, events], sub_id: sub_id))) + } + + + func handle_event(relay_id: String, ev: NostrConnectionEvent) { + switch ev { + case .ws_event: + break + case .nostr_event(let res): + switch res { + case .event(let sub_id, let ev): + if sub_id != self.sub_id || self.has_event[ev.id] != nil { + return + } + self.add_event(ev) + + case .notice(_): + // TODO: handle notices in threads? + break + } + } + } + + var NoteBody: some View { HStack { + let profile = profiles[event.pubkey]?.profile + VStack { - ProfilePicView(picture: profile?.picture, size: 64) + ProfilePicView(picture: profile?.picture, size: 64, highlighted: false) Spacer() } @@ -30,20 +80,50 @@ struct EventDetailView: View { Text(event.content) .frame(maxWidth: .infinity, alignment: .leading) + EventActionBar(event: event) + Divider() .padding([.bottom], 10) + } + } + } - EventActionBar(event: event) - - Spacer() + var body: some View { + ScrollView { + ForEach(events, id: \.id) { ev in + let evdet = EventDetailView(event: ev, pool: pool, profiles: profiles) + .navigationBarTitle("Note") + NavigationLink(destination: evdet) { + EventView(event: ev, profile: self.profiles[ev.pubkey]?.profile, highlighted: ev.id == event.id) + } + .buttonStyle(PlainButtonStyle()) + //EventView(event: ev, profile: self.profiles[ev.pubkey]?.profile, highlighted: ev.id == event.id) } } .padding() + .onDisappear() { + unsubscribe_to_thread() + } + .onAppear() { + self.add_event(event) + subscribe_to_thread() + } + + } + + func add_event(_ ev: NostrEvent) { + if self.has_event[ev.id] == nil { + self.has_event[ev.id] = () + self.events.append(ev) + self.events = self.events.sorted { $0.created_at < $1.created_at } + } } } +/* struct EventDetailView_Previews: PreviewProvider { static var previews: some View { EventDetailView(event: NostrEvent(content: "Hello", pubkey: "Guy"), profile: nil) } } + */ diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -12,11 +12,12 @@ import CachedAsyncImage struct EventView: View { let event: NostrEvent let profile: Profile? + let highlighted: Bool var body: some View { HStack { VStack { - ProfilePicView(picture: profile?.picture, size: 64) + ProfilePicView(picture: profile?.picture, size: 64, highlighted: highlighted) Spacer() } diff --git a/damus/Views/ProfileName.swift b/damus/Views/ProfileName.swift @@ -10,8 +10,5 @@ import SwiftUI func ProfileName(pubkey: String, profile: Profile?) -> some View { Text(String(profile?.name ?? String(pubkey.prefix(16)))) .bold() - .onTapGesture { - UIPasteboard.general.string = pubkey - } } diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift @@ -14,6 +14,7 @@ let CORNER_RADIUS: CGFloat = 32 struct ProfilePicView: View { let picture: String? let size: CGFloat + let highlighted: Bool var body: some View { if let pic = picture.flatMap({ URL(string: $0) }) { @@ -23,17 +24,21 @@ struct ProfilePicView: View { Color.purple.opacity(0.1) } .frame(width: PFP_SIZE, height: PFP_SIZE) - .cornerRadius(CORNER_RADIUS) + .clipShape(Circle()) + .overlay(Circle().stroke(highlighted ? Color.red : Color.black, lineWidth: highlighted ? 4 : 0)) + .padding(2) } else { Color.purple.opacity(0.1) .frame(width: PFP_SIZE, height: PFP_SIZE) .cornerRadius(CORNER_RADIUS) + .overlay(Circle().stroke(highlighted ? Color.red : Color.black, lineWidth: highlighted ? 4 : 0)) + .padding(2) } } } struct ProfilePicView_Previews: PreviewProvider { static var previews: some View { - ProfilePicView(picture: "http://cdn.jb55.com/img/red-me.jpg", size: 64) + ProfilePicView(picture: "http://cdn.jb55.com/img/red-me.jpg", size: 64, highlighted: false) } }