Skip to content

Commit 8e1d167

Browse files
alltheseasclaude
andcommitted
feat: add purplepag.es as profile-only relay
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: #1331 Closes: #3174 Signed-off-by: alltheseas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 5058fb3 commit 8e1d167

File tree

11 files changed

+125
-46
lines changed

11 files changed

+125
-46
lines changed

damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ extension NostrNetworkManager {
212212

213213
/// Relay filters
214214
var relayFilters: RelayFilters { get }
215-
215+
216216
/// The user's connected NWC wallet
217217
var nwcWallet: WalletConnectURL? { get }
218218
}

damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ extension NostrNetworkManager {
3535
}
3636

3737
private func computeRelaysToConnectTo(with relayList: NIP65.RelayList) -> [RelayPool.RelayDescriptor] {
38-
let regularRelayDescriptorList = relayList.toRelayDescriptors()
38+
var descriptors = relayList.toRelayDescriptors()
39+
3940
if let nwcWallet = delegate.nwcWallet {
40-
return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)]
41+
descriptors.append(.nwc(url: nwcWallet.relay))
4142
}
42-
return regularRelayDescriptorList
43+
return descriptors
4344
}
4445

4546
// MARK: - Getting the user's relay list

damus/Core/Nostr/NostrRequest.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,28 @@ enum NostrRequestType {
3333
guard case .typical(let req) = self else {
3434
return true
3535
}
36-
36+
3737
return req.is_read
3838
}
3939
}
4040

41+
extension NostrRequestType {
42+
/// Profile-related kinds that should be queried on profiles-only relays
43+
static let profileKinds: Set<NostrKind> = [.metadata, .contacts, .relay_list]
44+
45+
/// Whether this request is for profile-related data only
46+
var isProfileRelated: Bool {
47+
guard case .typical(let req) = self else { return false }
48+
guard case .subscribe(let sub) = req else { return false }
49+
50+
// Check if ALL filters contain ONLY profile-related kinds
51+
return sub.filters.allSatisfy { filter in
52+
guard let kinds = filter.kinds else { return false } // No kinds specified = could be anything
53+
return kinds.allSatisfy { Self.profileKinds.contains($0) }
54+
}
55+
}
56+
}
57+
4158
/// Models a standard request/message that is sent to a Nostr relay.
4259
enum NostrRequest {
4360
/// Subscribes to receive information from the relay

damus/Core/Nostr/Relay.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ enum RelayVariant {
3232
case regular
3333
case ephemeral
3434
case nwc
35+
case profilesOnly // Only used for profile metadata queries (kind 0, 3, 10002)
3536
}
3637

3738
extension RelayPool {
@@ -55,12 +56,22 @@ extension RelayPool {
5556
return true
5657
case .nwc:
5758
return true
59+
case .profilesOnly:
60+
return false // Not ephemeral - filtering is handled separately via isProfileRelated check
5861
}
5962
}
60-
63+
64+
var isProfilesOnly: Bool {
65+
variant == .profilesOnly
66+
}
67+
6168
static func nwc(url: RelayURL) -> RelayDescriptor {
6269
return RelayDescriptor(url: url, info: .readWrite, variant: .nwc)
6370
}
71+
72+
static func profilesOnly(url: RelayURL) -> RelayDescriptor {
73+
return RelayDescriptor(url: url, info: .read, variant: .profilesOnly)
74+
}
6475
}
6576
}
6677

damus/Core/Nostr/RelayPool.swift

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,13 @@ actor RelayPool {
425425
if relay.descriptor.ephemeral && skip_ephemeral {
426426
continue // Do not send requests to ephemeral relays if we want to skip them
427427
}
428-
428+
429+
// Filter purplepag.es to only receive profile-related requests (kinds 0, 3, 10002)
430+
let isPurplePagesRelay = relay.descriptor.url.absoluteString.contains("purplepag.es")
431+
if (relay.descriptor.isProfilesOnly || isPurplePagesRelay) && !req.isProfileRelated {
432+
continue
433+
}
434+
429435
guard relay.connection.isConnected else {
430436
Task { await queue_req(r: req, relay: relay.id, skip_ephemeral: skip_ephemeral) }
431437
continue
@@ -465,16 +471,13 @@ actor RelayPool {
465471
}
466472

467473
func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) {
468-
if case .nostr_event(let ev) = event {
469-
if case .event(_, let nev) = ev {
470-
if seen[nev.id]?.contains(relay_id) == true {
471-
return
472-
}
473-
seen[nev.id, default: Set()].insert(relay_id)
474-
counts[relay_id, default: 0] += 1
475-
notify(.update_stats(note_id: nev.id))
476-
}
477-
}
474+
guard case .nostr_event(let ev) = event else { return }
475+
guard case .event(_, let nev) = ev else { return }
476+
guard seen[nev.id]?.contains(relay_id) != true else { return }
477+
478+
seen[nev.id, default: Set()].insert(relay_id)
479+
counts[relay_id, default: 0] += 1
480+
notify(.update_stats(note_id: nev.id))
478481
}
479482

480483
func resubscribeAll(relayId: RelayURL) async {

damus/Features/Relays/Models/RelayBootstrap.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fileprivate let BOOTSTRAP_RELAYS = [
1313
"wss://nostr.land",
1414
"wss://nostr.wine",
1515
"wss://nos.lol",
16+
"wss://purplepag.es",
1617
]
1718

1819
fileprivate let REGION_SPECIFIC_BOOTSTRAP_RELAYS: [Locale.Region: [String]] = [

damus/Features/Relays/Views/RelayConfigView.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,8 @@ struct RelayConfigView: View {
3737
}
3838

3939
var recommended: [RelayPool.RelayDescriptor] {
40-
let rs: [RelayPool.RelayDescriptor] = []
41-
let recommended_relay_addresses = get_default_bootstrap_relays()
42-
return recommended_relay_addresses.reduce(into: rs) { xs, x in
43-
xs.append(RelayPool.RelayDescriptor(url: x, info: .readWrite))
44-
}
40+
// Use default bootstrap relays (includes purplepag.es)
41+
return get_default_bootstrap_relays().map { RelayPool.RelayDescriptor(url: $0, info: .readWrite) }
4542
}
4643

4744
var body: some View {
@@ -134,7 +131,7 @@ struct RelayConfigView: View {
134131

135132
ForEach(relayList, id: \.url) { relay in
136133
Group {
137-
RelayView(state: state, relay: relay.url, showActionButtons: $showActionButtons, recommended: recommended)
134+
RelayView(state: state, relay: relay.url, showActionButtons: $showActionButtons, recommended: recommended, descriptor: relay)
138135
Divider()
139136
}
140137
}

damus/Features/Relays/Views/RelayDetailView.swift

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,25 @@ struct RelayDetailView: View {
2020
self.state = state
2121
self.relay = relay
2222
self.nip11 = nip11
23-
2423
log = state.relay_model_cache.model(with_relay_id: relay)?.log ?? RelayLog()
2524
}
26-
27-
func check_connection() -> Bool {
28-
return state.nostrNetwork.userRelayList.getUserCurrentRelayList()?.relays.keys.contains(self.relay) == true
25+
26+
/// Whether relay is connected (in the user's relay list)
27+
var isConnected: Bool {
28+
state.nostrNetwork.userRelayList.getUserCurrentRelayList()?.relays.keys.contains(self.relay) == true
29+
}
30+
31+
/// Check if relay URL is purplepag.es (for showing Profile badge)
32+
static func isPurplePagesRelay(_ relay: RelayURL) -> Bool {
33+
relay.absoluteString.contains("purplepag.es")
34+
}
35+
36+
/// Whether this relay is profile-only (for showing Profile badge)
37+
var isProfileOnlyRelay: Bool {
38+
if Self.isPurplePagesRelay(relay) {
39+
return true
40+
}
41+
return state.nostrNetwork.getRelay(relay)?.descriptor.isProfilesOnly ?? false
2942
}
3043

3144
func RemoveRelayButton(_ keypair: FullKeypair) -> some View {
@@ -87,10 +100,14 @@ struct RelayDetailView: View {
87100
.foregroundColor(.gray)
88101

89102
HStack {
103+
if isProfileOnlyRelay {
104+
RelayType(is_paid: false, is_profile_only: true)
105+
}
106+
90107
if nip11?.is_paid ?? false {
91108
RelayPaidDetail(payments_url: nip11?.payments_url, fees: nip11?.fees)
92109
}
93-
110+
94111
if let authentication_state: RelayAuthenticationState = relay_object?.authentication_state,
95112
authentication_state != .none {
96113
RelayAuthenticationDetail(state: authentication_state)
@@ -138,7 +155,7 @@ struct RelayDetailView: View {
138155
}
139156

140157
if let keypair = state.keypair.to_full() {
141-
if check_connection() {
158+
if isConnected {
142159
RemoveRelayButton(keypair)
143160
.padding(.top)
144161
} else {
@@ -187,18 +204,16 @@ struct RelayDetailView: View {
187204

188205
func removeRelay() async {
189206
do {
190-
// TODO: Concurrency problems?
191207
try await state.nostrNetwork.userRelayList.remove(relayURL: self.relay)
192208
dismiss()
193209
}
194210
catch {
195211
present_sheet(.error(error.humanReadableError))
196212
}
197213
}
198-
214+
199215
func connectRelay() async {
200216
do {
201-
// TODO: Concurrency problems?
202217
try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
203218
dismiss()
204219
}

damus/Features/Relays/Views/RelayType.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,23 @@ import SwiftUI
99

1010
struct RelayType: View {
1111
let is_paid: Bool
12-
12+
var is_profile_only: Bool = false
13+
1314
var body: some View {
14-
if is_paid {
15-
Image("bitcoin-logo")
15+
HStack(spacing: 4) {
16+
if is_paid {
17+
Image("bitcoin-logo")
18+
}
19+
if is_profile_only {
20+
Text("Profile")
21+
.font(.caption2)
22+
.fontWeight(.medium)
23+
.foregroundColor(.white)
24+
.padding(.horizontal, 6)
25+
.padding(.vertical, 2)
26+
.background(DamusColors.purple)
27+
.cornerRadius(4)
28+
}
1629
}
1730
}
1831
}
@@ -22,6 +35,8 @@ struct RelayType_Previews: PreviewProvider {
2235
VStack {
2336
RelayType(is_paid: false)
2437
RelayType(is_paid: true)
38+
RelayType(is_paid: false, is_profile_only: true)
39+
RelayType(is_paid: true, is_profile_only: true)
2540
}
2641
}
2742
}

damus/Features/Relays/Views/RelayView.swift

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,40 @@ struct RelayView: View {
1313
let recommended: Bool
1414
/// Disables navigation link
1515
let disableNavLink: Bool
16+
/// Optional descriptor for recommended relays (not yet connected)
17+
let descriptor: RelayPool.RelayDescriptor?
1618
@ObservedObject private var model_cache: RelayModelCache
1719

1820
@State var relay_state: Bool
1921
@Binding var showActionButtons: Bool
2022

21-
init(state: DamusState, relay: RelayURL, showActionButtons: Binding<Bool>, recommended: Bool, disableNavLink: Bool = false) {
23+
init(state: DamusState, relay: RelayURL, showActionButtons: Binding<Bool>, recommended: Bool, disableNavLink: Bool = false, descriptor: RelayPool.RelayDescriptor? = nil) {
2224
self.state = state
2325
self.relay = relay
2426
self.recommended = recommended
2527
self.model_cache = state.relay_model_cache
2628
_showActionButtons = showActionButtons
27-
let relay_state = RelayView.get_relay_state(state: state, relay: relay)
28-
self._relay_state = State(initialValue: relay_state)
29+
self._relay_state = State(initialValue: state.nostrNetwork.getRelay(relay) == nil)
2930
self.disableNavLink = disableNavLink
31+
self.descriptor = descriptor
3032
}
3133

32-
static func get_relay_state(state: DamusState, relay: RelayURL) -> Bool {
33-
return state.nostrNetwork.getRelay(relay) == nil
34+
/// Check if relay URL is purplepag.es (for showing Profile badge)
35+
static func isPurplePagesRelay(_ relay: RelayURL) -> Bool {
36+
relay.absoluteString.contains("purplepag.es")
37+
}
38+
39+
/// Whether this relay is profile-only (for showing Profile badge)
40+
var isProfileOnly: Bool {
41+
if Self.isPurplePagesRelay(relay) {
42+
return true
43+
}
44+
return state.nostrNetwork.getRelay(relay)?.descriptor.isProfilesOnly ?? descriptor?.isProfilesOnly ?? false
45+
}
46+
47+
/// Whether relay needs to be added (true = show Add button, false = show Added button)
48+
var needsToBeAdded: Bool {
49+
return relay_state
3450
}
3551

3652
var body: some View {
@@ -52,8 +68,11 @@ struct RelayView: View {
5268
.font(.headline)
5369
.padding(.bottom, 2)
5470
.lineLimit(1)
55-
RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false)
56-
71+
RelayType(
72+
is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false,
73+
is_profile_only: isProfileOnly
74+
)
75+
5776
if relay.absoluteString.hasSuffix(".onion") {
5877
Image("tor")
5978
.resizable()
@@ -79,7 +98,7 @@ struct RelayView: View {
7998
if recommended {
8099
if let keypair = state.keypair.to_full() {
81100
VStack(alignment: .center) {
82-
if relay_state {
101+
if needsToBeAdded {
83102
AddButton(keypair: keypair)
84103
} else {
85104
Button(action: {
@@ -110,7 +129,7 @@ struct RelayView: View {
110129
.contentShape(Rectangle())
111130
}
112131
.onReceive(handle_notify(.relays_changed)) { _ in
113-
self.relay_state = RelayView.get_relay_state(state: state, relay: self.relay)
132+
self.relay_state = state.nostrNetwork.getRelay(relay) == nil
114133
}
115134
.onTapGesture {
116135
if !disableNavLink {
@@ -131,7 +150,7 @@ struct RelayView: View {
131150
present_sheet(.error(error.humanReadableError))
132151
}
133152
}
134-
153+
135154
func remove_action(privkey: Privkey) async {
136155
do {
137156
try await state.nostrNetwork.userRelayList.remove(relayURL: relay)

0 commit comments

Comments
 (0)