damus

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

commit 52aefc8d648009e939c0a65d4874ff67c1d2b0f1
parent 8dbdff7ff0564c4edda469424f9c67c407b9c6bd
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 11 May 2024 09:02:09 -0700

nip10: simplify and fix reply-to-root bugs

This removes EventRefs alltogether and uses the form we use in Damus
Android.

This simplifies our ThreadReply logic and fixes a reply-to-root bug

Reported-by: NotBiebs <justinbieber@stemstr.app>
Changelog-Fixed: Fix thread bug where a quote isn't picked up as a reply
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 6------
Mdamus/ContentParsing.swift | 75+++++++++++++++++++++++++++++----------------------------------------------
Mdamus/Models/ContentFilters.swift | 2+-
Mdamus/Models/SearchHomeModel.swift | 2+-
Mdamus/Models/ThreadModel.swift | 2+-
Ddamus/NIP10/EventRef.swift | 151------------------------------------------------------------------------------
Mdamus/NIP10/ThreadReply.swift | 41+++++------------------------------------
Mdamus/Util/EventCache.swift | 4++--
Mdamus/Util/ReplyCounter.swift | 2+-
Mdamus/Views/Events/Components/ReplyPart.swift | 2+-
Mdamus/Views/Events/EventMenu.swift | 2+-
Mdamus/Views/Events/SelectedEventView.swift | 2+-
Mdamus/Views/PostView.swift | 10+---------
MdamusTests/NIP10Tests.swift | 153++++++++++++++++++++++++++++++++++++-------------------------------------------
MdamusTests/ReplyTests.swift | 114++++++++++---------------------------------------------------------------------
Mnostrdb/NdbNote.swift | 16++++++++--------
Mnostrdb/Test/NdbTests.swift | 46----------------------------------------------
17 files changed, 135 insertions(+), 495 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -136,7 +136,6 @@ 4C363A94282704FA006E126D /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A93282704FA006E126D /* Post.swift */; }; 4C363A962827096D006E126D /* PostBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A952827096D006E126D /* PostBlock.swift */; }; 4C363A9A28283854006E126D /* Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9928283854006E126D /* Reply.swift */; }; - 4C363A9C282838B9006E126D /* EventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9B282838B9006E126D /* EventRef.swift */; }; 4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9D2828A822006E126D /* ReplyTests.swift */; }; 4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9F2828A8DD006E126D /* LikeTests.swift */; }; 4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; }; @@ -555,7 +554,6 @@ D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; }; D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; }; D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; }; - D7CCFC0E2B0587C300323D86 /* EventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9B282838B9006E126D /* EventRef.swift */; }; D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; }; D7CCFC102B05880F00323D86 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B7BF12A71B6540049DEE7 /* Id.swift */; }; D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5D5C9C2A6B2CB40024563C /* AsciiCharacter.swift */; }; @@ -928,7 +926,6 @@ 4C363A93282704FA006E126D /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; }; 4C363A952827096D006E126D /* PostBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBlock.swift; sourceTree = "<group>"; }; 4C363A9928283854006E126D /* Reply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reply.swift; sourceTree = "<group>"; }; - 4C363A9B282838B9006E126D /* EventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRef.swift; sourceTree = "<group>"; }; 4C363A9D2828A822006E126D /* ReplyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyTests.swift; sourceTree = "<group>"; }; 4C363A9F2828A8DD006E126D /* LikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeTests.swift; sourceTree = "<group>"; }; 4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; }; @@ -1793,7 +1790,6 @@ 4C45E5002BED4CE10025A428 /* NIP10 */ = { isa = PBXGroup; children = ( - 4C363A9B282838B9006E126D /* EventRef.swift */, 4C45E5012BED4D000025A428 /* ThreadReply.swift */, ); path = NIP10; @@ -3350,7 +3346,6 @@ 4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */, 4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */, 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */, - 4C363A9C282838B9006E126D /* EventRef.swift in Sources */, 4C5E54032A9522F600FF6E60 /* UserStatus.swift in Sources */, 4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */, 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */, @@ -3606,7 +3601,6 @@ D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */, D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */, D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */, - D7CCFC0E2B0587C300323D86 /* EventRef.swift in Sources */, D7CCFC192B058A3F00323D86 /* Block.swift in Sources */, D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */, D798D2202B08592000234419 /* NdbTagIterator.swift in Sources */, diff --git a/damus/ContentParsing.swift b/damus/ContentParsing.swift @@ -59,26 +59,24 @@ func parse_note_content(content: NoteContent) -> Blocks { } } -func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] { +func interpret_event_refs(tags: TagsSequence) -> ThreadReply? { + // migration is long over, lets just do this to fix tests + return interpret_event_refs_ndb(tags: tags) +} + +func interpret_event_refs_ndb(tags: TagsSequence) -> ThreadReply? { if tags.count == 0 { - return [] + return nil } - - /// build a set of indices for each event mention - let mention_indices = build_mention_indices(blocks, type: .e) - /// simpler case with no mentions - if mention_indices.count == 0 { - return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs) - } - - return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices) + return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags)) } -func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] { - var evrefs: [EventRef] = [] +func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> ThreadReply? { var first: Bool = true var root_id: NoteRef? = nil + var reply_id: NoteRef? = nil + var mention: NoteRef? = nil var any_marker: Bool = false for ref in ev_tags { @@ -86,47 +84,32 @@ func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [ any_marker = true switch marker { case .root: root_id = ref - case .reply: evrefs.append(.reply(ref)) - case .mention: evrefs.append(.mention(.noteref(ref))) + case .reply: reply_id = ref + case .mention: mention = ref } - } else { - if !any_marker && first { + // deprecated form, only activate if we don't have any markers set + } else if !any_marker { + if first { root_id = ref first = false - } else if !any_marker { - evrefs.append(.reply(ref)) + } else { + reply_id = ref } } } - if let root_id { - if evrefs.count == 0 { - return [.reply_to_root(root_id)] - } else { - evrefs.insert(.thread_id(root_id), at: 0) - } + // If either reply or root_id is blank while the other is not, then this is + // considered reply-to-root. We should always have a root and reply tag, if they + // are equal this is reply-to-root + if reply_id == nil && root_id != nil { + reply_id = root_id + } else if root_id == nil && reply_id != nil { + root_id = reply_id } - return evrefs -} - -func interp_event_refs_with_mentions_ndb(tags: TagsSequence, mention_indices: Set<Int>) -> [EventRef] { - var mentions: [EventRef] = [] - var ev_refs: [NoteRef] = [] - var i: Int = 0 - - for tag in tags { - if let note_id = NoteRef.from_tag(tag: tag) { - if mention_indices.contains(i) { - mentions.append(.mention(.noteref(note_id, index: i))) - } else { - ev_refs.append(note_id) - } - } - i += 1 + guard let reply_id, let root_id else { + return nil } - - var replies = interp_event_refs_without_mentions(ev_refs) - replies.append(contentsOf: mentions) - return replies + + return ThreadReply(root: root_id, reply: reply_id, mention: mention.map { m in .noteref(m) }) } diff --git a/damus/Models/ContentFilters.swift b/damus/Models/ContentFilters.swift @@ -16,7 +16,7 @@ enum FilterState : Int { func filter(ev: NostrEvent) -> Bool { switch self { case .posts: - return ev.known_kind == .boost || !ev.is_reply(.empty) + return ev.known_kind == .boost || !ev.is_reply() case .posts_and_replies: return true } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject { guard sub_id == self.base_subid || sub_id == self.profiles_subid else { return } - if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply(damus_state.keypair) + if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply() { if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) { return diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -60,7 +60,7 @@ class ThreadModel: ObservableObject { var event_filter = NostrFilter() var ref_events = NostrFilter() - let thread_id = event.thread_id(keypair: .empty) + let thread_id = event.thread_id() ref_events.referenced_ids = [thread_id, event.id] ref_events.kinds = [.text] diff --git a/damus/NIP10/EventRef.swift b/damus/NIP10/EventRef.swift @@ -1,151 +0,0 @@ -// -// EventRef.swift -// damus -// -// Created by William Casarin on 2022-05-08. -// - -import Foundation - -enum EventRef: Equatable { - case mention(Mention<NoteRef>) - case thread_id(NoteRef) - case reply(NoteRef) - case reply_to_root(NoteRef) - - var note_ref: NoteRef { - switch self { - case .mention(let mnref): return mnref.ref - case .thread_id(let ref): return ref - case .reply(let ref): return ref - case .reply_to_root(let ref): return ref - } - } - - var is_mention: NoteRef? { - if case .mention(let m) = self { return m.ref } - return nil - } - - var is_direct_reply: NoteRef? { - switch self { - case .mention: - return nil - case .thread_id: - return nil - case .reply(let refid): - return refid - case .reply_to_root(let refid): - return refid - } - } - - var is_thread_id: NoteRef? { - switch self { - case .mention: - return nil - case .thread_id(let referencedId): - return referencedId - case .reply: - return nil - case .reply_to_root(let referencedId): - return referencedId - } - } - - var is_reply: NoteRef? { - switch self { - case .mention: - return nil - case .thread_id: - return nil - case .reply(let refid): - return refid - case .reply_to_root(let refid): - return refid - } - } -} - -func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> { - return blocks.reduce(into: []) { acc, block in - switch block { - case .mention(let m): - if m.ref.key == type, let idx = m.index { - acc.insert(idx) - } - case .relay: - return - case .text: - return - case .hashtag: - return - case .url: - return - case .invoice: - return - } - } -} - -func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] { - if refs.count == 0 { - return [] - } - - if refs.count == 1 { - return [.reply_to_root(refs[0])] - } - - var evrefs: [EventRef] = [] - var first: Bool = true - for ref in refs { - if first { - evrefs.append(.thread_id(ref)) - first = false - } else { - evrefs.append(.reply(ref)) - } - } - return evrefs -} - -func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] { - var mentions: [EventRef] = [] - var ev_refs: [NoteRef] = [] - var i: Int = 0 - - for tag in tags { - if let ref = NoteRef.from_tag(tag: tag) { - if mention_indices.contains(i) { - let mention = Mention<NoteRef>(index: i, ref: ref) - mentions.append(.mention(mention)) - } else { - ev_refs.append(ref) - } - } - i += 1 - } - - var replies = interp_event_refs_without_mentions(ev_refs) - replies.append(contentsOf: mentions) - return replies -} - -func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] { - if tags.count == 0 { - return [] - } - - /// build a set of indices for each event mention - let mention_indices = build_mention_indices(blocks, type: .e) - - /// simpler case with no mentions - if mention_indices.count == 0 { - return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags)) - } - - return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices) -} - - diff --git a/damus/NIP10/ThreadReply.swift b/damus/NIP10/ThreadReply.swift @@ -10,54 +10,23 @@ import Foundation struct ThreadReply { let root: NoteRef - let reply: NoteRef? + let reply: NoteRef let mention: Mention<NoteRef>? var is_reply_to_root: Bool { - guard let reply else { - // if we have no reply and only root then this is reply-to-root, - // but it should never really be in this form... - return true - } - return root.id == reply.id } - init(root: NoteRef, reply: NoteRef?, mention: Mention<NoteRef>?) { + init(root: NoteRef, reply: NoteRef, mention: Mention<NoteRef>?) { self.root = root self.reply = reply self.mention = mention } - init?(event_refs: [EventRef]) { - var root: NoteRef? = nil - var reply: NoteRef? = nil - var mention: Mention<NoteRef>? = nil - - for evref in event_refs { - switch evref { - case .mention(let m): - mention = m - case .thread_id(let r): - root = r - case .reply(let r): - reply = r - case .reply_to_root(let r): - root = r - reply = r - } - } - - // reply with no root should be considered reply-to-root - if root == nil && reply != nil { - root = reply - } - - // nip10 threads must have a root - guard let root else { + init?(tags: TagsSequence) { + guard let tr = interpret_event_refs_ndb(tags: tags) else { return nil } - - self = ThreadReply(root: root, reply: reply, mention: mention) + self = tr } } diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -169,7 +169,7 @@ class EventCache { var ev = event while true { - guard let direct_reply = ev.direct_replies(keypair), + guard let direct_reply = ev.direct_replies(), let next_ev = lookup(direct_reply), next_ev != ev else { break @@ -183,7 +183,7 @@ class EventCache { } func add_replies(ev: NostrEvent, keypair: Keypair) { - if let reply = ev.direct_replies(keypair) { + if let reply = ev.direct_replies() { replies.add(id: reply, reply_id: ev.id) } } diff --git a/damus/Util/ReplyCounter.swift b/damus/Util/ReplyCounter.swift @@ -39,7 +39,7 @@ class ReplyCounter { counted.insert(event.id) - if let reply = event.direct_replies(keypair) { + if let reply = event.direct_replies() { if event.pubkey == our_pubkey { self.our_replies[reply] = event } diff --git a/damus/Views/Events/Components/ReplyPart.swift b/damus/Views/Events/Components/ReplyPart.swift @@ -15,7 +15,7 @@ struct ReplyPart: View { var body: some View { Group { - if let reply_ref = event.thread_reply(keypair)?.reply { + if let reply_ref = event.thread_reply()?.reply { ReplyDescription(event: event, replying_to: events.lookup(reply_ref.note_id), ndb: ndb) } else { EmptyView() diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift @@ -111,7 +111,7 @@ struct MenuItems: View { if event.known_kind != .dm { MuteDurationMenu { duration in if let full_keypair = self.damus_state.keypair.to_full(), - let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) { + let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) { damus_state.mutelist_manager.set_mutelist(new_mutelist_ev) damus_state.postbox.send(new_mutelist_ev) } diff --git a/damus/Views/Events/SelectedEventView.swift b/damus/Views/Events/SelectedEventView.swift @@ -40,7 +40,7 @@ struct SelectedEventView: View { .minimumScaleFactor(0.75) .lineLimit(1) - if let reply_ref = event.thread_reply(damus.keypair)?.reply { + if let reply_ref = event.thread_reply()?.reply { ReplyDescription(event: event, replying_to: damus.events.lookup(reply_ref.note_id), ndb: damus.ndb) .padding(.horizontal) } diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -616,7 +616,7 @@ private func isAlphanumeric(_ char: Character) -> Bool { } func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] { - guard let nip10 = replying_to.thread_reply(keypair) else { + guard let nip10 = replying_to.thread_reply() else { // we're replying to a post that isn't in a thread, // just add a single reply-to-root tag return [["e", replying_to.id.hex(), "", "root"]] @@ -629,14 +629,6 @@ func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] { ["e", replying_to.id.hex(), "", "reply"] ] - // we also add the parent's nip10 reply tag as an additional e tag for context - /* this is incorrect for deprecated nip 10, let's just not add it for now - if let reply = nip10.reply { - tags.append(["e", reply.note_id.hex(), reply.relay ?? ""]) - } - */ - - return tags } diff --git a/damusTests/NIP10Tests.swift b/damusTests/NIP10Tests.swift @@ -26,36 +26,55 @@ final class NIP10Tests: XCTestCase { // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. } + func test_root_with_mention_nip10() { + let root_id_hex = "a32d70d331f4bea7a859ac71d85a9b4e0c2d1fa9aaf7237a17f85a6227f52fdb" + let root_id = NoteId(hex: root_id_hex)! + let mention_hex = "e47b7e156acec6881c89a53f1a9e349a982024245e2c398f8a5b4973b7a89ab3" + let mention_id = NoteId(hex: mention_hex)! + + let tags = + [["e", root_id_hex,"","root"], + ["e", mention_hex,"","mention"], + ["p","c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0"], + ["p","604e96e099936a104883958b040b47672e0f048c98ac793f37ffe4c720279eb2"], + ["p","ffd375eb40eb486656a028edbc83825f58ff0d5c4a1ba22fe7745d284529ed08","","mention"], + ["q","e47b7e156acec6881c89a53f1a9e349a982024245e2c398f8a5b4973b7a89ab3"] + ] + + let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! + let thread = ThreadReply(tags: note.tags) + + XCTAssertNotNil(thread) + guard let thread else { return } + + XCTAssertEqual(thread.root.note_id, root_id) + XCTAssertEqual(thread.reply.note_id, root_id) + XCTAssertEqual(thread.mention?.ref.note_id, mention_id) + } + func test_new_nip10() { let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52" let direct_reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d51" let reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d53" + let mention_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d54" let tags = [ + ["e", mention_hex, "", "mention"], ["e", direct_reply_hex, "", "reply"], ["e", root_note_id_hex, "", "root"], ["e", reply_hex, "", "reply"], - ["e", "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d54", "", "mention"], ] let root_note_id = NoteId(hex: root_note_id_hex)! - let direct_reply_id = NoteId(hex: direct_reply_hex)! let reply_id = NoteId(hex: reply_hex)! + let mention_id = NoteId(hex: mention_hex)! let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! - let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } - }), [root_note_id]) + let tr = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } - }), [direct_reply_id, reply_id]) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_reply?.note_id { xs.append(note_id) } - }), [direct_reply_id, reply_id]) + XCTAssertEqual(tr?.root.note_id, root_note_id) + XCTAssertEqual(tr?.reply.note_id, reply_id) + XCTAssertEqual(tr?.mention?.ref.note_id, mention_id) } func test_repost_root() { @@ -66,19 +85,9 @@ final class NIP10Tests: XCTestCase { let mention_id = NoteId(hex: mention_hex)! let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! - let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } - }), []) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } - }), []) + let tr = note.thread_reply() - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_reply?.note_id { xs.append(note_id) } - }), []) + XCTAssertNil(tr) } func test_direct_reply_old_nip10() { @@ -90,19 +99,14 @@ final class NIP10Tests: XCTestCase { let root_note_id = NoteId(hex: root_note_id_hex)! let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! - let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) + let tr = note.thread_reply() - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } - }), [root_note_id]) + XCTAssertNotNil(tr) + guard let tr else { return } - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } - }), [root_note_id]) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_reply?.note_id { xs.append(note_id) } - }), [root_note_id]) + XCTAssertEqual(tr.root.note_id, root_note_id) + XCTAssertEqual(tr.reply.note_id, root_note_id) + XCTAssertEqual(tr.is_reply_to_root, true) } func test_direct_reply_new_nip10() { @@ -114,24 +118,14 @@ final class NIP10Tests: XCTestCase { let root_note_id = NoteId(hex: root_note_id_hex)! let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! - let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } - }), [root_note_id]) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } - }), [root_note_id]) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_reply?.note_id { xs.append(note_id) } - }), [root_note_id]) - - let nip10 = note.thread_reply(test_keypair)! - XCTAssertEqual(nip10.is_reply_to_root, true) - XCTAssertEqual(nip10.root.note_id, root_note_id) - XCTAssertEqual(nip10.reply!.note_id, root_note_id) + let tr = note.thread_reply() + XCTAssertNotNil(tr) + guard let tr else { return } + + XCTAssertEqual(tr.root.note_id, root_note_id) + XCTAssertEqual(tr.reply.note_id, root_note_id) + XCTAssertNil(tr.mention) + XCTAssertEqual(tr.is_reply_to_root, true) } // seen in the wild by the gleasonator @@ -143,13 +137,14 @@ final class NIP10Tests: XCTestCase { let root_note_id = NoteId(hex: root_note_id_hex)! let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! - let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) - let thread_reply = ThreadReply(event_refs: refs)! - - XCTAssertEqual(thread_reply.mention, nil) - XCTAssertEqual(thread_reply.root.note_id, root_note_id) - XCTAssertEqual(thread_reply.reply!.note_id, root_note_id) - XCTAssertEqual(thread_reply.is_reply_to_root, true) + let tr = note.thread_reply() + XCTAssertNotNil(tr) + guard let tr else { return } + + XCTAssertNil(tr.mention) + XCTAssertEqual(tr.root.note_id, root_note_id) + XCTAssertEqual(tr.reply.note_id, root_note_id) + XCTAssertEqual(tr.is_reply_to_root, true) } func test_marker_reply() { @@ -193,7 +188,6 @@ final class NIP10Tests: XCTestCase { [ ["e", root_hex, "wss://nostr.mutinywallet.com/", "root"], ["e", replying_to_hex, "", "reply"], - //["e", last_reply_hex, "wss://relay.nostrplebs.com"], ["p", "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e"], ["p", "6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e"], ]) @@ -218,16 +212,13 @@ final class NIP10Tests: XCTestCase { let reply_id = NoteId(hex: reply_hex)! let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! - let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } - }), [root_note_id]) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_reply?.note_id { xs.append(note_id) } - }), [reply_id]) + let tr = note.thread_reply() + XCTAssertNotNil(tr) + guard let tr else { return } + XCTAssertEqual(tr.root.note_id, root_note_id) + XCTAssertEqual(tr.reply.note_id, reply_id) + XCTAssertEqual(tr.is_reply_to_root, false) } func test_deprecated_nip10() { @@ -245,19 +236,13 @@ final class NIP10Tests: XCTestCase { let reply_id = NoteId(hex: reply_hex)! let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)! - let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_thread_id?.note_id { xs.append(note_id) } - }), [root_note_id]) - - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) } - }), [direct_reply_id, reply_id]) + let tr = note.thread_reply() + XCTAssertNotNil(tr) + guard let tr else { return } - XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in - if let note_id = r.is_reply?.note_id { xs.append(note_id) } - }), [direct_reply_id, reply_id]) + XCTAssertEqual(tr.root.note_id, root_note_id) + XCTAssertEqual(tr.reply.note_id, reply_id) + XCTAssertEqual(tr.is_reply_to_root, false) } diff --git a/damusTests/ReplyTests.swift b/damusTests/ReplyTests.swift @@ -18,24 +18,6 @@ class ReplyTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func testMentionIsntReply() throws { - let evid = NoteId(hex: "4090a9017a2beac3f17795d1aafb80d9f2b9eda97e4738501082ed5c927be014")! - let content = "this is #[0] a mention" - let tags = [evid.tag] - let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)! - let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks - let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags) - - XCTAssertEqual(event_refs.count, 1) - - let ref = event_refs[0] - - XCTAssertNil(ref.is_reply) - XCTAssertNil(ref.is_thread_id) - XCTAssertNil(ref.is_direct_reply) - XCTAssertEqual(ref.is_mention, .some(.init(note_id: evid))) - } - func testAtAtEnd() { let content = "what @" let blocks = parse_post_blocks(content: content) @@ -70,49 +52,20 @@ class ReplyTests: XCTestCase { XCTAssertEqual(blocks[2].asHashtag, "nope") } - func testRootReplyWithMention() throws { - let content = "this is #[1] a mention" - let thread_id = NoteId(hex: "c75e5cbafbefd5de2275f831c2a2386ea05ec5e5a78a5ccf60d467582db48945")! - let mentioned_id = NoteId(hex: "5a534797e8cd3b9f4c1cf63e20e48bd0e8bd7f8c4d6353fbd576df000f6f54d3")! - let tags = [thread_id.tag, mentioned_id.tag] - let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)! - let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks - let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags) - - XCTAssertEqual(event_refs.count, 2) - XCTAssertNotNil(event_refs[0].is_reply) - XCTAssertNotNil(event_refs[0].is_thread_id) - XCTAssertNotNil(event_refs[0].is_reply) - XCTAssertNotNil(event_refs[0].is_direct_reply) - XCTAssertEqual(event_refs[0].is_reply, .some(NoteRef(note_id: thread_id))) - XCTAssertEqual(event_refs[0].is_thread_id, .some(NoteRef(note_id: thread_id))) - XCTAssertNotNil(event_refs[1].is_mention) - XCTAssertEqual(event_refs[1].is_mention, .some(NoteRef(note_id: mentioned_id))) - } - func testEmptyMention() throws { let content = "this is some & content" let ev = NostrEvent(content: content, keypair: test_keypair, tags: [])! let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks let post_blocks = parse_post_blocks(content: content) let post_tags = make_post_tags(post_blocks: post_blocks, tags: []) - let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags) + let tr = interpret_event_refs(tags: ev.tags) - XCTAssertEqual(event_refs.count, 0) + XCTAssertNil(tr) XCTAssertEqual(post_tags.blocks.count, 1) XCTAssertEqual(post_tags.tags.count, 0) XCTAssertEqual(post_blocks.count, 1) } - func testManyMentions() throws { - let content = "#[10]" - let tags: [[String]] = [[],[],[],[],[],[],[],[],[],[],["p", "3e999f94e2cb34ef44a64b351141ac4e51b5121b2d31aed4a6c84602a1144692"]] - let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)! - let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks - let mentions = blocks.filter { $0.asMention != nil } - XCTAssertEqual(mentions.count, 1) - } - func testNewlineMentions() throws { let bech32_pk = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s" let pk = bech32_pubkey_decode(bech32_pk)! @@ -145,17 +98,12 @@ class ReplyTests: XCTestCase { let reply_id = NoteId(hex: "80093e9bdb495728f54cda2bad4aed096877189552b3d41264e73b9a9595be22")! let tags = [thread_id.tag, reply_id.tag] let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)! - let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks - let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags) + let tr = interpret_event_refs(tags: ev.tags) + XCTAssertNotNil(tr) + guard let tr else { return } - XCTAssertEqual(event_refs.count, 2) - let r1 = event_refs[0] - let r2 = event_refs[1] - - XCTAssertEqual(r1.is_thread_id, .some(.note_id(thread_id))) - XCTAssertEqual(r2.is_reply, .some(.note_id(reply_id))) - XCTAssertEqual(r2.is_direct_reply, .some(.note_id(reply_id))) - XCTAssertNil(r1.is_direct_reply) + XCTAssertEqual(tr.root.note_id, thread_id) + XCTAssertEqual(tr.reply.note_id, reply_id) } func testRootReply() throws { @@ -163,16 +111,14 @@ class ReplyTests: XCTestCase { let thread_id = NoteId(hex: "53f60f5114c06f069ffe9da2bc033e533d09cae44d37a8462154a663771a4ce6")! let tags = [thread_id.tag] let ev = NostrEvent(content: content, keypair: test_keypair, tags: tags)! - let blocks = parse_note_content(content: .content(ev.content,nil)).blocks - let event_refs = interpret_event_refs(blocks: blocks, tags: ev.tags) + let tr = interpret_event_refs(tags: ev.tags) - XCTAssertEqual(event_refs.count, 1) - let r = event_refs[0] - - XCTAssertEqual(r.is_direct_reply, .some(.note_id(thread_id))) - XCTAssertEqual(r.is_reply, .some(.note_id(thread_id))) - XCTAssertEqual(r.is_thread_id, .some(.note_id(thread_id))) - XCTAssertNil(r.is_mention) + XCTAssertNotNil(tr) + guard let tr else { return } + + XCTAssertEqual(tr.root.note_id, thread_id) + XCTAssertEqual(tr.reply.note_id, thread_id) + XCTAssertNil(tr.mention) } func testAdjacentComposedMention() throws { @@ -262,28 +208,6 @@ class ReplyTests: XCTestCase { XCTAssertEqual(new_post.string, "cc @jb55 ") } - func testNoReply() throws { - let content = "this is a #[0] reply" - let ev = NostrEvent(content: content, keypair: test_keypair, tags: [])! - let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks - let event_refs = interpret_event_refs(blocks: blocks, tags:ev.tags) - - XCTAssertEqual(event_refs.count, 0) - } - - func testParseMention() throws { - let note_id = NoteId(hex: "53f60f5114c06f069ffe9da2bc033e533d09cae44d37a8462154a663771a4ce6")! - let tags = [note_id.tag] - let ev = NostrEvent(content: "this is #[0] a mention", keypair: test_keypair, tags: tags)! - let parsed = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks - - XCTAssertNotNil(parsed) - XCTAssertEqual(parsed.count, 3) - XCTAssertEqual(parsed[0].asText, "this is ") - XCTAssertNotNil(parsed[1].asMention) - XCTAssertEqual(parsed[2].asText, " a mention") - } - func testEmptyPostReference() throws { let parsed = parse_post_blocks(content: "") XCTAssertEqual(parsed.count, 0) @@ -442,14 +366,4 @@ class ReplyTests: XCTestCase { XCTAssertEqual(t2, " event mention") } - func testParseInvalidMention() throws { - let parsed = parse_note_content(content: .content("this is #[0] a mention",nil)).blocks - - XCTAssertNotNil(parsed) - XCTAssertEqual(parsed.count, 3) - XCTAssertEqual(parsed[0].asText, "this is ") - XCTAssertEqual(parsed[1].asText, "#[0]") - XCTAssertEqual(parsed[2].asText, " a mention") - } - } diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift @@ -340,8 +340,8 @@ extension NdbNote { References<RefId>(tags: self.tags) } - func thread_reply(_ keypair: Keypair) -> ThreadReply? { - ThreadReply(event_refs: interpret_event_refs_ndb(blocks: self.blocks(keypair).blocks, tags: self.tags)) + func thread_reply() -> ThreadReply? { + ThreadReply(tags: self.tags) } func get_content(_ keypair: Keypair) -> String { @@ -387,13 +387,13 @@ extension NdbNote { return dec } - public func direct_replies(_ keypair: Keypair) -> NoteId? { - return thread_reply(keypair)?.reply?.note_id + public func direct_replies() -> NoteId? { + return thread_reply()?.reply.note_id } // NDBTODO: just use Id - public func thread_id(keypair: Keypair) -> NoteId { - guard let root = self.thread_reply(keypair)?.root else { + public func thread_id() -> NoteId { + guard let root = self.thread_reply()?.root else { return self.id } @@ -421,8 +421,8 @@ extension NdbNote { } */ - func is_reply(_ keypair: Keypair) -> Bool { - return thread_reply(keypair)?.reply != nil + func is_reply() -> Bool { + return thread_reply() != nil } func note_language(_ keypair: Keypair) -> String? { diff --git a/nostrdb/Test/NdbTests.swift b/nostrdb/Test/NdbTests.swift @@ -202,52 +202,6 @@ final class NdbTests: XCTestCase { return opts } - func test_perf_interp_evrefs_old() { - guard let event = decode_nostr_event_json(test_reply_json) else { - return - } - self.measure(options: longer_iter()) { - let blocks = event.blocks(test_keypair).blocks - let xs = interpret_event_refs(blocks: blocks, tags: event.tags) - XCTAssertEqual(xs.count, 1) - } - } - - func test_perf_interp_evrefs_ndb() { - guard let note = NdbNote.owned_from_json(json: test_reply_json) else { - return - } - self.measure(options: longer_iter()) { - let blocks = note.blocks(test_keypair).blocks - let xs = interpret_event_refs_ndb(blocks: blocks, tags: note.tags) - XCTAssertEqual(xs.count, 1) - } - } - - func test_decoded_events_are_equal() { - let event = decode_nostr_event_json(test_reply_json) - let note = NdbNote.owned_from_json(json: test_reply_json) - - XCTAssertNotNil(note) - XCTAssertNotNil(event) - guard let note else { return } - guard let event else { return } - - XCTAssertEqual(note.content_len, UInt32(event.content.utf8.count)) - XCTAssertEqual(note.pubkey, event.pubkey) - XCTAssertEqual(note.id, event.id) - - let ev_blocks = event.blocks(test_keypair) - let note_blocks = note.blocks(test_keypair) - - XCTAssertEqual(ev_blocks, note_blocks) - - let event_refs = interpret_event_refs(blocks: ev_blocks.blocks, tags: event.tags) - let note_refs = interpret_event_refs_ndb(blocks: note_blocks.blocks, tags: note.tags) - - XCTAssertEqual(event_refs, note_refs) - } - func test_iteration_perf() throws { guard let note = NdbNote.owned_from_json(json: test_contact_list_json) else { XCTAssert(false)