Skip to content

Conversation

@alltheseas
Copy link
Collaborator

@alltheseas alltheseas commented Dec 20, 2025

User Benefit

Improve chances that profiles are found (i.e. reduce chances profiles are not found).

Summary

Add wss://purplepag.es as a system relay that's always included for all users but only used for profile metadata queries (kinds 0, 3, 10002). This improves profile metadata fetching without polluting the relay with non-profile requests.

Changes:

  • Add profilesOnly variant to RelayVariant enum
  • Add isProfileRelated check to NostrRequestType to detect profile queries
  • Filter profiles-only relays in send_raw to only receive profile requests
  • Always include purplepag.es in relay list via UserRelayListManager

Standard PR Checklist

  • I have read (or I am familiar with) the Contribution Guidelines
  • 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.
    • Not needed: This adds minimal filtering logic that runs in O(1) for relay type check
  • I have opened or referred to an existing github issue related to this change: Modify profile only relay purplepag.es to only query for profile info #1331
  • 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 Signing off your work
  • I have added appropriate changelog entries for the changes in this PR. See Adding changelog entries
  • I have added appropriate Closes: or Fixes: tags in the commit messages wherever applicable, or made sure those are not needed. See Submitting patches

Test report

Device: iPhone 16 Pro Simulator

iOS: 18.x

Damus: feature/purplepages-profile-relay (be1290c)

Setup: Fresh install on simulator

Steps:

  1. Build and run on simulator
  2. Monitor console for purplepag.es connections
  3. Verify only profile-related kinds (0, 3, 10002) are sent to purplepag.es
  4. Verify non-profile requests (kind 1 notes, DMs, etc.) are NOT sent to purplepag.es

Results:

  • PASS
    • Console shows only NostrKind.metadata, NostrKind.contacts, and NostrKind.relay_list subscriptions to purplepag.es
    • No kind 1 (notes) or other non-profile kinds sent to purplepag.es

Other notes

Closes #1331

Debug logging is included (prints to Xcode console only) to help verify filtering is working:

  • 🟣 PURPLEPAGES: sending subscription with kinds: [...]
  • 🟣 PURPLEPAGES: received kind X event

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for profile-only relays with visual badge indicator
    • Added PurplePages relay to default bootstrap relay list
    • Improved relay request routing for intelligent relay selection based on request type

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

Add purplepag.es to the default bootstrap relays for new accounts.
Existing accounts can manually add it from the recommended relays tab.

The relay is filtered to only receive profile-related queries (kinds 0,
3, 10002) regardless of how it was added, ensuring it's used optimally
for profile metadata lookup.

Changes:
- Add purplepag.es to BOOTSTRAP_RELAYS for new accounts
- Filter purplepag.es to only receive profile-related requests
- Show "Profile" badge on purplepag.es in relay views
- Keep isProfileRelated check for subscription filtering

Closes: damus-io#1331
Closes: damus-io#3174
Signed-off-by: alltheseas

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@alltheseas alltheseas force-pushed the feature/purplepages-profile-relay branch from be1290c to 8e1d167 Compare December 20, 2025 15:56
@alltheseas
Copy link
Collaborator Author

Screenshot confirmed

image

@alltheseas
Copy link
Collaborator Author

see purlpepag.es profile badge on recommended relays tab

Screenshot 2025-12-20 at 3 41 06 PM

@alltheseas
Copy link
Collaborator Author

alltheseas commented Dec 21, 2025

I learned that purplepag.es servers also sorts of non-kind1 kinds including e.g. blossom servers. Relates to #3218

@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 22, 2025
@alltheseas
Copy link
Collaborator Author

@alltheseas
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

📝 Walkthrough

Walkthrough

The changes implement a profile-only relay system for the purplepag.es relay. They introduce infrastructure to identify profile-related requests, mark relays as profile-only, filter profile-only relays for non-profile requests, and update the UI to display profile-only indicators and handle the new relay descriptor.

Changes

Cohort / File(s) Summary
Profile-Only Relay Infrastructure
damus/Core/Nostr/Relay.swift, damus/Core/Nostr/NostrRequest.swift
Added RelayVariant.profilesOnly case to distinguish profile-only relays. Added isProfileRelated computed property to identify requests that query only for profile kinds (metadata, contacts, relay\_list). Added isProfilesOnly property and factory method to RelayDescriptor.
Relay Pool & Network Logic
damus/Core/Networking/NostrNetworkManager/RelayPool.swift, damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift
Modified send_raw to skip profile-only relays and purplepag.es for non-profile-related requests. Refactored computeRelaysToConnectTo to build descriptor list with NWC support. Refactored record_seen with guard-based control flow.
Network Manager Delegate
damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift
Added nwcWallet property to Delegate protocol for public WalletConnect URL access.
Bootstrap Configuration
damus/Features/Relays/Models/RelayBootstrap.swift
Added "wss://purplepag.es" to default bootstrap relays.
Relay Configuration View
damus/Features/Relays/Views/RelayConfigView.swift
Refactored recommended relays building to use direct map over bootstrap relays, passing RelayDescriptor to RelayView.
Relay Detail & Type Views
damus/Features/Relays/Views/RelayDetailView.swift, damus/Features/Relays/Views/RelayType.swift
Added stateful isConnected computed property. Added isPurplePagesRelay detector and isProfileOnlyRelay computed property. Added profile label display to RelayType with is_profile_only flag. Updated UI to show Profile badge conditionally.
Relay View
damus/Features/Relays/Views/RelayView.swift
Added optional descriptor property and updated initializer. Added isPurplePagesRelay helper and isProfileOnly computed property. Added needsToBeAdded computed property. Updated RelayType initialization with is_profile_only flag.
Settings Store
damus/Features/Settings/Models/UserSettingsStore.swift
Whitespace cleanup (removed blank line).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RelayPool
    participant RelayDescriptor
    participant Filter

    Client->>RelayPool: send_raw(request)
    RelayPool->>Filter: isProfileRelated(request)
    Filter->>Filter: check if all filters only contain<br/>profileKinds (metadata, contacts, relay_list)
    Filter-->>RelayPool: Boolean result

    loop For each relay descriptor
        RelayPool->>RelayDescriptor: check if isProfilesOnly<br/>or isPurplePages
        
        alt Relay is profile-only and request is NOT profile-related
            RelayPool->>RelayPool: Skip relay, do not send
        else Send to relay
            RelayPool->>Client: Dispatch request to relay
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A profile-only relay hops in,
Purple pages for profiles to spin,
Smart filtering keeps requests aligned,
Notes stay home, profile queries find,
Damus now knows what each relay's for! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% 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 clearly and specifically describes the main change: adding purplepag.es as a profile-only relay.
Description check ✅ Passed The PR description fully completes the Standard PR Checklist with all required items marked as done, includes comprehensive test report with device, iOS version, steps, and passing results, and provides clear summary of changes.
Linked Issues check ✅ Passed The PR fully addresses #1331 by marking purplepag.es as profile-only, implementing filtering to restrict non-profile queries, and adding UI indicators for profile-only relays.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing profile-only relay functionality for purplepag.es; no unrelated modifications detected in the codebase changes.
✨ 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: 2

Fix all issues with AI Agents 🤖
In @damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift:
- Around line 40-42: The appended .nwc descriptor can duplicate an existing
descriptor with the same URL (often added as .regular), causing apply()'s
first(where: { $0.url == url }) to pick the wrong variant; before appending the
nwc descriptor in the block that reads delegate.nwcWallet, check descriptors for
an existing descriptor with the same relay URL and either replace/update its
variant to .nwc (or remove the existing and append the .nwc one) so the list
contains a single entry per URL; this ensures apply() will match the intended
.nwc variant rather than a conflicting .regular entry.

In @damus/Core/Nostr/NostrRequest.swift:
- Around line 46-55: The isProfileRelated logic incorrectly treats empty filters
or empty kinds as profile-only due to vacuous truth; update the check in
NostrRequest.isProfileRelated (the guard for .typical -> .subscribe and the
return using sub.filters.allSatisfy) to first return false if
sub.filters.isEmpty, and inside the allSatisfy closure require that filter.kinds
is non-nil AND not empty (e.g., guard let kinds = filter.kinds, !kinds.isEmpty
else { return false }) before checking kinds.allSatisfy {
Self.profileKinds.contains($0) } so empty filters or empty kinds do not get
classified as profile-related.
🧹 Nitpick comments (4)
damus/Features/Relays/Views/RelayConfigView.swift (1)

40-41: Consider creating profile-only descriptor for purplepag.es.

All recommended relays are created with .readWrite configuration, but purplepag.es is a profile-only relay. While the filtering logic in RelayPool.send_raw handles this via URL detection, using the appropriate descriptor would be more consistent.

🔎 Suggested improvement
     var recommended: [RelayPool.RelayDescriptor] {
-        // Use default bootstrap relays (includes purplepag.es)
-        return get_default_bootstrap_relays().map { RelayPool.RelayDescriptor(url: $0, info: .readWrite) }
+        // Use default bootstrap relays (includes purplepag.es)
+        return get_default_bootstrap_relays().map { url in
+            if url.absoluteString.contains("purplepag.es") {
+                return RelayPool.RelayDescriptor.profilesOnly(url: url)
+            }
+            return RelayPool.RelayDescriptor(url: url, info: .readWrite)
+        }
     }
damus/Core/Nostr/RelayPool.swift (1)

428-433: Consider centralizing the isPurplePagesRelay check.

The purplepag.es URL detection is duplicated in RelayView.swift (line 35), RelayDetailView.swift (line 32), and here. Consider extracting this to a single location (e.g., on RelayURL or as a shared utility) to ensure consistency and reduce duplication.

🔎 Suggested centralization

Add an extension on RelayURL:

extension RelayURL {
    var isPurplePages: Bool {
        absoluteString.contains("purplepag.es")
    }
}

Then use it consistently:

-            let isPurplePagesRelay = relay.descriptor.url.absoluteString.contains("purplepag.es")
-            if (relay.descriptor.isProfilesOnly || isPurplePagesRelay) && !req.isProfileRelated {
+            if (relay.descriptor.isProfilesOnly || relay.descriptor.url.isPurplePages) && !req.isProfileRelated {
damus/Features/Relays/Views/RelayDetailView.swift (1)

31-42: Duplicate isPurplePagesRelay implementation.

This is the same implementation as in RelayView.swift (line 35) and similar to the check in RelayPool.swift (line 430). As noted in the RelayPool review, consider centralizing this to a single location.

damus/Features/Relays/Views/RelayView.swift (1)

34-45: Duplicate isPurplePagesRelay - consolidate with other files.

Same duplication issue as noted in RelayDetailView.swift and RelayPool.swift. The isProfileOnly logic with its fallback chain is good, but the underlying helper should be centralized.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0cecdc and 8e1d167.

📒 Files selected for processing (11)
  • damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift
  • damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift
  • damus/Core/Nostr/NostrRequest.swift
  • damus/Core/Nostr/Relay.swift
  • damus/Core/Nostr/RelayPool.swift
  • damus/Features/Relays/Models/RelayBootstrap.swift
  • damus/Features/Relays/Views/RelayConfigView.swift
  • damus/Features/Relays/Views/RelayDetailView.swift
  • damus/Features/Relays/Views/RelayType.swift
  • damus/Features/Relays/Views/RelayView.swift
  • damus/Features/Settings/Models/UserSettingsStore.swift
🧰 Additional context used
🧬 Code graph analysis (5)
damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift (1)
damus/Core/Nostr/Relay.swift (1)
  • nwc (68-70)
damus/Core/Nostr/RelayPool.swift (4)
damus/Features/Relays/Views/RelayDetailView.swift (1)
  • isPurplePagesRelay (32-34)
damus/Features/Relays/Views/RelayView.swift (1)
  • isPurplePagesRelay (35-37)
damus/Notify/Notify.swift (1)
  • notify (34-39)
damus/Notify/UpdateStatsNotify.swift (1)
  • update_stats (22-24)
damus/Features/Relays/Views/RelayDetailView.swift (3)
damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift (1)
  • getUserCurrentRelayList (63-68)
damus/Features/Relays/Views/RelayView.swift (1)
  • isPurplePagesRelay (35-37)
damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift (1)
  • getRelay (125-128)
damus/Features/Relays/Views/RelayConfigView.swift (2)
damus/Features/Relays/Models/RelayBootstrap.swift (1)
  • get_default_bootstrap_relays (67-75)
damus/Shared/Utilities/Log.swift (1)
  • info (58-60)
damus/Features/Relays/Views/RelayView.swift (1)
damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift (1)
  • getRelay (125-128)
🔇 Additional comments (14)
damus/Features/Settings/Models/UserSettingsStore.swift (1)

249-254: LGTM!

This is a trivial whitespace change (blank line removal) with no semantic impact. The formatting is consistent with the rest of the file.

damus/Features/Relays/Models/RelayBootstrap.swift (1)

16-16: LGTM!

Adding purplepag.es to bootstrap relays is appropriate. The profile-only query filtering is correctly handled in RelayPool.send_raw.

damus/Features/Relays/Views/RelayConfigView.swift (1)

132-137: LGTM!

Properly passing the descriptor to RelayView enables correct profile-only badge rendering.

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

473-481: LGTM!

Good refactoring to use guard statements for cleaner control flow. The logic remains functionally equivalent while improving readability.

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

71-74: Verify read-only configuration for profile-only relays.

The factory method creates descriptors with .read configuration. If users should be able to publish their own profile metadata (kind 0) to purplepag.es, this should be .readWrite. The PR description mentions "profile metadata queries" which suggests read-only is intentional, but please verify this aligns with the expected behavior.


31-36: LGTM!

The new profilesOnly variant is well-documented and integrates cleanly with the existing enum structure.

damus/Features/Relays/Views/RelayType.swift (1)

10-31: LGTM!

Clean implementation of the profile-only badge. The HStack properly handles the case where both paid and profile-only badges need to be displayed together.

damus/Features/Relays/Views/RelayDetailView.swift (2)

25-29: LGTM!

The isConnected computed property cleanly encapsulates the connection check logic.


103-110: LGTM!

Profile badge is correctly displayed for profile-only relays, and the existing paid relay logic is preserved.

damus/Features/Relays/Views/RelayView.swift (2)

16-31: LGTM!

The descriptor parameter with proper initialization enables profile-only detection for both connected and recommended relays. The fallback chain in isProfileOnly correctly handles all cases.


71-75: LGTM!

RelayType is correctly invoked with the is_profile_only flag derived from the new isProfileOnly computed property.

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

38-43: These changes don't match the PR objectives.

The PR description states this is for adding purplepag.es as a profile-only relay, introducing RelayVariant.profilesOnly, and filtering profile queries. However, these changes implement NWC (Nostr Wallet Connect) relay management, which is unrelated to the stated objectives.

Are these changes from a different PR that were accidentally included, or is the PR description incorrect?

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

216-217: These changes don't match the PR objectives.

Similar to the changes in UserRelayListManager.swift, this adds NWC wallet functionality, which is unrelated to the stated PR objective of adding purplepag.es as a profile-only relay.

Please clarify whether these files are from the correct PR or if the PR description needs to be updated.

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

43-43: The profileKinds set is correctly configured for its intended purpose. The values [.metadata, .contacts, .relay_list] (kinds 0, 3, 10002) are intentional and explicitly documented in RelayPool.swift with the comment: "Filter purplepag.es to only receive profile-related requests (kinds 0, 3, 10002)."

These three kinds represent the core profile metadata: user profile (kind 0), contact list (kind 3), and relay preferences (kind 10002). While NIP-51 defines additional list kinds like mute list (10000) and pinned notes (10001), these are user preference/content lists rather than profile metadata. The purplepag.es relay is specifically designed for profile data, not general user lists. Additionally, kind 10001 is not defined in the codebase's NostrKind enum, and kind 30008 does not exist in the Nostr specification.

Comment on lines 40 to 42
if let nwcWallet = delegate.nwcWallet {
return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)]
descriptors.append(.nwc(url: nwcWallet.relay))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential duplicate relay with conflicting variants.

If a user manually adds their NWC relay URL to their relay list, it will be created with variant .regular (line 264). This code then appends the same URL again with variant .nwc. When apply() processes the list and calls first(where: { $0.url == url }) (line 230), it will select the first descriptor with that URL—likely the .regular variant—causing the NWC relay to be misconfigured.

🔎 Proposed fix to prevent duplicate relay URLs
 private func computeRelaysToConnectTo(with relayList: NIP65.RelayList) -> [RelayPool.RelayDescriptor] {
     var descriptors = relayList.toRelayDescriptors()
 
     if let nwcWallet = delegate.nwcWallet {
-        descriptors.append(.nwc(url: nwcWallet.relay))
+        // Remove any existing descriptor with the NWC relay URL to avoid conflicts
+        descriptors.removeAll(where: { $0.url == nwcWallet.relay })
+        descriptors.append(.nwc(url: nwcWallet.relay))
     }
     return descriptors
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let nwcWallet = delegate.nwcWallet {
return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)]
descriptors.append(.nwc(url: nwcWallet.relay))
}
if let nwcWallet = delegate.nwcWallet {
// Remove any existing descriptor with the NWC relay URL to avoid conflicts
descriptors.removeAll(where: { $0.url == nwcWallet.relay })
descriptors.append(.nwc(url: nwcWallet.relay))
}
🤖 Prompt for AI Agents
In @damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift around
lines 40-42, The appended .nwc descriptor can duplicate an existing descriptor
with the same URL (often added as .regular), causing apply()'s first(where: {
$0.url == url }) to pick the wrong variant; before appending the nwc descriptor
in the block that reads delegate.nwcWallet, check descriptors for an existing
descriptor with the same relay URL and either replace/update its variant to .nwc
(or remove the existing and append the .nwc one) so the list contains a single
entry per URL; this ensures apply() will match the intended .nwc variant rather
than a conflicting .regular entry.

Comment on lines +46 to +55
var isProfileRelated: Bool {
guard case .typical(let req) = self else { return false }
guard case .subscribe(let sub) = req else { return false }

// Check if ALL filters contain ONLY profile-related kinds
return sub.filters.allSatisfy { filter in
guard let kinds = filter.kinds else { return false } // No kinds specified = could be anything
return kinds.allSatisfy { Self.profileKinds.contains($0) }
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against edge cases with empty filter or kinds arrays.

The allSatisfy method returns true for empty collections (vacuous truth), which could lead to incorrect classification:

  1. Empty filters array: If sub.filters is empty, the function returns true, treating it as profile-related. However, a subscription with no filters would conceptually match all events and should not be sent to a profile-only relay.

  2. Empty kinds array: If filter.kinds is an empty array (rather than nil), the inner allSatisfy returns true, incorrectly treating it as profile-related. An empty kinds array typically means no kind restriction.

These edge cases could cause non-profile queries to be sent to purplepag.es, defeating the purpose of this filtering.

🔎 Proposed fix to handle edge cases
     /// Whether this request is for profile-related data only
     var isProfileRelated: Bool {
         guard case .typical(let req) = self else { return false }
         guard case .subscribe(let sub) = req else { return false }
-
+        
+        // Must have at least one filter
+        guard !sub.filters.isEmpty else { return false }
+        
         // Check if ALL filters contain ONLY profile-related kinds
         return sub.filters.allSatisfy { filter in
             guard let kinds = filter.kinds else { return false }  // No kinds specified = could be anything
+            guard !kinds.isEmpty else { return false }  // Empty kinds = could be anything
             return kinds.allSatisfy { Self.profileKinds.contains($0) }
         }
     }
🤖 Prompt for AI Agents
In @damus/Core/Nostr/NostrRequest.swift around lines 46-55, The isProfileRelated
logic incorrectly treats empty filters or empty kinds as profile-only due to
vacuous truth; update the check in NostrRequest.isProfileRelated (the guard for
.typical -> .subscribe and the return using sub.filters.allSatisfy) to first
return false if sub.filters.isEmpty, and inside the allSatisfy closure require
that filter.kinds is non-nil AND not empty (e.g., guard let kinds =
filter.kinds, !kinds.isEmpty else { return false }) before checking
kinds.allSatisfy { Self.profileKinds.contains($0) } so empty filters or empty
kinds do not get classified as profile-related.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Modify profile only relay purplepag.es to only query for profile info

2 participants