damus

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

commit c122035851be18b1329bab3f497a1794e97ac0ed
parent 0744156c0cfc1b94b2ac22b1493d77dd690e0a7e
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 30 Jun 2022 07:16:48 -0700

Implement NIP04: Encrypted Direct Messages

Closes #5

This adds encrypted direct message support to damus

Changelog-Added: Implement NIP04: Encrypted Direct Messages
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 52++++++++++++++++++++++++++++++++++++----------------
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 5++---
Mdamus/ContentView.swift | 7+++++--
Mdamus/Models/Contacts.swift | 4++--
Adamus/Models/DirectMessagesModel.swift | 15+++++++++++++++
Mdamus/Models/EventRef.swift | 4++--
Mdamus/Models/HomeModel.swift | 218++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mdamus/Models/ThreadModel.swift | 33+++++++++++++++++++++++----------
Mdamus/Nostr/NostrEvent.swift | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mdamus/Nostr/NostrKind.swift | 1+
Adamus/Util/InputDismissKeyboard.swift | 39+++++++++++++++++++++++++++++++++++++++
Mdamus/Util/Keys.swift | 4++--
Mdamus/Views/ChatView.swift | 40++++++++++++++++++++++------------------
Mdamus/Views/ChatroomView.swift | 8++++----
Adamus/Views/DMChatView.swift | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/DMView.swift | 39+++++++++++++++++++++++++++++++++++++++
Adamus/Views/DirectMessagesView.swift | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/EventDetailView.swift | 21++++++++++++++-------
Mdamus/Views/EventView.swift | 70++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mdamus/Views/MainTabView.swift | 4++++
Mdamus/Views/NoteContentView.swift | 17++++++++++-------
Mdamus/Views/ProfileView.swift | 36+++++++++++++++++++++++-------------
Mdamus/Views/ReplyQuoteView.swift | 7++++---
Mdamus/Views/TimelineView.swift | 2+-
24 files changed, 889 insertions(+), 225 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -13,6 +13,9 @@ 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.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 */; }; 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; }; 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; }; 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; }; @@ -57,6 +60,9 @@ 4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; }; 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C633351283D419F00B1C9C3 /* SignalModel.swift */; }; 4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C649843285A952100EAE2B3 /* LocalUserConfig.swift */; }; + 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; }; + 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; }; + 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; }; 4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; }; 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; }; @@ -90,7 +96,6 @@ 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 */; }; - 4CEE2AEB2805AEA300AB5EEF /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4CEE2AEA2805AEA300AB5EEF /* secp256k1 */; }; 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 */; }; @@ -124,6 +129,9 @@ 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.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>"; }; 4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; }; 4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; }; 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; }; @@ -168,6 +176,8 @@ 4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; }; 4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; }; 4C649843285A952100EAE2B3 /* LocalUserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalUserConfig.swift; sourceTree = "<group>"; }; + 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; }; + 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; }; 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>"; }; @@ -219,8 +229,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4CEE2AEB2805AEA300AB5EEF /* secp256k1 in Frameworks */, 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */, + 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -268,6 +278,7 @@ 4C987B56283FD07F0042CE38 /* FollowersModel.swift */, 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */, 4C649843285A952100EAE2B3 /* LocalUserConfig.swift */, + 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */, ); path = Models; sourceTree = "<group>"; @@ -308,6 +319,9 @@ 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */, 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */, 4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */, + 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */, + 4C216F31286E388800040376 /* DMChatView.swift */, + 4C216F33286F5ACD00040376 /* DMView.swift */, ); path = Views; sourceTree = "<group>"; @@ -343,6 +357,7 @@ 4C477C9D282C3A4800033AA3 /* TipCounter.swift */, 4C285C8B28398BC6008A31F1 /* Keys.swift */, 4C90BD19283AA67F008EE7EF /* Bech32.swift */, + 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */, ); path = Util; sourceTree = "<group>"; @@ -447,7 +462,7 @@ name = damus; packageProductDependencies = ( 4CE6DF1127F7A2B300C66700 /* Starscream */, - 4CEE2AEA2805AEA300AB5EEF /* secp256k1 */, + 4C649880286E0EE300EAE2B3 /* secp256k1 */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -523,7 +538,7 @@ mainGroup = 4CE6DEDA27F7A08100C66700; packageReferences = ( 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */, - 4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */, + 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -571,9 +586,11 @@ 4C363A8A28236B57006E126D /* MentionView.swift in Sources */, 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */, 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, + 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, 4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */, 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, + 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */, 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, 4C363A8628234FDE006E126D /* ImageCache.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, @@ -591,6 +608,7 @@ 4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */, 4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */, 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, + 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */, 4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */, @@ -617,6 +635,8 @@ 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */, 4C363A94282704FA006E126D /* Post.swift in Sources */, + 4C216F32286E388800040376 /* DMChatView.swift in Sources */, + 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C363A9C282838B9006E126D /* EventRef.swift in Sources */, @@ -991,35 +1011,35 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = { + 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/daltoniam/Starscream"; + repositoryURL = "https://github.com/jb55/secp256k1.swift"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.0.0; + kind = revision; + revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9; }; }; - 4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */ = { + 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/GigaBitcoin/secp256k1.swift"; + repositoryURL = "https://github.com/daltoniam/Starscream"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.5.0; + minimumVersion = 4.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 4C649880286E0EE300EAE2B3 /* secp256k1 */ = { + isa = XCSwiftPackageProductDependency; + package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */; + productName = secp256k1; + }; 4CE6DF1127F7A2B300C66700 /* Starscream */ = { isa = XCSwiftPackageProductDependency; package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; - 4CEE2AEA2805AEA300AB5EEF /* secp256k1 */ = { - isa = XCSwiftPackageProductDependency; - package = 4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */; - productName = secp256k1; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -3,10 +3,9 @@ { "identity" : "secp256k1.swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/GigaBitcoin/secp256k1.swift", + "location" : "https://github.com/jb55/secp256k1.swift", "state" : { - "revision" : "abe7c8232970c1fd57f4c77590bce2c868df7137", - "version" : "0.5.0" + "revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9" } }, { diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -121,6 +121,9 @@ struct ContentView: View { case .notifications: TimelineView(events: $home.notifications, loading: $home.loading, damus: damus) .navigationTitle("Notifications") + + case .dms: + DirectMessagesView(damus_state: damus_state!, dms: $home.dms) case .none: EmptyView() @@ -142,7 +145,7 @@ struct ContentView: View { var MaybeThreadView: some View { Group { if let evid = self.active_event_id { - let thread_model = ThreadModel(evid: evid, pool: damus_state!.pool) + let thread_model = ThreadModel(evid: evid, pool: damus_state!.pool, privkey: damus_state!.keypair.privkey) ThreadView(thread: thread_model, damus: damus_state!) } else { EmptyView() @@ -163,7 +166,7 @@ struct ContentView: View { } var body: some View { - VStack { + VStack(alignment: .leading, spacing: 0) { if let damus = self.damus_state { NavigationView { MainContent(damus: damus) diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift @@ -204,13 +204,13 @@ func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] { } // TODO: tests for this -func is_friend_event(_ ev: NostrEvent, our_pubkey: String, contacts: Contacts) -> Bool +func is_friend_event(_ ev: NostrEvent, keypair: Keypair, contacts: Contacts) -> Bool { if !contacts.is_friend(ev.pubkey) { return false } - if !ev.is_reply { + if ev.is_reply(keypair.privkey) { return true } diff --git a/damus/Models/DirectMessagesModel.swift b/damus/Models/DirectMessagesModel.swift @@ -0,0 +1,15 @@ +// +// DirectMessagesModel.swift +// damus +// +// Created by William Casarin on 2022-06-29. +// + +import Foundation + +class DirectMessagesModel: ObservableObject { + @Published var events: [(String, [NostrEvent])] = [] + @Published var loading: Bool = false + + +} diff --git a/damus/Models/EventRef.swift b/damus/Models/EventRef.swift @@ -147,8 +147,8 @@ func interpret_event_refs(blocks: [Block], tags: [[String]]) -> [EventRef] { } -func event_is_reply(_ ev: NostrEvent) -> Bool { - return ev.event_refs.contains { evref in +func event_is_reply(_ ev: NostrEvent, privkey: String?) -> Bool { + return ev.event_refs(privkey).contains { evref in return evref.is_reply != nil } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -9,91 +9,102 @@ import Foundation struct NewEventsBits { let bits: Int - + init() { bits = 0 } - + init (prev: NewEventsBits, setting: Timeline) { self.bits = prev.bits | timeline_bit(setting) } - + init (prev: NewEventsBits, unsetting: Timeline) { self.bits = prev.bits & ~timeline_bit(unsetting) } - + func is_set(_ timeline: Timeline) -> Bool { let notification_bit = timeline_bit(timeline) return (bits & notification_bit) == notification_bit } - + } class HomeModel: ObservableObject { var damus_state: DamusState - + var has_event: [String: Set<String>] = [:] var last_event_of_kind: [String: [Int: NostrEvent]] = [:] var done_init: Bool = false - + let home_subid = UUID().description let contacts_subid = UUID().description let notifications_subid = UUID().description let dms_subid = UUID().description let init_subid = UUID().description - + @Published var new_events: NewEventsBits = NewEventsBits() @Published var notifications: [NostrEvent] = [] + @Published var dms: [(String, [NostrEvent])] = [] @Published var events: [NostrEvent] = [] @Published var loading: Bool = false @Published var signal: SignalModel = SignalModel() - + init() { self.damus_state = DamusState.empty } - + init(damus_state: DamusState) { self.damus_state = damus_state } - + var pool: RelayPool { return damus_state.pool } - + func has_sub_id_event(sub_id: String, ev_id: String) -> Bool { if !has_event.keys.contains(sub_id) { has_event[sub_id] = Set() return false } - + return has_event[sub_id]!.contains(ev_id) } - + func process_event(sub_id: String, relay_id: String, ev: NostrEvent) { if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) { return } - + let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind) if last_k == nil || ev.created_at > last_k!.created_at { last_event_of_kind[relay_id]?[ev.kind] = ev } - if ev.kind == 1 { + + guard let kind = ev.known_kind else { + return + } + + switch kind { + case .text: handle_text_event(sub_id: sub_id, ev) - } else if ev.kind == 0 { + case .contacts: + handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev) + case .metadata: handle_metadata_event(ev) - } else if ev.kind == 6 { + case .boost: handle_boost_event(sub_id: sub_id, ev) - } else if ev.kind == 7 { + case .like: handle_like_event(ev) - } else if ev.kind == 3 { - handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev) + case .dm: + handle_dm(ev) + case .delete: + break } } - + func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) { process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev) - + if sub_id == init_subid { pool.send(.unsubscribe(init_subid), to: [relay_id]) if !done_init { @@ -102,23 +113,23 @@ class HomeModel: ObservableObject { } } } - + func handle_boost_event(sub_id: String, _ ev: NostrEvent) { var boost_ev_id = ev.last_refid()?.ref_id - + // CHECK SIGS ON THESE if let inner_ev = ev.inner_event { boost_ev_id = inner_ev.id - + if inner_ev.kind == 1 { handle_text_event(sub_id: sub_id, ev) } } - + guard let e = boost_ev_id else { return } - + switch self.damus_state.boosts.add_event(ev, target: e) { case .already_counted: break @@ -127,15 +138,15 @@ class HomeModel: ObservableObject { notify(.boosted, boosted) } } - + func handle_like_event(_ ev: NostrEvent) { guard let e = ev.last_refid() else { // no id ref? invalid like event return } - + // CHECK SIGS ON THESE - + switch damus_state.likes.add_event(ev, target: e.ref_id) { case .already_counted: break @@ -144,8 +155,8 @@ class HomeModel: ObservableObject { notify(.liked, liked) } } - - + + func handle_event(relay_id: String, conn_event: NostrConnectionEvent) { switch conn_event { case .ws_event(let ev): @@ -156,7 +167,7 @@ class HomeModel: ObservableObject { self.events.insert(wsev, at: 0) } */ - + switch ev { case .connected: @@ -182,7 +193,7 @@ class HomeModel: ObservableObject { default: break } - + update_signal_from_pool(signal: self.signal, pool: self.pool) print("ws_event \(ev)") @@ -191,44 +202,58 @@ class HomeModel: ObservableObject { switch ev { case .event(let sub_id, let ev): // globally handle likes - let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata + let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata if !always_process { // TODO: other views like threads might have their own sub ids, so ignore those events... or should we? return } - + self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev) case .notice(let msg): //self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0) print(msg) - + case .eose: self.loading = false break } } } - - + + /// Send the initial filters, just our contact list mostly func send_initial_filters(relay_id: String) { var filter = NostrFilter.filter_contacts filter.authors = [self.damus_state.pubkey] filter.limit = 1 - + pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id]) } - + func send_home_filters(relay_id: String?) { // TODO: since times should be based on events from a specific relay // perhaps we could mark this in the relay pool somehow - + var friends = damus_state.contacts.get_friend_list() friends.append(damus_state.pubkey) - + var contacts_filter = NostrFilter.filter_kinds([0]) contacts_filter.authors = friends - + + var dms_filter = NostrFilter.filter_kinds([ + NostrKind.dm.rawValue, + ]) + + var our_dms_filter = NostrFilter.filter_kinds([ + NostrKind.dm.rawValue, + ]) + + // friends only?... + //dms_filter.authors = friends + dms_filter.limit = 500 + dms_filter.pubkeys = [ damus_state.pubkey ] + our_dms_filter.authors = [ damus_state.pubkey ] + // TODO: separate likes? var home_filter = NostrFilter.filter_kinds([ NostrKind.text.rawValue, @@ -238,7 +263,7 @@ class HomeModel: ObservableObject { // include our pubkey as well even if we're not technically a friend home_filter.authors = friends home_filter.limit = 500 - + var notifications_filter = NostrFilter.filter_kinds([ NostrKind.text.rawValue, NostrKind.like.rawValue, @@ -250,56 +275,60 @@ class HomeModel: ObservableObject { var home_filters = [home_filter] var notifications_filters = [notifications_filter] var contacts_filters = [contacts_filter] - + var dms_filters = [dms_filter, our_dms_filter] + let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:] - + home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters) contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters) notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters) - - print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters]) - + dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters) + + print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters]) + if let relay_id = relay_id { pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id]) pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id]) pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id]) + pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: [relay_id]) } else { pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid))) pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid))) pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid))) + pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid))) } } - + func handle_metadata_event(_ ev: NostrEvent) { process_metadata_event(profiles: damus_state.profiles, ev: ev) } - + func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? { guard let m = last_event_of_kind[relay_id] else { last_event_of_kind[relay_id] = [:] return nil } - + return m[kind] } - + func handle_last_event(ev: NostrEvent, timeline: Timeline) { let last_ev = get_last_event(timeline) - + if last_ev == nil || last_ev!.created_at < ev.created_at { save_last_event(ev, timeline: timeline) new_events = NewEventsBits(prev: new_events, setting: timeline) } } - + func handle_notification(ev: NostrEvent) { if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { return } - + handle_last_event(ev: ev, timeline: .notifications) } - + func insert_home_event(_ ev: NostrEvent) -> Bool { let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at }) if ok { @@ -307,24 +336,67 @@ class HomeModel: ObservableObject { } return ok } - + func should_hide_event(_ ev: NostrEvent) -> Bool { return false } - + func handle_text_event(sub_id: String, _ ev: NostrEvent) { if should_hide_event(ev) { return } - + if sub_id == home_subid { - if is_friend_event(ev, our_pubkey: damus_state.pubkey, contacts: damus_state.contacts) { + if is_friend_event(ev, keypair: damus_state.keypair, contacts: damus_state.contacts) { let _ = insert_home_event(ev) } } else if sub_id == notifications_subid { handle_notification(ev: ev) } } + + func handle_dm(_ ev: NostrEvent) { + + var inserted = false + var found = false + let ours = ev.pubkey == self.damus_state.pubkey + var i = 0 + + var the_pk = ev.pubkey + if ours { + if let ref_pk = ev.referenced_pubkeys.first { + the_pk = ref_pk.ref_id + } else { + // self dm!? + print("TODO: handle self dm?") + } + } + + for (pk, _) in dms { + if pk == the_pk { + found = true + inserted = insert_uniq_sorted_event(events: &(dms[i].1), new_ev: ev) { + $0.created_at < $1.created_at + } + + break + } + i += 1 + } + + if !found { + inserted = true + dms.append((the_pk, [ev])) + } + + if inserted { + handle_last_event(ev: ev, timeline: .dms) + + dms = dms.sorted { a, b in + a.1.last!.created_at > b.1.last!.created_at + } + } + } } @@ -332,7 +404,7 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) { if signal.max_signal != pool.relays.count { signal.max_signal = pool.relays.count } - + if signal.signal != pool.num_connecting { signal.signal = signal.max_signal - pool.num_connecting } @@ -342,7 +414,7 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) { if !contacts.is_friend(ev.pubkey) { return } - + contacts.add_friend_contact(ev) } @@ -350,9 +422,9 @@ func load_our_contacts(contacts: Contacts, our_pubkey: String, ev: NostrEvent) { guard ev.pubkey == our_pubkey else { return } - + contacts.event = ev - + // our contacts for tag in ev.tags { if tag.count > 1 && tag[0] == "p" { @@ -398,7 +470,7 @@ func print_filter(_ f: NostrFilter) { abbrev_field("until", f.until), abbrev_field("limit", f.limit) ].filter({ !$0.isEmpty }).joined(separator: ",") - + print("Filter(\(fmt))") } @@ -427,7 +499,7 @@ func process_metadata_event(profiles: Profiles, ev: NostrEvent) { let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at) profiles.add(id: ev.pubkey, profile: tprof) - + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) } @@ -441,11 +513,11 @@ func load_our_relays(our_pubkey: String, pool: RelayPool, ev: NostrEvent) { guard ev.pubkey == our_pubkey else { return } - + guard let decoded = decode_json_relays(ev.content) else { return } - + for key in decoded.keys { if let url = URL(string: key) { if let _ = try? pool.add_relay(url, info: decoded[key]!) { @@ -460,11 +532,11 @@ func remove_bootstrap_nodes(_ damus_state: DamusState) { guard let contacts = damus_state.contacts.event else { return } - + guard let relays = decode_json_relays(contacts.content) else { return } - + let descriptors = relays.reduce(into: []) { arr, kv in guard let url = URL(string: kv.key) else { return diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -30,6 +30,8 @@ enum InitialEvent { /// manages the lifetime of a thread class ThreadModel: ObservableObject { + let privkey: String? + let kind: Int @Published var initial_event: InitialEvent @Published var events: [NostrEvent] = [] @Published var event_map: [String: Int] = [:] @@ -54,14 +56,25 @@ class ThreadModel: ObservableObject { let pool: RelayPool var sub_id = UUID().description - init(evid: String, pool: RelayPool) { + init(evid: String, pool: RelayPool, privkey: String?) { self.pool = pool self.initial_event = .event_id(evid) + self.privkey = privkey + self.kind = NostrKind.text.rawValue } - init(event: NostrEvent, pool: RelayPool) { + init(event: NostrEvent, pool: RelayPool, privkey: String?) { self.pool = pool self.initial_event = .event(event) + self.privkey = privkey + self.kind = NostrKind.text.rawValue + } + + init(event: NostrEvent, pool: RelayPool, privkey: String?, kind: Int) { + self.pool = pool + self.initial_event = .event(event) + self.privkey = privkey + self.kind = kind } func unsubscribe() { @@ -89,7 +102,7 @@ class ThreadModel: ObservableObject { return true } - func set_active_event(_ ev: NostrEvent) { + func set_active_event(_ ev: NostrEvent, privkey: String?) { if should_resubscribe(ev) { unsubscribe() self.initial_event = .event(ev) @@ -97,14 +110,14 @@ class ThreadModel: ObservableObject { } else { self.initial_event = .event(ev) if events.count == 0 { - add_event(ev) + add_event(ev, privkey: privkey) } } } func subscribe() { - var ref_events = NostrFilter.filter_kinds([1,5,6,7]) - var events_filter = NostrFilter.filter_kinds([1]) + var ref_events = NostrFilter.filter_kinds([self.kind,5,6,7]) + var events_filter = NostrFilter.filter_kinds([self.kind]) //var likes_filter = NostrFilter.filter_kinds(7]) // TODO: add referenced relays @@ -134,12 +147,12 @@ class ThreadModel: ObservableObject { return nil } - func add_event(_ ev: NostrEvent) { + func add_event(_ ev: NostrEvent, privkey: String?) { if event_map[ev.id] != nil { return } - for reply in ev.direct_replies() { + for reply in ev.direct_replies(privkey) { self.replies.add(id: ev.id, reply_id: reply.ref_id) } @@ -158,7 +171,7 @@ class ThreadModel: ObservableObject { if let evid = self.initial_event.is_event_id { if ev.id == evid { // this should trigger a resubscribe... - set_active_event(ev) + set_active_event(ev, privkey: privkey) } } @@ -167,7 +180,7 @@ class ThreadModel: ObservableObject { func handle_event(relay_id: String, ev: NostrConnectionEvent) { let done = handle_subid_event(pool: pool, sub_id: sub_id, relay_id: relay_id, ev: ev) { ev in if ev.known_kind == .text { - self.add_event(ev) + self.add_event(ev, privkey: self.privkey) } } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -8,6 +8,8 @@ import Foundation import CommonCrypto import secp256k1 +import secp256k1_implementation +import CryptoKit struct OtherEvent { let event_id: String @@ -23,7 +25,7 @@ struct ReferencedId: Identifiable, Hashable { let ref_id: String let relay_id: String? let key: String - + var id: String { return ref_id } @@ -53,24 +55,74 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { let created_at: Int64 let kind: Int let content: String - - lazy var blocks: [Block] = { - return parse_mentions(content: self.content, tags: self.tags) - }() - + + private var _blocks: [Block]? = nil + func blocks(_ privkey: String?) -> [Block] { + if let bs = _blocks { + return bs + } + let blocks = parse_mentions(content: self.get_content(privkey), tags: self.tags) + self._blocks = blocks + return blocks + } + lazy var inner_event: NostrEvent? = { return event_from_json(dat: self.content) }() - - lazy var event_refs: [EventRef] = { - return interpret_event_refs(blocks: self.blocks, tags: self.tags) - }() - + + private var _event_refs: [EventRef]? = nil + func event_refs(_ privkey: String?) -> [EventRef] { + if let rs = _event_refs { + return rs + } + let refs = interpret_event_refs(blocks: self.blocks(privkey), tags: self.tags) + self._event_refs = refs + return refs + } + + var decrypted_content: String? = nil + + func decrypted(privkey: String?) -> String? { + if let decrypted_content = decrypted_content { + return decrypted_content + } + + guard let key = privkey else { + return nil + } + + guard let our_pubkey = privkey_to_pubkey(privkey: key) else { + return nil + } + + var pubkey = self.pubkey + // This is our DM, we need to use the pubkey of the person we're talking to instead + if our_pubkey == pubkey { + guard let refkey = self.referenced_pubkeys.first else { + return nil + } + + pubkey = refkey.ref_id + } + + let dec = decrypt_dm(key, pubkey: pubkey, content: self.content) + self.decrypted_content = dec + + return dec + } + + func get_content(_ privkey: String?) -> String { + if known_kind == .dm { + return decrypted(privkey: privkey) ?? "*failed to decrypt content*" + } + return content + } + var description: String { let p = pow.map { String($0) } ?? "?" return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) pow \(p) content '\(content)' }" } - + var known_kind: NostrKind? { return NostrKind.init(rawValue: kind) } @@ -82,7 +134,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { private func get_referenced_ids(key: String) -> [ReferencedId] { return damus.get_referenced_ids(tags: self.tags, key: key) } - + public func is_root_event() -> Bool { for tag in tags { if tag.count >= 1 && tag[0] == "e" { @@ -91,15 +143,15 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { } return true } - - public func direct_replies() -> [ReferencedId] { - return event_refs.reduce(into: []) { acc, evref in + + public func direct_replies(_ privkey: String?) -> [ReferencedId] { + return event_refs(privkey).reduce(into: []) { acc, evref in if let direct_reply = evref.is_direct_reply { acc.append(direct_reply) } } } - + public func last_refid() -> ReferencedId? { var mlast: Int? = nil var i: Int = 0 @@ -109,14 +161,14 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { } i += 1 } - + guard let last = mlast else { return nil } - + return tag_to_refid(tags[last]) } - + public func references(id: String, key: String) -> Bool { for tag in tags { if tag.count >= 2 && tag[0] == key { @@ -129,18 +181,18 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { return false } - public var is_reply: Bool { - return event_is_reply(self) + func is_reply(_ privkey: String?) -> Bool { + return event_is_reply(self, privkey: privkey) } - + public var referenced_ids: [ReferencedId] { return get_referenced_ids(key: "e") } - + public func count_ids() -> Int { return count_refs("e") } - + public func count_refs(_ type: String) -> Int { var count: Int = 0 for tag in tags { @@ -150,7 +202,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { } return count } - + public var referenced_pubkeys: [ReferencedId] { return get_referenced_ids(key: "p") } @@ -165,7 +217,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { public var is_local: Bool { return (self.flags & 1) != 0 } - + init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) { self.id = "" self.sig = "" @@ -176,12 +228,12 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { self.tags = tags self.created_at = Int64(Date().timeIntervalSince1970) } - - init(from: NostrEvent) { + + init(from: NostrEvent, content: String? = nil) { self.id = from.id self.sig = from.sig - self.content = from.content + self.content = content ?? from.content self.pubkey = from.pubkey self.kind = from.kind self.tags = from.tags @@ -224,13 +276,13 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible { } func sign_event(privkey: String, ev: NostrEvent) -> String { - let priv_key_bytes = try! privkey.byteArray() + let priv_key_bytes = try! privkey.bytes let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes) // Extra params for custom signing var aux_rand = random_bytes(count: 64) - var digest = try! ev.id.byteArray() + var digest = try! ev.id.bytes // API allows for signing variable length messages let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand) @@ -343,12 +395,12 @@ func tag_to_refid(_ tag: [String]) -> ReferencedId? { if tag.count == 1 { return nil } - + var relay_id: String? = nil if tag.count > 2 { relay_id = tag[2] } - + return ReferencedId(ref_id: tag[1], relay_id: relay_id, key: tag[0]) } @@ -380,7 +432,7 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? { guard let privkey = keypair.privkey else { return nil } - + let rw_relay_info = RelayInfo(read: true, write: true) let relays: [String: RelayInfo] = ["wss://relay.damus.io": rw_relay_info] let relay_json = encode_json(relays)! @@ -402,13 +454,13 @@ func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEven guard let privkey = keypair.privkey else { return nil } - + let metadata_json = encode_json(metadata)! let ev = NostrEvent(content: metadata_json, pubkey: keypair.pubkey, kind: NostrKind.metadata.rawValue, tags: []) - + ev.calculate_id() ev.sign(privkey: privkey) return ev @@ -418,7 +470,7 @@ func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> N var tags: [[String]] = boosted.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") } tags.append(["e", boosted.id]) tags.append(["p", boosted.pubkey]) - + let ev = NostrEvent(content: event_to_json(ev: boosted), pubkey: pubkey, kind: 6, tags: tags) ev.calculate_id() ev.sign(privkey: privkey) @@ -432,13 +484,13 @@ func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> Nost let ev = NostrEvent(content: "", pubkey: pubkey, kind: 7, tags: tags) ev.calculate_id() ev.sign(privkey: privkey) - + return ev } func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] { var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? [] - + ids.append(ReferencedId(ref_id: from.id, relay_id: nil, key: "e")) ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }) if from.pubkey != our_pubkey { @@ -461,3 +513,152 @@ func event_to_json(ev: NostrEvent) -> String { } return str } + +func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String? { + guard let privkey = privkey else { + return nil + } + guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else { + return nil + } + guard let dat = decode_dm_base64(content) else { + return nil + } + guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else { + return nil + } + return String(data: dat, encoding: .utf8) +} + + +func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? { + guard let privkey_bytes = try? privkey.bytes else { + return nil + } + guard var pk_bytes = try? pubkey.bytes else { + return nil + } + pk_bytes.insert(2, at: 0) + + var publicKey = secp256k1_pubkey() + var shared_secret = [UInt8](repeating: 0, count: 32) + + var ok = + secp256k1_ec_pubkey_parse( + secp256k1.Context.raw, + &publicKey, + pk_bytes, + pk_bytes.count) != 0 + + if !ok { + return nil + } + + ok = secp256k1_ecdh( + secp256k1.Context.raw, + &shared_secret, + &publicKey, + privkey_bytes, {(output,x32,_,_) in + memcpy(output,x32,32) + return 1 + }, nil) != 0 + + if !ok { + return nil + } + + return shared_secret +} + +struct DirectMessageBase64 { + let content: [UInt8] + let iv: [UInt8] +} + +func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String { + let content_b64 = base64_encode(content) + let iv_b64 = base64_encode(iv) + return content_b64 + "?iv=" + iv_b64 +} + +func decode_dm_base64(_ all: String) -> DirectMessageBase64? { + let splits = Array(all.split(separator: "?")) + + if splits.count != 2 { + return nil + } + + guard let content = base64_decode(String(splits[0])) else { + return nil + } + + var sec = String(splits[1]) + if !sec.hasPrefix("iv=") { + return nil + } + + sec = String(sec.dropFirst(3)) + guard let iv = base64_decode(sec) else { + return nil + } + + return DirectMessageBase64(content: content, iv: iv) +} + +func base64_encode(_ content: [UInt8]) -> String { + return Data(content).base64EncodedString() +} + +func base64_decode(_ content: String) -> [UInt8]? { + guard let dat = Data(base64Encoded: content) else { + return nil + } + return dat.bytes +} + +func aes_decrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { + return aes_operation(operation: CCOperation(kCCDecrypt), data: data, iv: iv, shared_sec: shared_sec) +} + +func aes_encrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { + return aes_operation(operation: CCOperation(kCCEncrypt), data: data, iv: iv, shared_sec: shared_sec) +} + +func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? { + let data_len = data.count + let bsize = kCCBlockSizeAES128 + let len = Int(data_len) + bsize + var decrypted_data = [UInt8](repeating: 0, count: len) + + let key_length = size_t(kCCKeySizeAES256) + if shared_sec.count != key_length { + assert(false, "unexpected shared_sec len: \(shared_sec.count) != 32") + return nil + } + + let algorithm: CCAlgorithm = UInt32(kCCAlgorithmAES128) + let options: CCOptions = UInt32(kCCOptionPKCS7Padding) + + var num_bytes_decrypted :size_t = 0 + + let status = CCCrypt(operation, /*op:*/ + algorithm, /*alg:*/ + options, /*options:*/ + shared_sec, /*key:*/ + key_length, /*keyLength:*/ + iv, /*iv:*/ + data, /*dataIn:*/ + data_len, /*dataInLength:*/ + &decrypted_data,/*dataOut:*/ + len,/*dataOutAvailable:*/ + &num_bytes_decrypted/*dataOutMoved:*/ + ) + + if UInt32(status) != UInt32(kCCSuccess) { + return nil + } + + return Data(bytes: decrypted_data, count: num_bytes_decrypted) + +} + diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift @@ -12,6 +12,7 @@ enum NostrKind: Int { case metadata = 0 case text = 1 case contacts = 3 + case dm = 4 case delete = 5 case boost = 6 case like = 7 diff --git a/damus/Util/InputDismissKeyboard.swift b/damus/Util/InputDismissKeyboard.swift @@ -0,0 +1,39 @@ +// +// InputDismissKeyboard.swift +// damus +// +// Created by William Casarin on 2022-07-02. +// + +import Foundation +import SwiftUI + +public extension View { + func dismissKeyboardOnTap() -> some View { + modifier(DismissKeyboardOnTap()) + } +} + +public struct DismissKeyboardOnTap: ViewModifier { + public func body(content: Content) -> some View { + #if os(macOS) + return content + #else + return content.gesture(tapGesture) + #endif + } + + private var tapGesture: some Gesture { + TapGesture().onEnded(endEditing) + } + + private func endEditing() { + UIApplication.shared.connectedScenes + .filter {$0.activationState == .foregroundActive} + .map {$0 as? UIWindowScene} + .compactMap({$0}) + .first?.windows + .filter {$0.isKeyWindow} + .first?.endEditing(true) + } +} diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift @@ -61,7 +61,7 @@ func bech32_pubkey(_ pubkey: String) -> String? { func generate_new_keypair() -> Keypair { let key = try! secp256k1.Signing.PrivateKey() let privkey = hex_encode(key.rawRepresentation) - let pubkey = hex_encode(Data(key.publicKey.xonlyKeyBytes)) + let pubkey = hex_encode(Data(key.publicKey.xonly.bytes)) print("generating privkey:\(privkey) pubkey:\(pubkey)") return Keypair(pubkey: pubkey, privkey: privkey) } @@ -73,7 +73,7 @@ func privkey_to_pubkey(privkey: String) -> String? { guard let key = try? secp256k1.Signing.PrivateKey(rawRepresentation: sec) else { return nil } - return hex_encode(Data(key.publicKey.xonlyKeyBytes)) + return hex_encode(Data(key.publicKey.xonly.bytes)) } func save_pubkey(pubkey: String) { diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -12,7 +12,7 @@ struct ChatView: View { let prev_ev: NostrEvent? let next_ev: NostrEvent? - let damus: DamusState + let damus_state: DamusState @EnvironmentObject var thread: ThreadModel @@ -45,16 +45,7 @@ struct ChatView: View { } func prev_reply_is_same() -> String? { - if let prev = prev_ev { - if let prev_reply_id = thread.replies.lookup(prev.id) { - if let cur_reply_id = thread.replies.lookup(event.id) { - if prev_reply_id != cur_reply_id { - return cur_reply_id - } - } - } - } - return nil + return damus.prev_reply_is_same(event: event, prev_ev: prev_ev, replies: thread.replies) } func reply_is_new() -> String? { @@ -71,7 +62,7 @@ struct ChatView: View { } var ReplyDescription: some View { - Text("\(reply_desc(profiles: damus.profiles, event: event))") + Text("\(reply_desc(profiles: damus_state.profiles, event: event))") .font(.footnote) .foregroundColor(.gray) .frame(alignment: .leading) @@ -83,7 +74,7 @@ struct ChatView: View { HStack { VStack { if is_active || just_started { - ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, image_cache: damus.image_cache, profiles: damus.profiles) + ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles) } Spacer() @@ -94,7 +85,7 @@ struct ChatView: View { VStack(alignment: .leading) { if just_started { HStack { - ProfileName(pubkey: event.pubkey, profile: damus.profiles.lookup(id: event.pubkey)) + ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey)) .foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black) //.shadow(color: Color.black, radius: 2) Text("\(format_relative_time(event.created_at))") @@ -104,17 +95,17 @@ struct ChatView: View { if let ref_id = thread.replies.lookup(event.id) { if !is_reply_to_prev() { - ReplyQuoteView(quoter: event, event_id: ref_id, image_cache: damus.image_cache, profiles: damus.profiles) + ReplyQuoteView(privkey: damus_state.keypair.privkey, quoter: event, event_id: ref_id, image_cache: damus_state.image_cache, profiles: damus_state.profiles) .environmentObject(thread) ReplyDescription } } - NoteContentView(event: event, profiles: damus.profiles, content: event.content) + NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, content: event.content) if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { - let bar = make_actionbar_model(ev: event, damus: damus) - EventActionBar(damus_state: damus, event: event, bar: bar) + let bar = make_actionbar_model(ev: event, damus: damus_state) + EventActionBar(damus_state: damus_state, event: event, bar: bar) } //Spacer() @@ -154,3 +145,16 @@ struct ChatView_Previews: PreviewProvider { */ +func prev_reply_is_same(event: NostrEvent, prev_ev: NostrEvent?, replies: ReplyMap) -> String? { + if let prev = prev_ev { + if let prev_reply_id = replies.lookup(prev.id) { + if let cur_reply_id = replies.lookup(event.id) { + if prev_reply_id != cur_reply_id { + return cur_reply_id + } + } + } + } + return nil +} + diff --git a/damus/Views/ChatroomView.swift b/damus/Views/ChatroomView.swift @@ -21,7 +21,7 @@ struct ChatroomView: View { ChatView(event: thread.events[ind], prev_ev: ind > 0 ? thread.events[ind-1] : nil, next_ev: ind == count-1 ? nil : thread.events[ind+1], - damus: damus + damus_state: damus ) .event_context_menu(ev) .onTapGesture { @@ -29,7 +29,7 @@ struct ChatroomView: View { //dismiss() toggle_thread_view() } else { - thread.set_active_event(ev) + thread.set_active_event(ev, privkey: damus.keypair.privkey) } } .environmentObject(thread) @@ -39,7 +39,7 @@ struct ChatroomView: View { .onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in let ev = notif.object as! NostrEvent if ev.id != thread.initial_event.id { - thread.set_active_event(ev) + thread.set_active_event(ev, privkey: damus.keypair.privkey) } scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true, anchor: .top) } @@ -63,7 +63,7 @@ struct ChatroomView_Previews: PreviewProvider { static var previews: some View { let state = test_damus_state() ChatroomView(damus: state) - .environmentObject(ThreadModel(evid: "&849ab9bb263ed2819db06e05f1a1a3b72878464e8c7146718a2fc1bf1912f893", pool: state.pool)) + .environmentObject(ThreadModel(evid: "&849ab9bb263ed2819db06e05f1a1a3b72878464e8c7146718a2fc1bf1912f893", pool: state.pool, privkey: state.keypair.privkey)) } } diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -0,0 +1,155 @@ +// +// DMChatView.swift +// damus +// +// Created by William Casarin on 2022-06-30. +// + +import SwiftUI + +struct DMChatView: View { + let damus_state: DamusState + let pubkey: String + @Binding var events: [NostrEvent] + @State var message: String = "" + + var Messages: some View { + ScrollViewReader { scroller in + ScrollView { + VStack(alignment: .leading) { + ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in + DMView(event: events[ind], damus_state: damus_state) + .event_context_menu(ev) + } + Color.white.opacity(0) + .id("endblock") + .frame(height: 80) + } + } + .onAppear { + scroller.scrollTo("endblock") + } + } + } + + var Header: some View { + let profile = damus_state.profiles.lookup(id: pubkey) + let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) + let fmodel = FollowersModel(damus_state: damus_state, target: pubkey) + let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel) + return NavigationLink(destination: profile_page) { + HStack { + ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles) + + ProfileName(pubkey: pubkey, profile: profile) + } + } + .buttonStyle(PlainButtonStyle()) + } + + var InputField: some View { + TextField("New Message", text: $message) + .padding([.leading], 12) + .padding([.top, .bottom], 8) + .background { + InputBackground() + } + .foregroundColor(Color.primary) + .cornerRadius(20) + .padding([.leading, .top, .bottom], 8) + } + + @Environment(\.colorScheme) var colorScheme + + func InputBackground() -> some View { + if colorScheme == .dark { + return Color.black.brightness(0.1) + } else { + return Color.gray.brightness(0.35) + } + } + + func BackgroundColor() -> some View { + if colorScheme == .dark { + return Color.black.opacity(0.9) + } else { + return Color.white.opacity(0.9) + } + } + + var Footer: some View { + ZStack { + BackgroundColor() + + HStack { + InputField + + Button(role: .none, action: send_message) { + Label("", systemImage: "arrow.right.circle") + .font(.title) + } + } + } + .frame(height: 70) + } + + func send_message() { + guard let dm = create_dm(message, to_pk: pubkey, keypair: damus_state.keypair) else { + print("error creating dm") + return + } + + message = "" + + damus_state.pool.send(.event(dm)) + } + + var body: some View { + ZStack { + Messages + .padding([.top, .leading, .trailing], 10) + .dismissKeyboardOnTap() + + VStack { + Spacer() + + Footer + } + } + .toolbar { Header } + } +} + +struct DMChatView_Previews: PreviewProvider { + static var previews: some View { + let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: []) + let evs = Binding<[NostrEvent]>.init( + get: { [ev] }, + set: { _ in }) + + DMChatView(damus_state: test_damus_state(), pubkey: "pubkey", events: evs) + } +} + + +func create_dm(_ message: String, to_pk: String, keypair: Keypair) -> NostrEvent? +{ + guard let privkey = keypair.privkey else { + return nil + } + + let tags = [["p", to_pk]] + let iv = random_bytes(count: 16).bytes + guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else { + return nil + } + let utf8_message = Data(message.utf8).bytes + guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else { + return nil + } + let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv) + let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags) + ev.calculate_id() + ev.sign(privkey: privkey) + return ev +} diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift @@ -0,0 +1,39 @@ +// +// DMView.swift +// damus +// +// Created by William Casarin on 2022-07-01. +// + +import SwiftUI + +struct DMView: View { + let event: NostrEvent + let damus_state: DamusState + + var is_ours: Bool { + event.pubkey == damus_state.pubkey + } + + var body: some View { + HStack { + if is_ours { + Spacer() + } + + NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, content: event.get_content(damus_state.keypair.privkey)) + .foregroundColor(is_ours ? Color.white : Color.primary) + .padding(10) + .background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15)) + .cornerRadius(8.0) + .tint(is_ours ? Color.white : Color.accentColor) + } + } +} + +struct DMView_Previews: PreviewProvider { + static var previews: some View { + let ev = NostrEvent(content: "Hey there *buddy*, want to grab some drinks later? 🍻", pubkey: "pubkey", kind: 1, tags: []) + DMView(event: ev, damus_state: test_damus_state()) + } +} diff --git a/damus/Views/DirectMessagesView.swift b/damus/Views/DirectMessagesView.swift @@ -0,0 +1,52 @@ +// +// DirectMessagesView.swift +// damus +// +// Created by William Casarin on 2022-06-29. +// + +import SwiftUI + +struct DirectMessagesView: View { + let damus_state: DamusState + @Binding var dms: [(String, [NostrEvent])] + + var MainContent: some View { + ScrollView { + VStack { + ForEach(dms, id: \.0) { tup in + let evs = Binding<[NostrEvent]>.init( + get: { tup.1 }, + set: { _ in } + ) + let chat = DMChatView(damus_state: damus_state, pubkey: tup.0, events: evs) + NavigationLink(destination: chat) { + EventView(damus: damus_state, event: tup.1.last!, pubkey: tup.0) + } + .buttonStyle(PlainButtonStyle()) + } + } + } + } + + var body: some View { + MainContent + .navigationTitle("Encrypted DMs") + } +} + +struct DirectMessagesView_Previews: PreviewProvider { + static var previews: some View { + let ev = NostrEvent(content: "encrypted stuff", + pubkey: "pubkey", + kind: 4, + tags: []) + let dms = Binding<[(String, [NostrEvent])]>.init( + get: { + return [ ("pubkey", [ ev ]) ] + }, + set: { _ in } + ) + DirectMessagesView(damus_state: test_damus_state(), dms: dms) + } +} diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -73,7 +73,7 @@ struct EventDetailView: View { if thread.initial_event.id == ev.id { toggle_thread_view() } else { - thread.set_active_event(ev) + thread.set_active_event(ev, privkey: damus.keypair.privkey) } } .onAppear() { @@ -88,7 +88,12 @@ struct EventDetailView: View { var body: some View { ScrollViewReader { proxy in ScrollView { - let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: thread.event, events: thread.events) + 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) } @@ -112,7 +117,7 @@ struct EventDetailView_Previews: PreviewProvider { */ /// Find the entire reply path for the active event -func make_reply_map(active: NostrEvent, events: [NostrEvent]) -> [String: ()] +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 @@ -129,7 +134,8 @@ func make_reply_map(active: NostrEvent, events: [NostrEvent]) -> [String: ()] for ev in events { /// does this event reply to the active event? - for ev_ref in ev.event_refs { + 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] = () @@ -139,7 +145,8 @@ func make_reply_map(active: NostrEvent, events: [NostrEvent]) -> [String: ()] } /// does the active event reply to this event? - for active_ref in active.event_refs { + 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] = () @@ -194,14 +201,14 @@ func determine_highlight(reply_map: [String: ()], current: NostrEvent, active: N } } -func calculated_collapsed_events(collapsed: Bool, active: NostrEvent?, events: [NostrEvent]) -> [CollapsedEvent] { +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) + let reply_map = make_reply_map(active: active, events: events, privkey: privkey) if !collapsed { return events.reduce(into: []) { acc, ev in diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -20,7 +20,7 @@ enum Highlight { } return false } - + var is_none: Bool { if case .none = self { return true @@ -41,9 +41,34 @@ struct EventView: View { let highlight: Highlight let has_action_bar: Bool let damus: DamusState + let pubkey: String @EnvironmentObject var action_bar: ActionBarModel - + + init(event: NostrEvent, highlight: Highlight, has_action_bar: Bool, damus: DamusState) { + self.event = event + self.highlight = highlight + self.has_action_bar = has_action_bar + self.damus = damus + self.pubkey = event.pubkey + } + + init(damus: DamusState, event: NostrEvent) { + self.event = event + self.highlight = .none + self.has_action_bar = false + self.damus = damus + self.pubkey = event.pubkey + } + + init(damus: DamusState, event: NostrEvent, pubkey: String) { + self.event = event + self.highlight = .none + self.has_action_bar = false + self.damus = damus + self.pubkey = pubkey + } + var body: some View { return Group { if event.known_kind == .boost, let inner_ev = event.inner_event { @@ -51,7 +76,7 @@ struct EventView: View { HStack { Label("", systemImage: "arrow.2.squarepath") .foregroundColor(Color.gray) - ProfileName(pubkey: event.pubkey, profile: damus.profiles.lookup(id: event.pubkey)) + ProfileName(pubkey: pubkey, profile: damus.profiles.lookup(id: pubkey)) .foregroundColor(Color.gray) Text(" Boosted") .foregroundColor(Color.gray) @@ -63,16 +88,17 @@ struct EventView: View { } } } - + func TextEvent(_ event: NostrEvent) -> some View { + let content = event.get_content(damus.keypair.privkey) return HStack(alignment: .top) { - let profile = damus.profiles.lookup(id: event.pubkey) + let profile = damus.profiles.lookup(id: pubkey) VStack { - let pmodel = ProfileModel(pubkey: event.pubkey, damus: damus) - let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: event.pubkey)) - + let pmodel = ProfileModel(pubkey: pubkey, damus: damus) + let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey)) + NavigationLink(destination: pv) { - ProfilePicView(pubkey: event.pubkey, size: PFP_SIZE, highlight: highlight, image_cache: damus.image_cache, profiles: damus.profiles) + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, image_cache: damus.image_cache, profiles: damus.profiles) } Spacer() @@ -80,19 +106,19 @@ struct EventView: View { VStack(alignment: .leading) { HStack(alignment: .center) { - ProfileName(pubkey: event.pubkey, profile: profile) + ProfileName(pubkey: pubkey, profile: profile) Text("\(format_relative_time(event.created_at))") .foregroundColor(.gray) } - - if event.is_reply { + + if event.is_reply(damus.keypair.privkey) { Text("\(reply_desc(profiles: damus.profiles, event: event))") .font(.footnote) .foregroundColor(.gray) .frame(maxWidth: .infinity, alignment: .leading) } - NoteContentView(event: event, profiles: damus.profiles, content: event.content) + NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, content: content) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) @@ -122,7 +148,7 @@ extension View { } label: { Label("Copy Text", systemImage: "doc.on.doc") } - + Button { UIPasteboard.general.string = "@" + event.pubkey } label: { @@ -134,20 +160,20 @@ extension View { } label: { Label("Copy Note ID", systemImage: "tag") } - + Button { UIPasteboard.general.string = event_to_json(ev: event) } label: { Label("Copy Note", systemImage: "note") } - + Button { NotificationCenter.default.post(name: .broadcast_event, object: event) } label: { Label("Broadcast", systemImage: "globe") } } - + } } @@ -160,16 +186,16 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String { let desc = make_reply_description(event.tags) let pubkeys = desc.pubkeys let n = desc.others - + if desc.pubkeys.count == 0 { return "Reply to self" } - + let names: [String] = pubkeys.map { let prof = profiles.lookup(id: $0) return Profile.displayName(profile: prof, pubkey: $0) } - + if names.count == 2 { if n > 2 { let and_other = reply_others_desc(n: n, n_pubkeys: pubkeys.count) @@ -177,7 +203,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String { } return "Replying to \(names[0]) & \(names[1])" } - + let and_other = reply_others_desc(n: n, n_pubkeys: pubkeys.count) return "Replying to \(names[0])\(and_other)" } @@ -197,7 +223,7 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel { let our_like = damus.likes.our_events[ev.id] let our_boost = damus.boosts.our_events[ev.id] let our_tip = damus.tips.our_tips[ev.id] - + return ActionBarModel(likes: likes ?? 0, boosts: boosts ?? 0, tips: tips ?? 0, diff --git a/damus/Views/MainTabView.swift b/damus/Views/MainTabView.swift @@ -11,6 +11,7 @@ enum Timeline: String, CustomStringConvertible { case home case notifications case search + case dms var description: String { return self.rawValue @@ -76,6 +77,7 @@ struct TabBar: View { Divider() HStack { TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, action: action) + TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, action: action) TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, action: action) TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, action: action) } @@ -83,3 +85,5 @@ struct TabBar: View { } } + + diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -8,8 +8,9 @@ import SwiftUI -func render_note_content(ev: NostrEvent, profiles: Profiles) -> String { - return ev.blocks.reduce("") { str, block in +func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> String { + let blocks = ev.blocks(privkey) + return blocks.reduce("") { str, block in switch block { case .mention(let m): return str + mention_str(m, profiles: profiles) @@ -22,6 +23,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles) -> String { } struct NoteContentView: View { + let privkey: String? let event: NostrEvent let profiles: Profiles @@ -31,8 +33,8 @@ struct NoteContentView: View { let md_opts: AttributedString.MarkdownParsingOptions = .init(interpretedSyntax: .inlineOnlyPreservingWhitespace) - guard let txt = try? AttributedString(markdown: content, options: md_opts) else { - return Text(event.content) + guard var txt = try? AttributedString(markdown: content, options: md_opts) else { + return Text(content) } return Text(txt) @@ -41,15 +43,16 @@ struct NoteContentView: View { var body: some View { MainContent() .onAppear() { - self.content = render_note_content(ev: event, profiles: profiles) + self.content = render_note_content(ev: event, profiles: profiles, privkey: privkey) } .onReceive(handle_notify(.profile_updated)) { notif in let profile = notif.object as! ProfileUpdate - for block in event.blocks { + let blocks = event.blocks(privkey) + for block in blocks { switch block { case .mention(let m): if m.type == .pubkey && m.ref.ref_id == profile.pubkey { - content = render_note_content(ev: event, profiles: profiles) + content = render_note_content(ev: event, profiles: profiles, privkey: privkey) } case .text: return case .hashtag: return diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -45,6 +45,27 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool { } } +struct ProfileNameView: View { + let pubkey: String + let profile: Profile? + + var body: some View { + Group { + if let real_name = profile?.display_name { + VStack(alignment: .leading) { + Text(real_name) + .font(.title) + ProfileName(pubkey: pubkey, profile: profile, prefix: "@") + .font(.callout) + .foregroundColor(.gray) + } + } else { + ProfileName(pubkey: pubkey, profile: profile) + } + } + } +} + struct ProfileView: View { let damus_state: DamusState @@ -60,19 +81,8 @@ struct ProfileView: View { HStack(alignment: .center) { ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE, highlight: .custom(Color.black, 2), image_cache: damus_state.image_cache, profiles: damus_state.profiles) - - if let real_name = data?.display_name { - VStack(alignment: .leading) { - Text(real_name) - .font(.title) - ProfileName(pubkey: profile.pubkey, profile: data, prefix: "@") - .font(.callout) - .foregroundColor(.gray) - } - } else { - ProfileName(pubkey: profile.pubkey, profile: data) - } - //.border(Color.green) + + ProfileNameView(pubkey: profile.pubkey, profile: data) Spacer() diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift @@ -8,6 +8,7 @@ import SwiftUI struct ReplyQuoteView: View { + let privkey: String? let quoter: NostrEvent let event_id: String let image_cache: ImageCache @@ -31,7 +32,7 @@ struct ReplyQuoteView: View { .foregroundColor(.gray) } - NoteContentView(event: event, profiles: profiles, content: event.content) + NoteContentView(privkey: privkey, event: event, profiles: profiles, content: event.content) .font(.callout) .foregroundColor(.accentColor) @@ -64,7 +65,7 @@ struct ReplyQuoteView_Previews: PreviewProvider { static var previews: some View { let s = test_damus_state() let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey") - ReplyQuoteView(quoter: quoter, event_id: "pubkey2", image_cache: s.image_cache, profiles: s.profiles) - .environmentObject(ThreadModel(event: quoter, pool: s.pool)) + ReplyQuoteView(privkey: s.keypair.privkey, quoter: quoter, event_id: "pubkey2", image_cache: s.image_cache, profiles: s.profiles) + .environmentObject(ThreadModel(event: quoter, pool: s.pool, privkey: s.keypair.privkey)) } } diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift @@ -19,7 +19,7 @@ struct InnerTimelineView: View { var body: some View { LazyVStack { ForEach(events, id: \.id) { (ev: NostrEvent) in - let tv = ThreadView(thread: ThreadModel(event: ev, pool: damus.pool), damus: damus) + let tv = ThreadView(thread: ThreadModel(event: ev, pool: damus.pool, privkey: damus.keypair.privkey), damus: damus) NavigationLink(destination: tv) { EventView(event: ev, highlight: .none, has_action_bar: true, damus: damus)