Skip to content

Conversation

@alltheseas
Copy link
Collaborator

@alltheseas alltheseas commented Dec 28, 2025

User Benefit

Reduce the chance that notes are not found.

Summary

Implements relay hint consumption from NIP-10/NIP-18/NIP-19 for event fetching. When users view content that references events on other relays (quotes, reposts, mentions, replies, links), Damus extracts relay hints from tags and bech32 TLV data and uses them to fetch events from relays not in the user's relay pool.

This enables users to view notes and profiles even when the content only exists on relays they don't follow.

Changes

  1. Core relay hints (804e40f): Parse relay hints from bech32 entities in URLHandler, pass through SearchType/NoteReference enums, add ensureConnected() to RelayPool for on-demand ephemeral relay connections
  2. Ephemeral cleanup (ebd006b): Disconnect ephemeral relays immediately after lookup completes to prevent pool growth
  3. Early return optimization (89eb631): Return early when hinted relay is already connected, reducing latency for repeated hints
  4. Reaction relay hints (93447ce): Pass relay hints through when loading referenced notes from reactions
  5. Debug logging (cd5512e): Add DEBUG-only logging for REQ/EVENT/EOSE on ephemeral relays
  6. Correctness fixes (e1d717c):
    - Grace period: Wait 300ms after first relay connects to let others connect (improves multi-hint success)
    - Ref-counted cleanup: Use acquire/release leasing to prevent race conditions during concurrent lookups
  7. Documentation (4a97265): Add comprehensive docstrings to relay hint functions and related code
  8. Tag relay hint extraction (2dcf30a): Add TagSequence.relayHints to extract relay URLs from position 2 of e, p, q, and a tags per NIP-10
  9. EventLoaderView relay hints (3bbe41e): Accept relayHints parameter to fetch events from hinted relays when not in cache
  10. MentionRef relay hints (b1bc0da): Preserve nevent/nprofile relay hints in MentionRef.from_tag for inline embeds
  11. Repost relay hints (baf18f5): Use e tag relay hints when loading kind 6 repost targets via repostTarget()
  12. Mention rendering hints (19ed5a4): Pass relay hints through EventShell, ChatEventView, SelectedEventView, DMView for embedded note fetching
  13. Race condition fix (3478153): Fix ephemeral relay lease release - decrement before await to prevent re-acquisition during suspension
  14. Grace period fix (d3cbb8d): Enforce grace period when relays already connected
  15. Quote relay hints (4e4aa60): Add QuoteRef struct to parse q tags with relay hints; update first_eref_mention_with_hints() to fall back to q tags for quote reposts (NIP-10/NIP-18)
  16. Reply relay hints (258e3f7): Add direct_reply_ref() returning NoteRef with relay hint; update ReplyQuoteView to use EventLoaderView when reply not in cache
  17. Documentation cleanup (79731e9): Correct NIP references (NIP-10/NIP-18 for q tags); document that addressable event quotes are not yet supported

Limitations

  • q tags with event addresses (::) are not yet supported; only hex event IDs are parsed

Checklist

Standard PR Checklist

  • I have read (or I am familiar with) the ../docs/CONTRIBUTING.md
  • I have tested the changes in this PR
  • I have profiled the changes to ensure there are no performance regressions, or I do not need to profile the changes.
    • If not needed, provide reason: Changes only affect relay hint lookups (user-initiated via link clicks), no hot paths affected
  • I have opened or referred to an existing github issue related to this change:Consume relay hints NIP-19 #1147
  • My PR is either small, or I have split it into smaller logical commits that are easier to review
  • I have added the signoff line to all my commits. See ../docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin
  • I have added appropriate changelog entries for the changes in this PR. See ../docs/CONTRIBUTING.md#add-changelog-changed-changelog-fixed-etc
  • I have added appropriate Closes: or Fixes: tags in the commit messages wherever applicable, or made sure those are not needed. See https://github.com/damus-io/damus/blob/master/docs/CONTRIBUTING.md#submitting-patches

Test report

Device: iPhone 17 Simulator

iOS: iOS 26

Damus: Branch relay-hints (commit 79731e9)

Setup: Xcode iOS Simulator with console debug logging enabled. Used nak CLI tool to decode nevents and query relays directly.

Steps:

  1. Open nevent with relay hints pointing to relay not in user's pool
  2. Open nprofile with relay hints
  3. Open naddr with relay hints
  4. Open nevent with incorrect relay hint (note doesn't exist on hinted relay)
  5. Verify ephemeral relays are cleaned up after lookup (no pool growth in logs)
  6. Verify debug logging shows REQ sent, EVENT/EOSE received from ephemeral relays
  7. Open reaction (like) with relay hints, verify referenced note loads
  8. View a note that quotes another note from a relay not in your pool → quoted note loads
  9. View a repost of a note from a foreign relay → reposted note loads
  10. View a reply where the parent is on a foreign relay → reply quote preview loads

Results:

  • PASS

###Other notes

Used nak CLI tool for debugging:
nak decode nevent1q...
nak req -i wss://relay.example.com

Closes #1147

Summary by CodeRabbit

  • New Features

    • Broad relay-hint support across the app: references, profiles, events, replies, quotes, search results and loaders now carry relay hints for targeted fetching and async loading.
  • Bug Fixes / Reliability

    • Improved loading with ephemeral-relay lease management, connection hinting, grace/timeout behavior and clear user-facing error sheets for deprecated/unsafe references.
  • Tests

    • Expanded NIP-10 relay-hint and pubkey propagation tests.

✏️ Tip: You can customize this high-level summary in your review settings.

@alltheseas alltheseas requested a review from tyiu December 28, 2025 02:43
@danieldaquino danieldaquino added the pr-in-queue This PR is waiting in a queue behind their other PRs marked with the label `pr-active-review`. label Dec 29, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

📝 Walkthrough

Walkthrough

Adds end‑to‑end NIP‑19/NIP‑10 relay‑hint support: parse and preserve relay hints from tags and URIs, propagate hints through mention extraction and loaders, use ephemeral relay lease acquire/ensureConnected/release for targeted lookups, and return explicit error sheets for deprecated/unsafe NIP‑19 references.

Changes

Cohort / File(s) Summary
Core parsing & tag utilities
damus/Core/Nostr/Mentions.swift, damus/Core/Nostr/Id.swift, nostrdb/NdbTagIterator.swift, nostrdb/NdbNote.swift, nostrdb/NdbNote+.swift
Parse and expose relay hints from tags; add MentionRef.from_tag, TagSequence.relayHint/relayHints, referenced_quote_refs, direct_reply_ref(), and NdbNote.repostTarget() returning (noteId, relayHints).
Event mention extraction
damus/Core/Nostr/NostrEvent.swift
Add NoteMentionWithHints and first_eref_mention_with_hints(...) to locate first event references while preserving relay hints.
Relay pool & lifecycle
damus/Core/Nostr/RelayPool.swift
Add ephemeral lease tracking (ephemeralLeases), acquireEphemeralRelays/releaseEphemeralRelays, ensureConnected(to:timeout:), safer remove_relay, ephemeral-aware add/subscribe/send logging and helpers.
Subscription & targeted lookups
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift
Use ensureConnected results for lookups, acquire/release ephemeral leases around searches, add fetchFromRelays(filter:relays:timeout:), start-time/total-time timeout handling and debug logs.
Event loading & resolution
damus/Features/Events/Models/LoadableNostrEventView.swift, damus/Features/Events/EventLoaderView.swift
Make loaders relay-aware (NoteReference now carries relays), add loadEvent(noteId:relays:), timeout-managed load() task, and pass targetRelays into lookup calls.
Views: mention/event plumbing
damus/Features/Events/MentionView.swift, damus/Features/Events/BuilderEventView.swift, damus/Features/Events/EventShell.swift, damus/Features/Events/EventView.swift, damus/Features/Events/SelectedEventView.swift
Add relayHints properties/initializers, propagate hints into MentionView/BuilderEventView/EventLoaderView, change mention callers to .note(noteId, index) with relayHints, and asynchronously load repost targets via loader when missing.
Chat / DMs / replies
damus/Features/Chat/ChatEventView.swift, damus/Features/Chat/ReplyQuoteView.swift, damus/Features/DMs/Views/DMView.swift
Replace first_eref_mention usages with hint-aware variant, pass relay hints into reply/quote views, add optional relayHint to ReplyQuoteView and load missing events using the hint.
Search & discovery
damus/Features/Search/Views/SearchResultsView.swift, damus/Features/Search/Views/SearchingEventView.swift
Extend search enums/cases to carry relays, parse NIP‑19 tokens into relays, propagate into find_from/to queries, add input guard and debug logs.
URL handling & content entry
damus/Shared/Utilities/URLHandler.swift, damus/ContentView.swift
Parse nevent/nprofile with relays, add ParsedURLInfo.profile_reference(pubkey, relays), route to view actions including relay hints, and map deprecated/unsafe refs to explicit error sheets.
Posting / NIP‑10 tags & IDs
damus/Features/Posting/Views/PostView.swift, damus/Core/Types/Ids/Referenced.swift, damus/Core/Nostr/Id.swift
Propagate pubkey into NIP‑10 reply/root tags and NoteRef tags, extend NoteRef to carry optional pubkey, add QuoteRef supporting relay hints and tag building/parsing.
UI loaders & mention consumers
damus/Features/Events/EventLoaderView.swift, damus/Features/Events/Models/LoadableNostrEventView.swift, damus/Features/Events/MentionView.swift
Ensure downstream components accept and forward relayHints into lookups and loading flows.
Tests & tracker
damusTests/NIP10Tests.swift, .beads/issues.jsonl
Add extensive NIP‑10/relay-hint tests and backlog/tracker entries related to ephemeral leases, hint timeouts, and test coverage.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant UI as EventView/BuilderEventView
    participant Loader as EventLoaderView
    participant Pool as RelayPool
    participant Net as SubscriptionManager

    User->>UI: open mention (noteId + relayHints)
    UI->>Loader: init(noteId, relayHints)
    Loader->>Pool: ensureConnected(relayHints)
    Pool-->>Loader: connectedRelays
    Loader->>Net: findEvent(filter, find_from: connectedRelays)
    Net->>Pool: acquireEphemeralRelays(connectedRelays)
    Net-->>Loader: event / not_found / EOSE
    Net->>Pool: releaseEphemeralRelays(connectedRelays)
    Loader-->>UI: loaded event or not_found
    UI-->>User: render event or fallback
Loading
sequenceDiagram
    autonumber
    participant User
    participant Parser as URL Parser
    participant Handler as DamusURLHandler
    participant Pool as RelayPool
    participant Net as SubscriptionManager
    participant UI as ContentView

    User->>Parser: supply nevent/nprofile URI
    Parser-->>Handler: ParsedURLInfo (includes relays)
    Handler->>Pool: ensureConnected(relays)
    Pool-->>Handler: connectedRelays
    Handler->>Net: lookup/find with connectedRelays
    Net->>Pool: acquireEphemeralRelays(connectedRelays)
    Net-->>Handler: lookup result (event/profile)
    Net->>Pool: releaseEphemeralRelays(connectedRelays)
    Handler->>UI: map to ViewAction (include relayHints or error sheet)
    UI-->>User: navigate / present error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰
I nibble tags and follow whispering trails,
carry relay crumbs in my soft, quick sails.
I lease a door, then close it with a hop—
so hidden notes at last find where to stop.
A twitch of whiskers: discovery complete!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Consume NIP-19 relay hints for event fetching' clearly and specifically describes the main change: parsing and using relay hints from NIP-19 bech32 entities for event fetching.
Description check ✅ Passed The PR description follows the Standard PR Checklist template with comprehensive sections: User Benefit, Summary, detailed Changes breakdown, Limitations, completed Checklist items, thorough Test report with device/setup/steps/results, and appropriate issue references (Closes #1147).
Linked Issues check ✅ Passed The PR implementation fully addresses issue #1147 objectives: parses NIP-19 relay hints (nevent, nprofile, naddr), propagates hints through search/lookup flows, adds ephemeral relay connections via ensureConnected(), implements ref-counted cleanup to prevent pool growth, and preserves hints across reactions, reposts, quotes, and replies.
Out of Scope Changes check ✅ Passed All code changes are scoped to relay hints consumption: URLHandler parsing, SearchType/NoteReference relay propagation, RelayPool ephemeral management, EventLoaderView relay hints, MentionRef/TagSequence relay extraction, repostTarget/direct_reply_ref implementations, and relay hint threading through view layers. No unrelated changes detected.
✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
damus/Core/Nostr/NostrEvent.swift (1)

812-869: Consider consolidating with first_eref_mention to reduce duplication.

The new first_eref_mention_with_hints function (lines 850-869) is structurally identical to first_eref_mention (lines 812-831), differing only in the return type and relay hint inclusion. You could deprecate the original and have callers use the hints-aware version, ignoring hints when not needed.

🔎 Example consolidation
+@available(*, deprecated, renamed: "first_eref_mention_with_hints", message: "Use first_eref_mention_with_hints and ignore relayHints if not needed")
 func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? {
-    return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blockGroup in
-        return blockGroup.forEachBlock({ index, block in
-            switch block {
-            case .mention(let mention):
-                guard let mention = MentionRef(block: mention) else { return .loopContinue }
-                switch mention.nip19 {
-                case .note(let noteId):
-                    return .loopReturn(Mention<NoteId>.note(noteId, index: index))
-                case .nevent(let nEvent):
-                    return .loopReturn(Mention<NoteId>.note(nEvent.noteid, index: index))
-                default:
-                    return .loopContinue
-                }
-            default:
-                return .loopContinue
-            }
-        })
-    })
+    guard let result = first_eref_mention_with_hints(ndb: ndb, ev: ev, keypair: keypair) else {
+        return nil
+    }
+    return Mention<NoteId>.note(result.noteId, index: result.index)
 }
damus/Features/Events/EventLoaderView.swift (1)

19-19: Remove redundant nil initialization.

SwiftLint correctly flags this as redundant. Optional @State properties are implicitly nil.

🔎 Proposed fix
-    @State var loadingTask: Task<Void, Never>? = nil
+    @State var loadingTask: Task<Void, Never>?
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (1)

433-434: Remove redundant nil initialization.

Per SwiftLint: optional variables are implicitly initialized to nil.

🔎 Proposed fix
     func findEvent(query: FindEvent) async -> FoundEvent? {
-        var filter: NostrFilter? = nil
+        var filter: NostrFilter?
         let find_from = query.find_from
         let query = query.type
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 368f94a and 82d5154.

📒 Files selected for processing (19)
  • damus/ContentView.swift
  • damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift
  • damus/Core/Nostr/Mentions.swift
  • damus/Core/Nostr/NostrEvent.swift
  • damus/Core/Nostr/RelayPool.swift
  • damus/Features/Chat/ChatEventView.swift
  • damus/Features/DMs/Views/DMView.swift
  • damus/Features/Events/BuilderEventView.swift
  • damus/Features/Events/EventLoaderView.swift
  • damus/Features/Events/EventShell.swift
  • damus/Features/Events/EventView.swift
  • damus/Features/Events/MentionView.swift
  • damus/Features/Events/Models/LoadableNostrEventView.swift
  • damus/Features/Events/SelectedEventView.swift
  • damus/Features/Search/Views/SearchResultsView.swift
  • damus/Features/Search/Views/SearchingEventView.swift
  • damus/Shared/Utilities/URLHandler.swift
  • nostrdb/NdbNote+.swift
  • nostrdb/NdbTagIterator.swift
🧰 Additional context used
🧬 Code graph analysis (17)
damus/Features/Events/BuilderEventView.swift (1)
damus/ContentView.swift (1)
  • event (985-987)
damus/Features/Events/EventLoaderView.swift (1)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (2)
  • lookup (359-378)
  • lookup (397-427)
damus/Features/Search/Views/SearchingEventView.swift (4)
damus/ContentView.swift (2)
  • event (985-987)
  • profile (981-983)
nostrdb/Ndb.swift (2)
  • query (821-826)
  • query (835-859)
damus/Core/Nostr/Profiles.swift (1)
  • lookup (98-108)
damus/Shared/Utilities/EventCache.swift (1)
  • lookup (220-231)
damus/Features/Events/EventShell.swift (2)
damus/Core/Nostr/NostrEvent.swift (1)
  • first_eref_mention_with_hints (850-869)
damus/Core/Nostr/Mentions.swift (2)
  • note (34-36)
  • note (193-195)
nostrdb/NdbNote+.swift (2)
damus/Features/Highlight/Models/HighlightEvent.swift (1)
  • tags (206-213)
nostrdb/NdbTagElem.swift (2)
  • matches_char (94-96)
  • id (128-131)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (1)
damus/Core/Nostr/RelayPool.swift (3)
  • acquireEphemeralRelays (183-190)
  • ensureConnected (280-363)
  • releaseEphemeralRelays (196-225)
damus/Features/Events/EventView.swift (1)
nostrdb/NdbNote+.swift (1)
  • repostTarget (42-56)
damus/Core/Nostr/Mentions.swift (2)
nostrdb/NdbTagElem.swift (3)
  • data (124-126)
  • id (128-131)
  • string (149-156)
damus/Shared/Utilities/Bech32Object.swift (1)
  • pubkey (105-116)
nostrdb/NdbTagIterator.swift (1)
nostrdb/NdbTagElem.swift (1)
  • string (149-156)
damus/Features/Chat/ChatEventView.swift (1)
damus/Core/Nostr/NostrEvent.swift (1)
  • first_eref_mention_with_hints (850-869)
damus/Features/Events/MentionView.swift (1)
damus/ContentView.swift (1)
  • event (985-987)
damus/ContentView.swift (2)
damus/Core/Nostr/Mentions.swift (4)
  • pubkey (30-32)
  • pubkey (197-199)
  • note (34-36)
  • note (193-195)
damus/Shared/Utilities/Bech32Object.swift (1)
  • pubkey (105-116)
damus/Core/Nostr/NostrEvent.swift (3)
nostrdb/NdbBlock.swift (2)
  • borrowBlockGroup (124-139)
  • forEachBlock (278-281)
nostrdb/UnownedNdbNote.swift (1)
  • borrow (57-70)
damus/Core/Nostr/Mentions.swift (2)
  • note (34-36)
  • note (193-195)
damus/Features/Events/SelectedEventView.swift (2)
damus/Core/Nostr/NostrEvent.swift (1)
  • first_eref_mention_with_hints (850-869)
damus/Core/Nostr/Mentions.swift (2)
  • note (34-36)
  • note (193-195)
damus/Features/DMs/Views/DMView.swift (1)
damus/Core/Nostr/NostrEvent.swift (1)
  • first_eref_mention_with_hints (850-869)
damus/Features/Events/Models/LoadableNostrEventView.swift (2)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (4)
  • findEvent (433-495)
  • query (380-386)
  • lookup (359-378)
  • lookup (397-427)
damus/ContentView.swift (1)
  • event (985-987)
damus/Features/Search/Views/SearchResultsView.swift (1)
damus/ContentView.swift (2)
  • profile (981-983)
  • event (985-987)
🪛 SwiftLint (0.57.0)
damus/Features/Events/EventLoaderView.swift

[Warning] 19-19: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift

[Warning] 434-434: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (41)
nostrdb/NdbTagIterator.swift (1)

45-73: LGTM - Clean relay hint extraction implementation.

The extension correctly extracts relay hints from position 2 of tags per NIP-01/NIP-10 semantics. The guard conditions properly handle missing or empty URLs, and the array wrapper provides good API consistency with NEvent.relays.

damus/Core/Nostr/RelayPool.swift (5)

47-50: LGTM - Ref-counted lease tracking is a sound approach.

The lease counter design prevents race conditions where multiple concurrent lookups might try to clean up the same ephemeral relay.


179-190: LGTM - Lease acquisition is straightforward.

The implementation correctly increments lease counts. The release function validates ephemeral status before removal, so acquiring leases on non-ephemeral URLs is harmless.


196-225: Well-designed race condition handling.

The re-check at line 212 after the await correctly handles the case where another caller re-acquires a lease during the suspension point. This prevents premature removal when the relay is still needed.


280-363: Solid connectivity assurance implementation.

The polling-based wait loop with a 300ms grace period after the first connection is a practical approach for allowing multiple hinted relays to connect. The early return optimization (line 311) and proper handling of already-connected relays improve latency.


242-250: Appropriate debug logging for ephemeral relay troubleshooting.

The #if DEBUG guards ensure no performance impact in release builds. Truncating event strings with prefix(200) prevents excessive log output.

damus/Features/Chat/ChatEventView.swift (1)

160-164: LGTM - Correctly adopts relay-hints-aware mention extraction.

The update properly uses first_eref_mention_with_hints and passes the extracted relayHints to MentionView, enabling relay-aware event loading for referenced notes.

nostrdb/NdbNote+.swift (1)

36-56: LGTM - Correct NIP-18 repost target extraction.

The implementation properly parses the e tag from repost events, extracting both the target note ID and relay hints per NIP-18 specification. Using the relayHints computed property from the earlier TagSequence extension keeps the code clean.

damus/Features/Events/EventView.swift (1)

36-46: LGTM - Graceful fallback for repost loading with relay hints.

The three-tier approach (cache hit → async load with hints → empty fallback) correctly handles reposts. Using EventLoaderView with relay hints enables fetching boosted content from hinted relays when not locally cached.

damus/Features/Events/EventShell.swift (2)

38-44: LGTM - Consistent adoption of relay-hints-aware mentions.

The return type and implementation updates align with the new NoteMentionWithHints pattern used throughout the codebase.


78-80: LGTM - Relay hints correctly passed to MentionView.

Both the Threaded (line 79) and Wide (line 111) layouts now pass mention.relayHints to MentionView, enabling relay-aware fetching for embedded note references.

damus/Core/Nostr/NostrEvent.swift (2)

833-838: LGTM - Clean data structure for relay-aware mentions.

The NoteMentionWithHints struct effectively bundles the note ID, relay hints, and optional index for propagation through the UI layer.


840-869: LGTM - Proper NIP-19 relay hint extraction.

The function correctly differentiates between plain note references (empty hints) and nevent references (with relay hints from nEvent.relays). The implementation mirrors the existing first_eref_mention pattern appropriately.

damus/Core/Nostr/Mentions.swift (1)

125-170: LGTM! Clear implementation of relay hint preservation.

The from_tag method correctly parses tag sequences and preserves relay hints by mapping to the appropriate NIP-19 variants (nprofile, nevent, naddr) when hints are present. The docstring accurately describes the NIP-01/NIP-10 semantics.

damus/Features/Events/MentionView.swift (1)

10-36: LGTM! Clean relay hints propagation.

The relayHints property is correctly added and threaded through to EventLoaderView and BuilderEventView. The default empty array maintains backward compatibility with existing call sites.

damus/Features/Events/EventLoaderView.swift (1)

42-49: LGTM! Correct relay hints integration in subscribe logic.

The conversion of relayHints to nil when empty ensures the lookup falls back to default relay behavior, while non-empty hints are correctly passed through.

damus/Features/Events/BuilderEventView.swift (2)

10-38: LGTM! Well-structured relay hints support.

The two initialization paths are handled appropriately: pre-loaded events set empty hints (not needed), while the ID-based loader accepts and propagates relay hints correctly.


50-59: LGTM! Proper propagation to EventLoaderView.

The relayHints are correctly forwarded to EventLoaderView when loading events by ID.

damus/ContentView.swift (2)

1117-1127: LGTM! Correct relay hints propagation for NOTE and NEVENT.

The mapping correctly passes empty relays for plain NOTE references and propagates nEvent.relays for NEVENT references. The TODO comment at line 1126 appropriately flags the missing nprofile relay hint handling for future improvement.


1128-1148: LGTM! Appropriate error handling for deprecated and unsafe references.

The error sheets for nrelay (deprecated per NIP) and nsec (security risk) provide clear, actionable user guidance.

damus/Shared/Utilities/URLHandler.swift (3)

93-101: Good catch: Direct parsing preserves relay hints.

The comment at line 94 clearly explains why nevent and nprofile are parsed directly before falling through to decode_nostr_uri — the latter discards relay hints. This is a correct approach.


135-145: LGTM! ParsedURLInfo extended correctly.

The new profile_reference case properly carries the pubkey and relay hints tuple, completing the URL parsing enhancement.


34-39: Document the background prefetch intent for profile_reference with relay hints.

The Task { ... } at lines 36-38 initiates a fire-and-forget network lookup from the relay hints in the nprofile URL. The result is discarded and the route is returned immediately, so the profile data may not arrive before the view loads. While this appears intentional—to prefetch from the specified relays before the user navigates to the profile—add a comment explaining this behavior. If the view cannot gracefully handle initially missing profile data, consider awaiting the result or implementing a synchronous load strategy instead.

damus/Features/Events/SelectedEventView.swift (1)

83-90: LGTM! Correctly adopts relay hints for mentions.

The update properly uses first_eref_mention_with_hints and constructs the MentionView with the structured .note(noteId, index:) mention and propagates relayHints. This is consistent with similar changes across ChatEventView, DMView, and EventShell.

damus/Features/Search/Views/SearchingEventView.swift (5)

17-22: LGTM! Enum updated to carry relay hints.

The SearchType enum now correctly carries associated relay URLs for event and profile cases, enabling relay-aware lookups downstream.


44-51: Good documentation for handle_search.

The docstring clearly describes the state transitions and possible outcomes.


87-96: Correct relay hint propagation for event lookup.

The pattern of converting empty arrays to nil for targetRelays is consistent with the FindEvent.event(evid:find_from:) API which expects nil for unrestricted lookup.


97-106: Consistent relay handling for profile lookup.

Same pattern applied correctly for profile lookups.


107-116: NAddr lookup correctly uses naddr.relays.

The naddr case appropriately extracts relays from the NAddr struct and passes them to the lookup.

damus/Features/Search/Views/SearchResultsView.swift (3)

103-115: LGTM! Relay hints correctly propagated to search views.

The implementation properly:

  • Passes empty relay arrays for cases without hints (.profile, .hex, .note)
  • Propagates nevent.relays and nprofile.relays for bech32 objects that include hints

201-206: Good documentation for search_for_string.

The docstring clearly describes the function's purpose and return values.


240-245: Debug logging appropriately scoped.

The #if DEBUG wrapper ensures this logging doesn't affect release builds.

damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (3)

391-427: LGTM! Ephemeral relay handling for NAddr lookups.

The implementation correctly:

  1. Acquires ephemeral relay leases before connecting
  2. Attempts connection and uses only successfully connected relays
  3. Defers release in a detached Task to avoid blocking
  4. Falls back to unrestricted lookup if no hints connect

The defer-based cleanup with Task { await ... } is appropriate for async cleanup without blocking the current flow.


429-432: Good documentation for findEvent.

The docstring clearly describes the relay hint behavior and return semantics.


459-475: Consistent ephemeral relay pattern applied to findEvent.

The same acquire/connect/defer-release pattern is correctly applied here, matching lookup(naddr:).

damus/Features/DMs/Views/DMView.swift (1)

18-26: LGTM! Relay hints propagated through DM mention handling.

The update correctly uses first_eref_mention_with_hints and passes mention.relayHints to BuilderEventView, enabling relay-aware fetching of mentioned notes in DMs.

damus/Features/Events/Models/LoadableNostrEventView.swift (5)

34-52: Consider checking cancellation within loadTask to ensure timeout is respected.

The timeout mechanism cancels loadTask, but executeLoadingLogic doesn't check for cancellation. Since the task body runs to completion regardless of cancellation signal, if the timeout fires while loading is in progress, the loading result may still overwrite .not_found after the timeout sets it.

Since both tasks run on @MainActor, operations are serialized, but the sequence could be:

  1. Timeout fires → sets state to .not_found
  2. loadTask completes → sets state to the loaded result

This might actually be acceptable behavior (successful load overrides timeout), but it makes the 10-second "give up" semantics inconsistent. If strict timeout semantics are desired, check Task.isCancelled before setting state in loadTask.


54-63: LGTM! Relay-aware event loading helper.

Correctly converts empty relay array to nil for unrestricted lookup, consistent with the pattern used elsewhere.


89-92: Good: relay hints propagated for reaction lookups.

Passing the same relay hints when following a .like reaction to its referenced note is sensible—the referenced note is likely on the same relay as the reaction.


99-102: Correct relay handling for NAddr lookups.

The empty-to-nil conversion is consistent with other relay hint handling in the codebase.


113-116: LGTM! NoteReference enum extended with relay hints.

The enum now carries relay hints for note_id references, enabling relay-aware lookups throughout the view model.

alltheseas and others added 15 commits January 5, 2026 12:49
Extract relay hints from nevent, nprofile, and naddr bech32 entities
and use them to fetch events from hinted relays not in the user's
relay pool.

Changes:
- Parse relay hints directly from bech32 TLV data in URLHandler
- Pass relay hints through SearchType and NoteReference enums
- Add ensureConnected() to RelayPool for on-demand relay connections
- Connect to hinted relays as ephemeral, return early when first connects
- Use connected hinted relays for event streaming

Changelog-Added: Added relay hint support for nevent, nprofile, and naddr links

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

Closes damus-io#1147

Signed-off-by: alltheseas
Ephemeral relays (added for relay hint lookups) are now disconnected
and removed from the pool as soon as the findEvent or lookup(naddr:)
call completes. This prevents unbounded growth of the relay pool
during long sessions with many hinted lookups.

- Add removeEphemeralRelays() method to RelayPool
- Call cleanup in defer blocks in findEvent and lookup(naddr:)
- Update ensureConnected documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

Closes damus-io#1147

Signed-off-by: alltheseas
When ensureConnected() is called with relay hints and at least one
hinted relay is already connected, return immediately instead of
waiting for additional relays to connect. The other relays are
connected in the background for potential future use.

This reduces latency when the same relay hint is used multiple times.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

Closes damus-io#1147

Signed-off-by: alltheseas
When loading a reaction (like) event via relay hints, pass the same
hints when fetching the referenced note. The referenced note is
likely on the same relay as the reaction itself.

Previously relay hints were dropped (relays: []) for the second hop,
causing lookups to fall back to default relays.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

Closes damus-io#1147

Signed-off-by: alltheseas
Add DEBUG-only logging to help diagnose relay hint issues:

- Log EVENT/EOSE messages received from ephemeral relays
- Log REQ messages sent to ephemeral relays
- Log subscription target relays and filter IDs
- Log getRelays mismatches (requested vs found)
- Log parsed nevent note ID and relay hints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

Closes damus-io#1147

Signed-off-by: alltheseas
1. Grace period for additional relays: When some hinted relays are
   already connected, wait a short grace period (300ms) for others
   to connect instead of returning immediately. This prevents missing
   events on relays that connect quickly.

2. Ref-counted ephemeral relay cleanup: Use acquire/release leases
   to track ephemeral relay usage. Only remove a relay when the last
   lookup using it completes. This prevents race conditions where
   concurrent lookups could have their relay removed mid-query.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

Closes damus-io#1147

Signed-off-by: alltheseas
Docstrings generation was requested by @alltheseas.

* #8 (comment)

The following files were modified:

* `damus/ContentView.swift`
* `damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift`
* `damus/Core/Nostr/RelayPool.swift`
* `damus/Features/Events/Models/LoadableNostrEventView.swift`
* `damus/Features/Search/Views/SearchResultsView.swift`
* `damus/Features/Search/Views/SearchingEventView.swift`
* `damus/Shared/Utilities/URLHandler.swift`
Two doc comments had em-dashes (—) instead of triple-slashes (///),
causing Swift compilation errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Per NIP-01/NIP-10, tag position 2 contains an optional relay URL hint.
Add relayHint and relayHints properties to TagSequence for extracting
these hints from e, p, q, and a tags.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add relayHints parameter to EventLoaderView, allowing callers to specify
relay URLs where the target event may be found. When hints are provided,
the lookup targets those relays specifically.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Update MentionRef.from_tag to use nevent/nprofile/naddr variants when
relay hints are present in the tag, rather than simple note/npub forms.
This preserves relay URL information for later use in event fetching.

Per NIP-01/NIP-10, position 2 in e, p, and a tags contains optional
relay hints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add repostTarget() helper to NdbNote that extracts both the event ID
and relay hints from a repost's e tag.

Update EventView to use EventLoaderView with relay hints when the
inner reposted event is not in cache, per NIP-18.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add first_eref_mention_with_hints() that preserves relay hints from
nevent bech32 entities in note content.

Update all call sites (EventShell, ChatEventView, SelectedEventView,
DMView) to use the new function and pass relay hints to MentionView
and BuilderEventView.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Decrement lease count immediately before any suspension point to ensure
atomicity. After await, only remove relay if lease is still nil (not
re-acquired). This prevents the 1→2→1 cycle from incorrectly removing
an active relay.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Move grace deadline check outside anyConnected condition so it's
evaluated every iteration. Previously, if alreadyConnected was not
empty but none of the toConnect relays connected, the grace deadline
was set but never checked, causing a full 2-second timeout instead
of the expected 300ms grace period.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
damus/Features/Events/Models/LoadableNostrEventView.swift (1)

37-52: Potential race condition in timeout handling.

The timeout implementation has a subtle issue:

  1. loadTask.cancel() on line 46 only sets a cancellation flag—it doesn't interrupt the running task.
  2. await loadTask.value on line 50 waits for loadTask to complete regardless of cancellation.
  3. If timeout fires first, self.state = .not_found is set (line 47), but then loadTask may complete and executeLoadingLogic returns, causing line 40 to overwrite state with the loaded result.

To fix, check for cancellation inside executeLoadingLogic or use a separate @Published guard to ignore late results:

🔎 Suggested fix
     func load() async {
-        // Start the loading process in a separate task to manage the timeout independently.
-        let loadTask = Task { @MainActor in
-            self.state = await executeLoadingLogic(note_reference: self.note_reference)
+        let loadTask = Task { @MainActor in
+            let result = await executeLoadingLogic(note_reference: self.note_reference)
+            // Only update state if not cancelled (timeout didn't fire)
+            if !Task.isCancelled {
+                self.state = result
+            }
         }
🧹 Nitpick comments (3)
damus/Core/Nostr/RelayPool.swift (1)

315-345: Grace period initialization logic may not match intent.

The grace period is initialized when alreadyConnected.isEmpty is false (line 317), meaning if there are already-connected relays, the grace period starts immediately. However, the comment on line 337 says "Start grace period on first connection" for relays in toConnect. This creates inconsistent behavior:

  • If all hinted relays are already connected → grace period starts immediately → exits after 300ms even though no new connections were needed.
  • If some relays need connecting → grace period only starts when first one connects.

If the intent is to always give newly connecting relays a 300ms window, consider initializing graceDeadline to nil unconditionally:

🔎 Suggested fix
-        var graceDeadline: ContinuousClock.Instant? = alreadyConnected.isEmpty ? nil : ContinuousClock.now + .milliseconds(300)
+        var graceDeadline: ContinuousClock.Instant? = nil

However, if the current behavior is intentional (return early when all relays are already connected), then the code is correct as-is — just the comment on line 337 is slightly misleading.

damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (2)

410-414: defer with Task may not guarantee cleanup timing.

The defer blocks wrap the release call in a Task, which means the ephemeral relay release happens asynchronously after the function returns. While the ref-counting in RelayPool prevents premature removal, this pattern has a subtle issue: if the caller immediately makes another lookup call before the deferred Task executes, the lease count will be temporarily higher than necessary.

This is unlikely to cause functional issues due to the ref-counting design, but for cleaner semantics, consider making the lookup functions fully async with cleanup at the end:

🔎 Alternative pattern

Instead of:

defer {
    if !ephemeralRelays.isEmpty {
        Task { await self.pool.releaseEphemeralRelays(ephemeralRelays) }
    }
}

You could structure as:

// At end of function, before return statements:
if !ephemeralRelays.isEmpty {
    await self.pool.releaseEphemeralRelays(ephemeralRelays)
}

This requires restructuring to ensure all code paths release, but guarantees synchronous cleanup.

The current implementation is functionally correct due to the ref-counting safeguards.

Also applies to: 471-475


434-434: Redundant optional initialization.

As flagged by SwiftLint, initializing an optional variable with nil is redundant.

🔎 Suggested fix
-            var filter: NostrFilter? = nil
+            var filter: NostrFilter?
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82d5154 and d3cbb8d.

📒 Files selected for processing (19)
  • damus/ContentView.swift
  • damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift
  • damus/Core/Nostr/Mentions.swift
  • damus/Core/Nostr/NostrEvent.swift
  • damus/Core/Nostr/RelayPool.swift
  • damus/Features/Chat/ChatEventView.swift
  • damus/Features/DMs/Views/DMView.swift
  • damus/Features/Events/BuilderEventView.swift
  • damus/Features/Events/EventLoaderView.swift
  • damus/Features/Events/EventShell.swift
  • damus/Features/Events/EventView.swift
  • damus/Features/Events/MentionView.swift
  • damus/Features/Events/Models/LoadableNostrEventView.swift
  • damus/Features/Events/SelectedEventView.swift
  • damus/Features/Search/Views/SearchResultsView.swift
  • damus/Features/Search/Views/SearchingEventView.swift
  • damus/Shared/Utilities/URLHandler.swift
  • nostrdb/NdbNote+.swift
  • nostrdb/NdbTagIterator.swift
🚧 Files skipped from review as they are similar to previous changes (7)
  • damus/Features/Chat/ChatEventView.swift
  • damus/Features/Events/EventShell.swift
  • damus/Features/Events/MentionView.swift
  • damus/Features/Events/EventView.swift
  • damus/Features/Events/EventLoaderView.swift
  • damus/Core/Nostr/NostrEvent.swift
  • damus/Features/Events/SelectedEventView.swift
🧰 Additional context used
🧬 Code graph analysis (9)
damus/Core/Nostr/Mentions.swift (2)
nostrdb/NdbTagElem.swift (3)
  • data (124-126)
  • id (128-131)
  • string (149-156)
damus/Shared/Utilities/Bech32Object.swift (1)
  • pubkey (105-116)
nostrdb/NdbNote+.swift (2)
damus/Features/Highlight/Models/HighlightEvent.swift (1)
  • tags (206-213)
nostrdb/NdbTagElem.swift (2)
  • matches_char (94-96)
  • id (128-131)
damus/Shared/Utilities/URLHandler.swift (2)
damus/ContentView.swift (1)
  • profile (981-983)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (2)
  • findEvent (433-495)
  • query (380-386)
damus/Features/Search/Views/SearchingEventView.swift (1)
damus/ContentView.swift (2)
  • event (985-987)
  • profile (981-983)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (2)
damus/Core/Nostr/RelayPool.swift (3)
  • acquireEphemeralRelays (183-190)
  • ensureConnected (280-363)
  • releaseEphemeralRelays (196-225)
damus/ContentView.swift (1)
  • event (985-987)
damus/Features/Events/BuilderEventView.swift (1)
damus/ContentView.swift (1)
  • event (985-987)
damus/Features/Events/Models/LoadableNostrEventView.swift (2)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (2)
  • lookup (359-378)
  • lookup (397-427)
damus/ContentView.swift (2)
  • event (985-987)
  • zap (37-39)
damus/Features/DMs/Views/DMView.swift (1)
damus/Core/Nostr/NostrEvent.swift (1)
  • first_eref_mention_with_hints (850-869)
nostrdb/NdbTagIterator.swift (1)
nostrdb/NdbTagElem.swift (1)
  • string (149-156)
🪛 SwiftLint (0.57.0)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift

[Warning] 434-434: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (22)
damus/Core/Nostr/Mentions.swift (1)

125-170: LGTM! Well-structured relay hint preservation.

The implementation correctly parses NIP-01/NIP-10 tags and preserves relay hints by upgrading simple variants (npub, note) to their relay-aware counterparts (nprofile, nevent) when hints are present. The docstring clearly explains the intent.

damus/Core/Nostr/RelayPool.swift (2)

196-225: Good race-safety pattern for ephemeral relay cleanup.

The implementation correctly handles the TOCTOU race by:

  1. Decrementing the lease count atomically (before any suspension point)
  2. Re-checking ephemeralLeases[url] == nil after the await get_relay() call to ensure the lease wasn't re-acquired during the suspension

280-363: LGTM! Ephemeral relay connection logic is well-designed.

The ensureConnected method properly:

  • Distinguishes between already-connected, needs-connection, and new ephemeral relays
  • Uses a polling loop with configurable timeout
  • Implements a grace period to allow multiple relays to connect before returning
  • Returns only the subset of relays that are actually connected
damus/Features/Events/BuilderEventView.swift (1)

10-66: LGTM! Clean relay hints integration.

The changes properly thread relay hints through the view hierarchy while maintaining backward compatibility via default empty arrays. The docstrings clearly explain the NIP-01/NIP-10 relay hint purpose.

damus/ContentView.swift (1)

1117-1149: LGTM! Proper handling of NIP-19 variants with clear error messaging.

The implementation correctly:

  • Propagates relay hints from nevent to the loadable note route
  • Provides user-friendly error sheets for deprecated (nrelay) and security-sensitive (nsec) references
  • Includes appropriate guidance in the error messages
nostrdb/NdbNote+.swift (1)

35-56: LGTM! Clean implementation of repost target extraction with relay hints.

The method correctly:

  • Guards for repost (boost) events before processing
  • Iterates tags to find the first valid e tag with a note ID
  • Leverages the new tag.relayHints extension for relay hint extraction
  • Returns nil for non-repost events or when no valid e-tag is found

The docstring accurately describes NIP-18 requirements.

damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (1)

397-427: LGTM! Consistent ephemeral relay handling pattern.

The lookup(naddr:) method properly:

  • Acquires leases before connecting
  • Falls back to default pool when no hinted relays connect
  • Logs relay hint usage in DEBUG builds
  • Releases leases in defer block

The pattern is consistent with findEvent and aligns with the RelayPool ephemeral relay lifecycle.

nostrdb/NdbTagIterator.swift (1)

45-73: LGTM! Clean relay hint extraction with proper bounds checking.

The implementation correctly:

  • Guards against out-of-bounds access with count >= 3 check
  • Validates non-empty string before creating RelayURL
  • Returns an array for consistency with NEvent.relays and future extensibility
  • Includes comprehensive docstrings

RelayURL gracefully handles invalid URL strings through its failable initializer, which returns nil for invalid URLs, missing schemes, or non-WebSocket schemes. The code correctly chains this optional result to the relayHint property type.

damus/Features/Search/Views/SearchResultsView.swift (3)

104-115: LGTM! Relay hints correctly propagated to SearchingEventView.

The empty relay arrays for legacy cases (profile, hex, note) provide a safe fallback, while nevent and nprofile correctly extract and pass their embedded relay hints.


201-212: Clean implementation with helpful documentation.

The early-return guard for empty strings prevents unnecessary processing and the docstring clearly documents the function's behavior.


241-244: Appropriate debug logging.

The #if DEBUG guard ensures this diagnostic output won't affect release builds.

damus/Features/Search/Views/SearchingEventView.swift (4)

17-22: Clean enum extension for relay hints.

The updated SearchType enum properly carries relay hints as associated values, enabling relay-aware lookups throughout the search flow.


44-51: Well-documented state transitions.

The docstring clearly describes the possible outcomes of handle_search, making the async state machine easier to understand.


87-106: Consistent relay-hint handling for event and profile lookups.

The targetRelays = relays.isEmpty ? nil : relays pattern correctly maps empty arrays to nil, aligning with the FindEvent.event(evid:find_from:) and FindEvent.profile(pubkey:find_from:) signatures that expect nil for "no hints."


107-116: Correct relay-hint propagation for naddr lookups.

The naddr case properly extracts relay hints from the NAddr object and passes them to the lookup method.

damus/Features/DMs/Views/DMView.swift (1)

18-26: Correct migration to relay-hint-aware mention extraction.

The switch from first_eref_mention to first_eref_mention_with_hints ensures that DM mentions can leverage relay hints for loading referenced notes, aligning with the PR's goal of propagating hints through the UI.

damus/Shared/Utilities/URLHandler.swift (3)

34-39: Verify fire-and-forget lookup is intentional.

The Task { ... } on lines 36-38 performs an async findEvent lookup, but the result is discarded (let _ = await ...). This appears to be a "warm the cache" pattern—the lookup fetches/caches the profile so it's available when ProfileByKey renders.

If this is intentional, consider adding a brief comment clarifying the intent (e.g., // Pre-fetch profile into local cache). If the profile must be loaded before navigation, this creates a race condition where the view may render before the fetch completes.


93-101: Good approach: direct parsing preserves relay hints.

The comment on line 94 explains the motivation well. Parsing nevent/nprofile directly before falling through to decode_nostr_uri ensures relay hints aren't discarded. This is a clean solution.


112-113: Correct fallback with empty relays.

Since this path goes through decode_nostr_uri which discards relay hints, returning an empty relays array is the appropriate fallback.

damus/Features/Events/Models/LoadableNostrEventView.swift (3)

54-63: Clean helper for relay-aware event loading.

The loadEvent helper encapsulates the relay-hint logic consistently with the rest of the codebase.


89-92: Smart relay-hint propagation for reactions.

Propagating the same relay hints when following a .like to its referenced note is a good heuristic—the reaction and referenced note are likely on the same relay.


113-116: Consistent enum extension for relay hints.

The NoteReference enum update aligns with the relay-hint propagation pattern used throughout this PR.

@alltheseas
Copy link
Collaborator Author

@danieldaquino working on adding relay hints for quotes (related to our discussion today) #3498, and replies to the existing commits

alltheseas and others added 2 commits January 5, 2026 13:50
Add QuoteRef struct that preserves relay hints from position 2 of q tags.
Update first_eref_mention_with_hints to fall back to q tags when no
inline mention is found in content, enabling quote reposts to use relay
hints for fetching the quoted event.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add direct_reply_ref() to return NoteRef with relay hint. Update
ReplyQuoteView to accept relayHint parameter and use EventLoaderView
when reply event is not in cache, enabling replies to be fetched
from hinted relays.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Reference both NIP-10 and NIP-18 for q tag documentation
- Document that QuoteRef only supports hex event IDs, not event addresses

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
damus/Features/Chat/ReplyQuoteView.swift (1)

75-92: Consider handling the case when event is not cached and no relay hint is available.

Currently, if state.events.lookup(event_id) returns nil and relayHint is also nil (or invalid), the Group renders nothing silently. This may be intentional for a compact reply preview, but could confuse users if the quoted event never appears.

If this is the expected behavior, a brief inline comment would clarify intent. Otherwise, consider a placeholder or loading indicator.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3cbb8d and 79731e9.

📒 Files selected for processing (5)
  • damus/Core/Nostr/Id.swift
  • damus/Core/Nostr/NostrEvent.swift
  • damus/Features/Chat/ChatEventView.swift
  • damus/Features/Chat/ReplyQuoteView.swift
  • nostrdb/NdbNote.swift
🧰 Additional context used
🧬 Code graph analysis (4)
nostrdb/NdbNote.swift (1)
damus/Features/Highlight/Models/HighlightEvent.swift (1)
  • tags (206-213)
damus/Core/Nostr/NostrEvent.swift (3)
nostrdb/NdbBlock.swift (2)
  • borrowBlockGroup (124-139)
  • forEachBlock (278-281)
nostrdb/UnownedNdbNote.swift (1)
  • borrow (57-70)
damus/Core/Nostr/Mentions.swift (2)
  • note (34-36)
  • note (193-195)
damus/Features/Chat/ReplyQuoteView.swift (2)
damus/ContentView.swift (1)
  • event (985-987)
damus/Features/Timeline/Models/HomeModel.swift (2)
  • should_show_event (1221-1227)
  • should_show_event (1229-1237)
damus/Features/Chat/ChatEventView.swift (2)
nostrdb/NdbNote.swift (1)
  • direct_reply_ref (550-552)
damus/Core/Nostr/NostrEvent.swift (1)
  • first_eref_mention_with_hints (853-883)
🔇 Additional comments (9)
damus/Core/Nostr/Id.swift (2)

52-67: LGTM!

Minor whitespace adjustments with no semantic changes to QuoteId.


83-127: Well-documented struct with appropriate NIP-10/NIP-18 references.

The implementation correctly:

  • Parses relay hints from tag position 2 via tag.relayHints
  • Serializes only the first relay hint in the tag property (matching the NIP spec)
  • Documents the limitation regarding event addresses vs hex IDs

The explicit documentation about unsupported event addresses (<kind>:<pubkey>:<d>) is helpful for future maintainers.

nostrdb/NdbNote.swift (2)

438-440: LGTM!

Follows the established pattern of other referenced_* computed properties in this extension.


546-552: LGTM!

Good addition that exposes the full NoteRef (including relay hint) rather than just the NoteId. The NIP-10 documentation is helpful context.

damus/Features/Chat/ReplyQuoteView.swift (1)

10-30: LGTM!

Clean API extension with a sensible default. The docstring correctly references NIP-10 for relay hints.

damus/Core/Nostr/NostrEvent.swift (2)

833-838: LGTM!

Simple, focused value type. The index being optional makes sense since q-tag fallback doesn't have a content index.


840-883: Well-structured function with clear fallback logic.

The implementation correctly:

  1. Prioritizes inline content mentions over q-tags
  2. Preserves relay hints from nevent references while returning empty hints for plain note references
  3. Falls back to q-tags for NIP-18 quote reposts

The try? pattern is consistent with the existing first_eref_mention function above, so the error handling approach is appropriate here.

damus/Features/Chat/ChatEventView.swift (2)

146-154: LGTM!

Good refactor from direct_replies() to direct_reply_ref() to leverage the full NoteRef including relay hints. The relay hint is correctly passed through to ReplyQuoteView.


160-164: LGTM!

Correctly updated to use first_eref_mention_with_hints and propagate relay hints to MentionView. The styling changes (background, clipShape) provide good visual containment for the mention.

Add DEBUG-only print statements to trace relay hint consumption:
- Inline nevent mentions in content
- Quote (q tag) relay hints
- Reply (e tag) relay hints
- Repost (e tag) relay hints
- URL nevent/nprofile link hints
- EventLoaderView fetch attempts

Filter console by "[relay-hints]" to see all activity.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
damus/Core/Nostr/NostrEvent.swift (1)

840-893: Solid implementation of relay-hint extraction.

The function correctly scans inline mentions before falling back to q tags, preserving relay hints from nevent entities as intended.

A few optional refinements:

Consider using a logging framework for DEBUG logs

While print() works for DEBUG builds, using a structured logger (e.g., os.Logger or the existing logging infrastructure) would provide better control and consistency:

 #if DEBUG
-if !nEvent.relays.isEmpty {
-    print("[relay-hints] Inline nevent: Found \(nEvent.relays.count) hint(s) for \(nEvent.noteid.hex().prefix(8))...: \(nEvent.relays.map { $0.absoluteString })")
-}
+Self.logger.debug("Inline nevent: Found \(nEvent.relays.count) hint(s) for \(nEvent.noteid.hex().prefix(8))...: \(nEvent.relays.map { $0.absoluteString })")
 #endif

Similar pattern at lines 887-891.

Minor documentation note: Line 845 mentions "NIP-10/NIP-18" — NIP-18 is accurate for quote reposts (q tags), but NIP-10 primarily covers replies. Consider clarifying as "NIP-18" or "NIP-18 (quotes) / NIP-10 (for broader tag context)".

damus/Features/Events/EventLoaderView.swift (1)

10-57: Excellent integration of relay hints into the event loader.

The changes cleanly extend EventLoaderView to support relay-hinted lookups while maintaining backward compatibility (empty array = no hints). The documentation is clear and the debug logging provides good visibility.

Optional: Use structured logging

For consistency with production logging patterns, consider replacing print() with a structured logger:

 #if DEBUG
-if let targetRelays, !targetRelays.isEmpty {
-    print("[relay-hints] EventLoaderView: Loading event \(event_id.hex().prefix(8))... with \(targetRelays.count) relay hint(s): \(targetRelays.map { $0.absoluteString })")
-}
+Self.logger.debug("EventLoaderView: Loading event \(event_id.hex().prefix(8))... with \(targetRelays.count) relay hint(s): \(targetRelays.map { $0.absoluteString })")
 #endif

Similar for lines 53-57.

damus/Shared/Utilities/URLHandler.swift (1)

94-123: Well-designed URL parsing that preserves relay hints.

The direct parsing of nevent and nprofile before the decode_nostr_uri fallback correctly preserves relay hints that would otherwise be lost. The comment at line 94 clearly explains the rationale, and the debug logging provides good visibility.

Optional: Consolidate debug logging

The debug logging pattern is repeated. Consider extracting to a helper:

#if DEBUG
private static func logRelayHints(type: String, count: Int, id: String, urls: [String]) {
    if count > 0 {
        print("[relay-hints] \(type): Found \(count) hint(s) for \(id): \(urls)")
    }
}
#endif

Then use as:

logRelayHints(type: "URL nevent", count: nevent.relays.count, 
              id: "\(nevent.noteid.hex().prefix(8))...", 
              urls: nevent.relays.map { $0.absoluteString })

Similar refactoring could apply to logging in NostrEvent.swift.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79731e9 and 5585827.

📒 Files selected for processing (4)
  • damus/Core/Nostr/Mentions.swift
  • damus/Core/Nostr/NostrEvent.swift
  • damus/Features/Events/EventLoaderView.swift
  • damus/Shared/Utilities/URLHandler.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • damus/Core/Nostr/Mentions.swift
🧰 Additional context used
🧬 Code graph analysis (3)
damus/Shared/Utilities/URLHandler.swift (3)
damus/ContentView.swift (1)
  • profile (981-983)
damus/Core/Nostr/Mentions.swift (2)
  • pubkey (30-32)
  • pubkey (200-202)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (2)
  • findEvent (433-495)
  • query (380-386)
damus/Core/Nostr/NostrEvent.swift (3)
nostrdb/NdbBlock.swift (1)
  • borrowBlockGroup (124-139)
nostrdb/UnownedNdbNote.swift (1)
  • borrow (57-70)
damus/Core/Nostr/Mentions.swift (2)
  • note (34-36)
  • note (196-198)
damus/Features/Events/EventLoaderView.swift (1)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (2)
  • lookup (359-378)
  • lookup (397-427)
🔇 Additional comments (2)
damus/Core/Nostr/NostrEvent.swift (1)

833-838: LGTM! Clean data structure for relay-hinted mentions.

The NoteMentionWithHints struct effectively encapsulates note references with their relay hints, supporting the NIP-19 relay hints feature.

damus/Shared/Utilities/URLHandler.swift (1)

34-39: LGTM! Appropriate prefetch pattern for profile relay hints.

The fire-and-forget Task correctly prefetches profile metadata from hinted relays while immediately navigating to the profile view, which will display cached data and update when the fetch completes.

alltheseas and others added 2 commits January 5, 2026 23:31
Add pubkey field to NoteRef struct for full NIP-10 compliance:
- Parse pubkey from position 4 when marker is recognized
- Include pubkey in tag generation
- Update nip10_reply_tags to propagate author pubkey

This enables clients to find reply targets on correct relays.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
When looking up events with relay hints:
- Try hinted relays first with short timeout
- Fall back to broadcast if hints don't respond
- Use total deadline to prevent timeout accumulation
- Skip hint phase if no relays resolve

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI Agents
In @damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift:
- Around line 447-455: Remove the leftover incomplete docstring lines that
contain a standalone "/// - Parameters:" (the small block at the top of the doc
comment in SubscriptionManager.swift for the method that finds a replaceable
event by naddr) and keep only the complete docstring that follows (the full
description including the detailed - Parameters: list and - Returns:). Ensure
the method's documentation for the naddr-based replaceable-event lookup is a
single coherent docblock (remove the three-line incomplete block and leave the
full block describing the naddr, targetRelays, timeout, and return value).
🧹 Nitpick comments (1)
damus/Features/Posting/Views/PostView.swift (1)

814-833: Unused keypair parameter should be removed.

The keypair parameter is declared but never used within the function body. Remove it from the function signature and update all call sites.

This requires changes at:

  • Function definition (line 816)
  • Call site in PostView.swift (line 922)
  • Test call sites in NIP10Tests.swift (lines 355 and 385)
🔎 Proposed changes

Function signature:

-func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair, relayURL: RelayURL?) -> [[String]] {
+func nip10_reply_tags(replying_to: NostrEvent, relayURL: RelayURL?) -> [[String]] {

Call site in PostView.swift (line 922):

-tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: await state.nostrNetwork.relaysForEvent(event: replying_to).first)
+tags = nip10_reply_tags(replying_to: replying_to, relayURL: await state.nostrNetwork.relaysForEvent(event: replying_to).first)

Call sites in NIP10Tests.swift:

-let tags = nip10_reply_tags(replying_to: parentNote, keypair: test_keypair, relayURL: relayUrl)
+let tags = nip10_reply_tags(replying_to: parentNote, relayURL: relayUrl)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5585827 and d9e9d19.

📒 Files selected for processing (5)
  • .beads/issues.jsonl
  • damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift
  • damus/Core/Types/Ids/Referenced.swift
  • damus/Features/Posting/Views/PostView.swift
  • damusTests/NIP10Tests.swift
🧰 Additional context used
📓 Path-based instructions (1)
**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

**/*.swift: Maximize usage of nostrdb facilities (Ndb, NdbNote, iterators) whenever possible for persistence and queries in the Damus iOS app
Favor Swift-first solutions that lean on nostrdb types (Ndb, NdbNote, iterators) before introducing new storage mechanisms
Ensure docstring coverage for any code added, or modified
Ensure nevernesting: favor early returns and guard clauses over deeply nested conditionals; simplify control flow by exiting early instead of wrapping logic in multiple layers of if statements

Files:

  • damus/Features/Posting/Views/PostView.swift
  • damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift
  • damus/Core/Types/Ids/Referenced.swift
  • damusTests/NIP10Tests.swift
🧬 Code graph analysis (3)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (1)
damus/Core/Nostr/RelayPool.swift (5)
  • getRelays (424-437)
  • subscribe (463-473)
  • subscribe (488-559)
  • acquireEphemeralRelays (183-190)
  • releaseEphemeralRelays (196-225)
damus/Core/Types/Ids/Referenced.swift (1)
nostrdb/NdbTagElem.swift (1)
  • id (128-131)
damusTests/NIP10Tests.swift (3)
damus/Core/Types/Ids/Referenced.swift (1)
  • note_id (49-51)
damus/Features/Posting/Views/PostView.swift (1)
  • nip10_reply_tags (816-833)
nostrdb/NdbTagElem.swift (1)
  • id (128-131)
🔇 Additional comments (16)
.beads/issues.jsonl (1)

1-8: LGTM! Well-structured issue tracking data.

The JSONL format is valid, field usage is consistent across all entries, and the content appropriately documents the relay hints implementation work described in the PR objectives. The dependency tracking, status management, and metadata are all properly structured.

damusTests/NIP10Tests.swift (9)

150-203: LGTM!

The updated test correctly validates NIP-10 tag structure, verifying that the root tag lacks a pubkey (since the parent's e-tag didn't include one) while the reply tag includes the author's pubkey at position 4.


268-289: LGTM!

Comprehensive test for NoteRef parsing with pubkey from position 4 of e-tags, correctly validating all NIP-10 fields.


291-310: LGTM!

Good backwards compatibility test ensuring NoteRef correctly handles e-tags without the optional pubkey field.


312-327: LGTM!

Validates that NoteRef.tag correctly generates a 5-element array including the pubkey when present.


329-342: LGTM!

Correctly validates that NoteRef.tag omits the pubkey field when it's nil, producing a 4-element array.


344-367: LGTM!

Tests the direct reply case where the parent note isn't in a thread, validating that the resulting root tag includes the parent's pubkey at position 4.


369-404: LGTM!

Excellent test for pubkey propagation in threaded replies. Correctly validates that the root pubkey from the parent's e-tag is preserved while the reply tag includes the immediate parent's pubkey.


406-420: LGTM!

Validates relay hint extraction from e-tag position 2.


422-435: LGTM!

Important edge case test ensuring empty relay hints are preserved as empty strings rather than being converted to nil.

damus/Core/Types/Ids/Referenced.swift (3)

30-58: LGTM!

Clean extension of NoteRef with optional pubkey support. The initializers maintain backwards compatibility with sensible defaults.


60-73: LGTM!

The tag generation correctly follows NIP-10 format, only including pubkey when a marker is present (since pubkey is at position 4 and marker at position 3).


75-106: LGTM!

The parsing logic correctly handles pubkey extraction with appropriate guards:

  • Only parses pubkey when a valid marker is recognized
  • Safely handles invalid pubkey data via pk.id() returning nil
  • Aligns with the documented NIP-10 format
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (3)

419-437: Well-structured helper function.

Clean implementation with good docstring coverage. The early return pattern and simple control flow align well with the coding guidelines.


469-473: Fire-and-forget Task in defer for ephemeral relay release.

The defer { Task { await ... } } pattern spawns an unstructured task that runs asynchronously after the function returns. This means:

  1. The release timing is non-deterministic
  2. Any errors from releaseEphemeralRelays are silently dropped

This may be intentional to avoid blocking the caller, and the releaseEphemeralRelays implementation appears to handle races. However, verify this is the intended behavior, especially if ephemeral relay cleanup timing is critical for pool size management.


488-554: Good implementation of ephemeral relay support.

The updated findEvent correctly uses the acquireEphemeralRelays + ensureConnected pattern, with appropriate fallback to broadcast when no hinted relays connect (line 524). The docstring is clear and follows coding guidelines. The same fire-and-forget defer pattern noted elsewhere applies here (lines 530-534).

Apply same acquireEphemeralRelays + ensureConnected pattern used by
lookup(naddr:) to lookup(noteId:). Previously, noteId lookup only
checked relays already in the pool, causing hint relays to be skipped
entirely if not pre-connected.

Also removes duplicate docstring block.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (1)

495-495: Remove redundant nil initialization.

Optional variables default to nil in Swift; explicit initialization is unnecessary.

🔎 Suggested fix
-            var filter: NostrFilter? = nil
+            var filter: NostrFilter?
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9e9d19 and db26556.

📒 Files selected for processing (1)
  • damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift
🧰 Additional context used
📓 Path-based instructions (1)
**/*.swift

📄 CodeRabbit inference engine (AGENTS.md)

**/*.swift: Maximize usage of nostrdb facilities (Ndb, NdbNote, iterators) whenever possible for persistence and queries in the Damus iOS app
Favor Swift-first solutions that lean on nostrdb types (Ndb, NdbNote, iterators) before introducing new storage mechanisms
Ensure docstring coverage for any code added, or modified
Ensure nevernesting: favor early returns and guard clauses over deeply nested conditionals; simplify control flow by exiting early instead of wrapping logic in multiple layers of if statements

Files:

  • damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift
🧬 Code graph analysis (1)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (3)
damus/Core/Nostr/RelayPool.swift (5)
  • acquireEphemeralRelays (183-190)
  • releaseEphemeralRelays (196-225)
  • ensureConnected (280-363)
  • subscribe (463-473)
  • subscribe (488-559)
nostrdb/Ndb+.swift (1)
  • subscribe (21-29)
nostrdb/UnownedNdbNote.swift (1)
  • justGetACopy (110-119)
🪛 SwiftLint (0.57.0)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift

[Warning] 495-495: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (4)
damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift (4)

358-422: Ephemeral relay handling now consistent and well-structured.

The two-phase lookup approach (hint relays first with short timeout, then broadcast fallback with remaining budget) is well-implemented. The acquire/ensureConnected/release pattern is now consistent with lookup(naddr:) and findEvent, addressing the previous review feedback.


424-442: Clean helper extraction.

Good encapsulation of the fetch logic with clear documentation of the return semantics.


452-488: LGTM!

The ephemeral relay handling follows the same consistent pattern, and the fallback to broadcast (via nil target relays) when no hints connect is a sensible default.


520-536: Ephemeral relay handling is consistent.

The acquire/ensureConnected/release pattern matches the other lookup functions, maintaining consistency across the codebase.

@alltheseas alltheseas requested a review from jb55 January 7, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-in-queue This PR is waiting in a queue behind their other PRs marked with the label `pr-active-review`. relay technical

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consume relay hints NIP-19

2 participants