diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 20881d19..fca48c16 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -430,6 +430,7 @@ D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; }; + D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; }; @@ -1115,6 +1116,7 @@ D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = ""; }; + D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = ""; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = ""; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = ""; }; E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = ""; }; @@ -1717,6 +1719,7 @@ 50088DA029E8271A008A1FDF /* WebSocket.swift */, 4C28A4112A6D03D200C1A7A5 /* ReferencedId.swift */, 4C2B7BF12A71B6540049DEE7 /* Id.swift */, + D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */, ); path = Nostr; sourceTree = ""; @@ -2732,6 +2735,7 @@ 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */, F71694F82A6983AF001F4053 /* GrayGradient.swift in Sources */, 4C1D4FB12A7958E60024F453 /* VersionInfo.swift in Sources */, + D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */, 4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */, 5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */, F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */, diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift index afb2cb94..7fea5796 100644 --- a/damus/Models/Contacts.swift +++ b/damus/Models/Contacts.swift @@ -184,8 +184,13 @@ func decode_json_relays(_ content: String) -> [String: RelayInfo]? { return decode_json(content) } -func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: String) -> NostrEvent?{ +func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? { + return decode_json(content) +} + +func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{ var relays = ensure_relay_info(relays: current_relays, content: ev.content) + relays.removeValue(forKey: relay) guard let content = encode_json(relays) else { @@ -195,9 +200,10 @@ func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: Fu return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) } -func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? { +func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? { var relays = ensure_relay_info(relays: current_relays, content: ev.content) + guard relays.index(forKey: relay) == nil else { return nil } @@ -211,11 +217,8 @@ func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescr return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) } -func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: RelayInfo] { - guard let relay_info = decode_json_relays(content) else { - return make_contact_relays(relays) - } - return relay_info +func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [RelayURL: RelayInfo] { + return decode_json_relays(content) ?? make_contact_relays(relays) } func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool { @@ -245,8 +248,8 @@ func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEven return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) } -func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] { +func make_contact_relays(_ relays: [RelayDescriptor]) -> [RelayURL: RelayInfo] { return relays.reduce(into: [:]) { acc, relay in - acc[relay.url.url.absoluteString] = relay.info + acc[relay.url] = relay.info } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index e943a6c7..4ee4d752 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -849,7 +849,7 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { d[r] = .rw } - guard let decoded = decode_json_relays(ev.content) else { + guard let decoded: [String: RelayInfo] = decode_json_relays(ev.content) else { return } diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift index 970a6c99..5822cfc8 100644 --- a/damus/Nostr/RelayConnection.swift +++ b/damus/Nostr/RelayConnection.swift @@ -13,37 +13,6 @@ enum NostrConnectionEvent { case nostr_event(NostrResponse) } -public struct RelayURL: Hashable { - private(set) var url: URL - - var id: String { - return url.absoluteString - } - - init?(_ str: String) { - guard let last = str.last else { return nil } - - var urlstr = str - if last == "/" { - urlstr = String(str.dropLast(1)) - } - - guard let url = URL(string: urlstr) else { - return nil - } - - guard let scheme = url.scheme else { - return nil - } - - guard scheme == "ws" || scheme == "wss" else { - return nil - } - - self.url = url - } -} - final class RelayConnection: ObservableObject { @Published private(set) var isConnected = false @Published private(set) var isConnecting = false diff --git a/damus/Nostr/RelayURL.swift b/damus/Nostr/RelayURL.swift new file mode 100644 index 00000000..9ad75e06 --- /dev/null +++ b/damus/Nostr/RelayURL.swift @@ -0,0 +1,87 @@ +// +// RelayURL.swift +// damus +// +// Created by Daniel D’Aquino on 2023-09-29. +// + +import Foundation + +public struct RelayURL: Hashable, Equatable, Codable, CodingKeyRepresentable { + private(set) var url: URL + + var id: String { + return url.absoluteString + } + + init?(_ str: String) { + guard let last = str.last else { return nil } + + var urlstr = str + if last == "/" { + urlstr = String(str.dropLast(1)) + } + + guard let url = URL(string: urlstr) else { + return nil + } + + guard let scheme = url.scheme else { + return nil + } + + guard scheme == "ws" || scheme == "wss" else { + return nil + } + + self.url = url + } + + // MARK: - Codable + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let urlString = try container.decode(String.self) + guard let instance = RelayURL(urlString) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid URL string.") + } + self = instance + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(url.absoluteString) + } + + // MARK: - CodingKeyRepresentable + // CodingKeyRepresentable conformance is necessary to ensure that + // a dictionary with type "[RelayURL: T] where T: Codable" can be encoded into a keyed container + // e.g. `{: , : }` instead of `[, , , ]`, which is Swift's default for non-string-keyed dictionaries + + public var codingKey: CodingKey { + return StringKey(stringValue: self.url.absoluteString) + } + + public init?(codingKey: T) where T : CodingKey { + self.init(codingKey.stringValue) + } + + // MARK: - Equatable + public static func == (lhs: RelayURL, rhs: RelayURL) -> Bool { + return lhs.url == rhs.url + } + + // MARK: - Hashable + public func hash(into hasher: inout Hasher) { + hasher.combine(self.url) + } + +} + +private struct StringKey: CodingKey { + var stringValue: String + init(stringValue: String) { + self.stringValue = stringValue + } + var intValue: Int? { return nil } + init?(intValue: Int) { return nil } +} diff --git a/damus/Views/AddRelayView.swift b/damus/Views/AddRelayView.swift index 046cf7aa..280fcfee 100644 --- a/damus/Views/AddRelayView.swift +++ b/damus/Views/AddRelayView.swift @@ -82,9 +82,11 @@ struct AddRelayView: View { new_relay = "wss://" + new_relay } + /* if new_relay.hasSuffix("/") { new_relay.removeLast(); } + */ guard let url = RelayURL(new_relay), let ev = state.contacts.event, @@ -109,7 +111,7 @@ struct AddRelayView: View { state.pool.connect(to: [new_relay]) - guard let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: new_relay, info: info) else { + guard let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: url, info: info) else { return } diff --git a/damus/Views/Relays/RecommendedRelayView.swift b/damus/Views/Relays/RecommendedRelayView.swift index b5265ce2..ba368c41 100644 --- a/damus/Views/Relays/RecommendedRelayView.swift +++ b/damus/Views/Relays/RecommendedRelayView.swift @@ -118,7 +118,8 @@ struct RecommendedRelayView: View { guard let ev_before_add = damus.contacts.event else { return } - guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: damus.pool.our_descriptors, relay: relay, info: .rw) else { + guard let relay_url = RelayURL(relay), + let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: damus.pool.our_descriptors, relay: relay_url, info: .rw) else { return } process_contact_event(state: damus, ev: ev_after_add) diff --git a/damus/Views/Relays/RelayDetailView.swift b/damus/Views/Relays/RelayDetailView.swift index 0b3023f1..76ad0b9d 100644 --- a/damus/Views/Relays/RelayDetailView.swift +++ b/damus/Views/Relays/RelayDetailView.swift @@ -48,7 +48,8 @@ struct RelayDetailView: View { } let descriptors = state.pool.our_descriptors - guard let new_ev = remove_relay( ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else { + guard let relay_url = RelayURL(relay), + let new_ev = remove_relay( ev: ev, current_relays: descriptors, keypair: keypair, relay: relay_url) else { return } @@ -71,7 +72,8 @@ struct RelayDetailView: View { guard let ev_before_add = state.contacts.event else { return } - guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else { + guard let relay_url = RelayURL(relay), + let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay_url, info: .rw) else { return } process_contact_event(state: state, ev: ev_after_add) diff --git a/damus/Views/Relays/RelayView.swift b/damus/Views/Relays/RelayView.swift index 4a097bc3..6116bb53 100644 --- a/damus/Views/Relays/RelayView.swift +++ b/damus/Views/Relays/RelayView.swift @@ -95,7 +95,8 @@ struct RelayView: View { let descriptors = state.pool.our_descriptors guard let keypair = state.keypair.to_full(), - let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else { + let relay_url = RelayURL(relay), + let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay_url) else { return }