damus

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

commit 28790ccfabe1f5c82a60afaa945423312166fa01
parent 37b5309dd4c6ea07d5219b0b5ba201e8fd73e952
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 11 Apr 2022 10:34:35 -0700

reorganize

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

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 53+++++++++++++++++++++++++++++++++++++++++++++++++++--
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 9+++++++++
Mdamus/ContentView.swift | 25-------------------------
Ddamus/Nostr.swift | 17-----------------
Adamus/Nostr/Nostr.swift | 33+++++++++++++++++++++++++++++++++
Adamus/Nostr/NostrEvent.swift | 43+++++++++++++++++++++++++++++++++++++++++++
Adamus/Nostr/NostrFilter.swift | 40++++++++++++++++++++++++++++++++++++++++
Adamus/Nostr/NostrResponse.swift | 39+++++++++++++++++++++++++++++++++++++++
Adamus/Nostr/Relay.swift | 35+++++++++++++++++++++++++++++++++++
Adamus/Nostr/RelayConnection.swift | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Nostr/RelayPool.swift | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddamus/RelayConnection.swift | 288-------------------------------------------------------------------------------
Adamus/Views/PostButton.swift | 26++++++++++++++++++++++++++
13 files changed, 444 insertions(+), 332 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -9,6 +9,13 @@ /* Begin PBXBuildFile section */ 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; }; + 4C75EFAA28049C9F0006080F /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = 4C75EFA928049C9F0006080F /* CachedAsyncImage */; }; + 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; }; + 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAE28049D340006080F /* NostrFilter.swift */; }; + 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB028049D510006080F /* NostrResponse.swift */; }; + 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB228049D640006080F /* NostrEvent.swift */; }; + 4C75EFB528049D790006080F /* Relay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB428049D790006080F /* Relay.swift */; }; + 4C75EFB728049D990006080F /* RelayPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB628049D990006080F /* RelayPool.swift */; }; 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEE627F7A08100C66700 /* damusApp.swift */; }; 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEE827F7A08100C66700 /* ContentView.swift */; }; 4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CE6DEEA27F7A08200C66700 /* Assets.xcassets */; }; @@ -41,6 +48,12 @@ 4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; }; 4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; }; 4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; + 4C75EFAC28049CFB0006080F /* PostButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostButton.swift; sourceTree = "<group>"; }; + 4C75EFAE28049D340006080F /* NostrFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilter.swift; sourceTree = "<group>"; }; + 4C75EFB028049D510006080F /* NostrResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrResponse.swift; sourceTree = "<group>"; }; + 4C75EFB228049D640006080F /* NostrEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEvent.swift; sourceTree = "<group>"; }; + 4C75EFB428049D790006080F /* Relay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relay.swift; sourceTree = "<group>"; }; + 4C75EFB628049D990006080F /* RelayPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPool.swift; sourceTree = "<group>"; }; 4CE6DEE327F7A08100C66700 /* damus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = damus.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4CE6DEE627F7A08100C66700 /* damusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusApp.swift; sourceTree = "<group>"; }; 4CE6DEE827F7A08100C66700 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; @@ -60,6 +73,7 @@ buildActionMask = 2147483647; files = ( 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */, + 4C75EFAA28049C9F0006080F /* CachedAsyncImage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,10 +98,25 @@ isa = PBXGroup; children = ( 4C75EFA327FA577B0006080F /* PostView.swift */, + 4C75EFAC28049CFB0006080F /* PostButton.swift */, ); path = Views; sourceTree = "<group>"; }; + 4C75EFAB28049CC80006080F /* Nostr */ = { + isa = PBXGroup; + children = ( + 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */, + 4C75EFA527FF87A20006080F /* Nostr.swift */, + 4C75EFAE28049D340006080F /* NostrFilter.swift */, + 4C75EFB028049D510006080F /* NostrResponse.swift */, + 4C75EFB228049D640006080F /* NostrEvent.swift */, + 4C75EFB428049D790006080F /* Relay.swift */, + 4C75EFB628049D990006080F /* RelayPool.swift */, + ); + path = Nostr; + sourceTree = "<group>"; + }; 4CE6DEDA27F7A08100C66700 = { isa = PBXGroup; children = ( @@ -111,14 +140,13 @@ 4CE6DEE527F7A08100C66700 /* damus */ = { isa = PBXGroup; children = ( + 4C75EFAB28049CC80006080F /* Nostr */, 4C75EFA72804823E0006080F /* Info.plist */, 4C75EFA227FA576C0006080F /* Views */, 4CE6DEE627F7A08100C66700 /* damusApp.swift */, 4CE6DEE827F7A08100C66700 /* ContentView.swift */, 4CE6DEEA27F7A08200C66700 /* Assets.xcassets */, 4CE6DEEC27F7A08200C66700 /* Preview Content */, - 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */, - 4C75EFA527FF87A20006080F /* Nostr.swift */, ); path = damus; sourceTree = "<group>"; @@ -166,6 +194,7 @@ name = damus; packageProductDependencies = ( 4CE6DF1127F7A2B300C66700 /* Starscream */, + 4C75EFA928049C9F0006080F /* CachedAsyncImage */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -241,6 +270,7 @@ mainGroup = 4CE6DEDA27F7A08100C66700; packageReferences = ( 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */, + 4C75EFA828049C9F0006080F /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -284,11 +314,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C75EFB728049D990006080F /* RelayPool.swift in Sources */, 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */, + 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, + 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, + 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, + 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, + 4C75EFB528049D790006080F /* Relay.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -619,6 +655,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 4C75EFA828049C9F0006080F /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/lorenzofiamingo/swiftui-cached-async-image"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/daltoniam/Starscream"; @@ -630,6 +674,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 4C75EFA928049C9F0006080F /* CachedAsyncImage */ = { + isa = XCSwiftPackageProductDependency; + package = 4C75EFA828049C9F0006080F /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */; + productName = CachedAsyncImage; + }; 4CE6DF1127F7A2B300C66700 /* Starscream */ = { isa = XCSwiftPackageProductDependency; package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -8,6 +8,15 @@ "revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21", "version" : "4.0.4" } + }, + { + "identity" : "swiftui-cached-async-image", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image", + "state" : { + "revision" : "eeb1565d780d1b75d045e21b5ca2a1e3650b0fc2", + "version" : "2.1.0" + } } ], "version" : 2 diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -64,11 +64,6 @@ enum Sheets: Identifiable { } } -enum NostrKind: Int { - case metadata = 0 - case text = 1 -} - struct ContentView: View { @State var status: String = "Not connected" @State var sub_id: String? = nil @@ -228,22 +223,6 @@ struct ContentView_Previews: PreviewProvider { } } -func PostButton(action: @escaping () -> ()) -> some View { - return Button(action: action, label: { - Text("+") - .font(.system(.largeTitle)) - .frame(width: 57, height: 50) - .foregroundColor(Color.white) - .padding(.bottom, 7) - }) - .background(Color.blue) - .cornerRadius(38.5) - .padding() - .shadow(color: Color.black.opacity(0.3), - radius: 3, - x: 3, - y: 3) -} func get_metadata_since_time(_ metadata_event: NostrEvent?) -> Int64? { @@ -281,7 +260,3 @@ func get_profiles() */ -func add_rw_relay(_ pool: RelayPool, _ url: String) { - let url_ = URL(string: url)! - try! pool.add_relay(url_, info: RelayInfo.rw) -} diff --git a/damus/Nostr.swift b/damus/Nostr.swift @@ -1,17 +0,0 @@ -// -// Nostr.swift -// damus -// -// Created by William Casarin on 2022-04-07. -// - -import Foundation - - -struct Profile: Decodable { - let name: String? - let about: String? - let picture: String? -} - - diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift @@ -0,0 +1,33 @@ +// +// Nostr.swift +// damus +// +// Created by William Casarin on 2022-04-07. +// + +import Foundation + + +struct Profile: Decodable { + let name: String? + let about: String? + let picture: String? +} + +enum NostrKind: Int { + case metadata = 0 + case text = 1 +} + +enum NostrTag { + case other_event(OtherEvent) + case key_event(KeyEvent) +} + +struct NostrSubscription { + let sub_id: String + let filter: NostrFilter +} + + + diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -0,0 +1,43 @@ +// +// NostrEvent.swift +// damus +// +// Created by William Casarin on 2022-04-11. +// + +import Foundation + +struct OtherEvent { + let event_id: String + let relay_url: String +} + +struct KeyEvent { + let key: String + let relay_url: String +} + +struct NostrEvent: Decodable, Identifiable { + let id: String + let pubkey: String + let created_at: Int64 + let kind: Int + let tags: [[String]] + let content: String + let sig: String +} + +func decode_nostr_event(txt: String) -> NostrResponse? { + return decode_data(Data(txt.utf8)) +} + +func decode_data<T: Decodable>(_ data: Data) -> T? { + let decoder = JSONDecoder() + do { + return try decoder.decode(T.self, from: data) + } catch { + print("decode_data failed for \(T.self): \(error)") + } + + return nil +} diff --git a/damus/Nostr/NostrFilter.swift b/damus/Nostr/NostrFilter.swift @@ -0,0 +1,40 @@ +// +// NostrFilter.swift +// damus +// +// Created by William Casarin on 2022-04-11. +// + +import Foundation + +struct NostrFilter: Codable { + var ids: [String]? + var kinds: [Int]? + var referenced_ids: [String]? + var pubkeys: [String]? + var since: Int64? + var until: Int64? + var authors: [String]? + + private enum CodingKeys : String, CodingKey { + case ids + case kinds + case referenced_ids = "#e" + case pubkeys = "#p" + case since + case until + case authors + } + + public static var filter_text: NostrFilter { + NostrFilter(ids: nil, kinds: [1], referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil) + } + + public static var filter_profiles: NostrFilter { + return NostrFilter(ids: nil, kinds: [0], referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil) + } + + public static func filter_since(_ val: Int64) -> NostrFilter { + return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: val, until: nil, authors: nil) + } +} diff --git a/damus/Nostr/NostrResponse.swift b/damus/Nostr/NostrResponse.swift @@ -0,0 +1,39 @@ +// +// NostrResponse.swift +// damus +// +// Created by William Casarin on 2022-04-11. +// + +import Foundation + +enum NostrResponse: Decodable { + case event(String, NostrEvent) + case notice(String) + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + // Only use first item + let typ = try container.decode(String.self) + if typ == "EVENT" { + let sub_id = try container.decode(String.self) + var ev: NostrEvent + do { + ev = try container.decode(NostrEvent.self) + } catch { + print(error) + throw error + } + self = .event(sub_id, ev) + return + } else if typ == "NOTICE" { + let msg = try container.decode(String.self) + self = .notice(msg) + return + } + + throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT or NOTICE, got \(typ)")) + } +} + diff --git a/damus/Nostr/Relay.swift b/damus/Nostr/Relay.swift @@ -0,0 +1,35 @@ +// +// Relay.swift +// damus +// +// Created by William Casarin on 2022-04-11. +// + +import Foundation + +struct RelayInfo { + let read: Bool + let write: Bool + + static let rw = RelayInfo(read: true, write: true) +} + +struct Relay: Identifiable { + let url: URL + let info: RelayInfo + let connection: RelayConnection + + var id: String { + return get_relay_id(url) + } + +} + +enum RelayError: Error { + case RelayAlreadyExists + case RelayNotFound +} + +func get_relay_id(_ url: URL) -> String { + return url.absoluteString +} diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift @@ -0,0 +1,89 @@ +// +// NostrConnection.swift +// damus +// +// Created by William Casarin on 2022-04-02. +// + +import Foundation +import Starscream + +enum NostrConnectionEvent { + case ws_event(WebSocketEvent) + case nostr_event(NostrResponse) +} + +class RelayConnection: WebSocketDelegate { + var isConnected: Bool = false + var socket: WebSocket + var handleEvent: (NostrConnectionEvent) -> () + + init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) { + var req = URLRequest(url: url) + req.timeoutInterval = 5 + self.socket = WebSocket(request: req) + self.handleEvent = handleEvent + + socket.delegate = self + } + + func connect(){ + socket.connect() + } + + func disconnect() { + socket.disconnect() + } + + func send(_ filters: [NostrFilter], sub_id: String) { + guard let req = make_nostr_req(filters, sub_id: sub_id) else { + print("failed to encode nostr req: \(filters)") + return + } + socket.write(string: req) + } + + func didReceive(event: WebSocketEvent, client: WebSocket) { + switch event { + case .connected: + self.isConnected = true + + case .disconnected: fallthrough + case .cancelled: fallthrough + case .error: + self.isConnected = false + + case .text(let txt): + if let ev = decode_nostr_event(txt: txt) { + handleEvent(.nostr_event(ev)) + return + } + + print("decode failed for \(txt)") + // TODO: trigger event error + + default: + break + } + + handleEvent(.ws_event(event)) + } + +} + +func make_nostr_req(_ filters: [NostrFilter], sub_id: String) -> String? { + let encoder = JSONEncoder() + var req = "[\"REQ\",\"\(sub_id)\"" + for filter in filters { + req += "," + guard let filter_json = try? encoder.encode(filter) else { + return nil + } + let filter_json_str = String(decoding: filter_json, as: UTF8.self) + req += filter_json_str + } + req += "]" + print("req: \(req)") + return req +} + diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -0,0 +1,79 @@ +// +// RelayPool.swift +// damus +// +// Created by William Casarin on 2022-04-11. +// + +import Foundation + +class RelayPool { + var relays: [Relay] = [] + let custom_handle_event: (String, NostrConnectionEvent) -> () + + init(handle_event: @escaping (String, NostrConnectionEvent) -> ()) { + self.custom_handle_event = handle_event + } + + func add_relay(_ url: URL, info: RelayInfo) throws { + let relay_id = get_relay_id(url) + if get_relay(relay_id) != nil { + throw RelayError.RelayAlreadyExists + } + 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) + self.relays.append(relay) + } + + func connect(to: [String]? = nil) { + let relays = to.map{ get_relays($0) } ?? self.relays + for relay in relays { + relay.connection.connect() + } + } + + func send(filters: [NostrFilter], sub_id: String, to: [String]? = nil) { + let relays = to.map{ get_relays($0) } ?? self.relays + + for relay in relays { + if relay.connection.isConnected { + relay.connection.send(filters, sub_id: sub_id) + } + } + } + + func get_relays(_ ids: [String]) -> [Relay] { + var relays: [Relay] = [] + + for id in ids { + if let relay = get_relay(id) { + relays.append(relay) + } + } + + return relays + } + + func get_relay(_ id: String) -> Relay? { + for relay in relays { + if relay.id == id { + return relay + } + } + + return nil + } + + func handle_event(relay_id: String, event: NostrConnectionEvent) { + // handle reconnect logic, etc? + custom_handle_event(relay_id, event) + } +} + +func add_rw_relay(_ pool: RelayPool, _ url: String) { + let url_ = URL(string: url)! + try! pool.add_relay(url_, info: RelayInfo.rw) +} + diff --git a/damus/RelayConnection.swift b/damus/RelayConnection.swift @@ -1,288 +0,0 @@ -// -// NostrConnection.swift -// damus -// -// Created by William Casarin on 2022-04-02. -// - -import Foundation -import Starscream - -struct OtherEvent { - let event_id: String - let relay_url: String -} - -struct KeyEvent { - let key: String - let relay_url: String -} - -enum NostrConnectionEvent { - case ws_event(WebSocketEvent) - case nostr_event(NostrResponse) -} - -enum NostrTag { - case other_event(OtherEvent) - case key_event(KeyEvent) -} - -struct NostrSubscription { - let sub_id: String - let filter: NostrFilter -} - -struct NostrFilter: Codable { - var ids: [String]? - var kinds: [Int]? - var referenced_ids: [String]? - var pubkeys: [String]? - var since: Int64? - var until: Int64? - var authors: [String]? - - private enum CodingKeys : String, CodingKey { - case ids - case kinds - case referenced_ids = "#e" - case pubkeys = "#p" - case since - case until - case authors - } - - public static var filter_text: NostrFilter { - NostrFilter(ids: nil, kinds: [1], referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil) - } - - public static var filter_profiles: NostrFilter { - return NostrFilter(ids: nil, kinds: [0], referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil) - } - - public static func filter_since(_ val: Int64) -> NostrFilter { - return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: val, until: nil, authors: nil) - } -} - -enum NostrResponse: Decodable { - case event(String, NostrEvent) - case notice(String) - - init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - - // Only use first item - let typ = try container.decode(String.self) - if typ == "EVENT" { - let sub_id = try container.decode(String.self) - var ev: NostrEvent - do { - ev = try container.decode(NostrEvent.self) - } catch { - print(error) - throw error - } - self = .event(sub_id, ev) - return - } else if typ == "NOTICE" { - let msg = try container.decode(String.self) - self = .notice(msg) - return - } - - throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT or NOTICE, got \(typ)")) - } -} - -struct NostrEvent: Decodable, Identifiable { - let id: String - let pubkey: String - let created_at: Int64 - let kind: Int - let tags: [[String]] - let content: String - let sig: String -} - -struct RelayInfo { - let read: Bool - let write: Bool - - static let rw = RelayInfo(read: true, write: true) -} - -struct Relay: Identifiable { - let url: URL - let info: RelayInfo - let connection: RelayConnection - - var id: String { - return get_relay_id(url) - } - -} - -func get_relay_id(_ url: URL) -> String { - return url.absoluteString -} - -enum RelayError: Error { - case RelayAlreadyExists - case RelayNotFound -} - -class RelayPool { - var relays: [Relay] = [] - let custom_handle_event: (String, NostrConnectionEvent) -> () - - init(handle_event: @escaping (String, NostrConnectionEvent) -> ()) { - self.custom_handle_event = handle_event - } - - func add_relay(_ url: URL, info: RelayInfo) throws { - let relay_id = get_relay_id(url) - if get_relay(relay_id) != nil { - throw RelayError.RelayAlreadyExists - } - 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) - self.relays.append(relay) - } - - func connect(to: [String]? = nil) { - let relays = to.map{ get_relays($0) } ?? self.relays - for relay in relays { - relay.connection.connect() - } - } - - func send(filters: [NostrFilter], sub_id: String, to: [String]? = nil) { - let relays = to.map{ get_relays($0) } ?? self.relays - - for relay in relays { - if relay.connection.isConnected { - relay.connection.send(filters, sub_id: sub_id) - } - } - } - - func get_relays(_ ids: [String]) -> [Relay] { - var relays: [Relay] = [] - - for id in ids { - if let relay = get_relay(id) { - relays.append(relay) - } - } - - return relays - } - - func get_relay(_ id: String) -> Relay? { - for relay in relays { - if relay.id == id { - return relay - } - } - - return nil - } - - func handle_event(relay_id: String, event: NostrConnectionEvent) { - // handle reconnect logic, etc? - custom_handle_event(relay_id, event) - } -} - -class RelayConnection: WebSocketDelegate { - var isConnected: Bool = false - var socket: WebSocket - var handleEvent: (NostrConnectionEvent) -> () - - init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) { - var req = URLRequest(url: url) - req.timeoutInterval = 5 - self.socket = WebSocket(request: req) - self.handleEvent = handleEvent - - socket.delegate = self - } - - func connect(){ - socket.connect() - } - - func disconnect() { - socket.disconnect() - } - - func send(_ filters: [NostrFilter], sub_id: String) { - guard let req = make_nostr_req(filters, sub_id: sub_id) else { - print("failed to encode nostr req: \(filters)") - return - } - socket.write(string: req) - } - - func didReceive(event: WebSocketEvent, client: WebSocket) { - switch event { - case .connected: - self.isConnected = true - - case .disconnected: fallthrough - case .cancelled: fallthrough - case .error: - self.isConnected = false - - case .text(let txt): - if let ev = decode_nostr_event(txt: txt) { - handleEvent(.nostr_event(ev)) - return - } - - print("decode failed for \(txt)") - // TODO: trigger event error - - default: - break - } - - handleEvent(.ws_event(event)) - } - -} - -func decode_nostr_event(txt: String) -> NostrResponse? { - return decode_data(Data(txt.utf8)) -} - -func decode_data<T: Decodable>(_ data: Data) -> T? { - let decoder = JSONDecoder() - do { - return try decoder.decode(T.self, from: data) - } catch { - print("decode_data failed for \(T.self): \(error)") - } - - return nil -} - -func make_nostr_req(_ filters: [NostrFilter], sub_id: String) -> String? { - let encoder = JSONEncoder() - var req = "[\"REQ\",\"\(sub_id)\"" - for filter in filters { - req += "," - guard let filter_json = try? encoder.encode(filter) else { - return nil - } - let filter_json_str = String(decoding: filter_json, as: UTF8.self) - req += filter_json_str - } - req += "]" - print("req: \(req)") - return req -} - diff --git a/damus/Views/PostButton.swift b/damus/Views/PostButton.swift @@ -0,0 +1,26 @@ +// +// PostButton.swift +// damus +// +// Created by William Casarin on 2022-04-11. +// + +import Foundation +import SwiftUI + +func PostButton(action: @escaping () -> ()) -> some View { + return Button(action: action, label: { + Text("+") + .font(.system(.largeTitle)) + .frame(width: 57, height: 50) + .foregroundColor(Color.white) + .padding(.bottom, 7) + }) + .background(Color.blue) + .cornerRadius(38.5) + .padding() + .shadow(color: Color.black.opacity(0.3), + radius: 3, + x: 3, + y: 3) +}