Turn default relays into required relays
Some checks failed
CI / Lint (pull_request) Failing after 31s
CI / Test (pull_request) Successful in 55s

This commit is contained in:
2026-06-07 16:11:38 +04:00
parent 59bc5ca046
commit 76897c9e69
7 changed files with 509 additions and 72 deletions

View File

@@ -5,7 +5,11 @@ import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { fn } from '@ember/helper';
import Icon from '#components/icon';
import { normalizeRelayUrl } from '../../../utils/nostr';
import {
excludeRequiredRelays,
mergeRequiredRelays,
normalizeRelayUrl,
} from '../../../utils/nostr';
const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : '');
@@ -17,6 +21,72 @@ export default class AppMenuSettingsNostr extends Component {
@tracked newReadRelay = '';
@tracked newWriteRelay = '';
get customReadRelays() {
return excludeRequiredRelays(
this.settings.nostrReadRelays || [],
this.nostrData.requiredReadRelays
);
}
get customWriteRelays() {
return excludeRequiredRelays(
this.settings.nostrWriteRelays || [],
this.nostrData.requiredWriteRelays
);
}
get readRelayExclusions() {
return this.settings.nostrReadRelayExclusions || [];
}
get writeRelayExclusions() {
return this.settings.nostrWriteRelayExclusions || [];
}
get requiredReadRelaySet() {
return new Set(this.nostrData.requiredReadRelays.filter(Boolean));
}
get requiredWriteRelaySet() {
return new Set(this.nostrData.requiredWriteRelays.filter(Boolean));
}
get mailboxReadRelaySet() {
return new Set(this.nostrData.mailboxReadRelays);
}
get mailboxWriteRelaySet() {
return new Set(this.nostrData.mailboxWriteRelays);
}
get hasReadOverrides() {
return this.customReadRelays.length > 0 || this.readRelayExclusions.length > 0;
}
get hasWriteOverrides() {
return (
this.customWriteRelays.length > 0 || this.writeRelayExclusions.length > 0
);
}
get readRelaysForDisplay() {
return this.nostrData.activeReadRelays.map((url) => {
return {
url,
isRequired: this.requiredReadRelaySet.has(url),
};
});
}
get writeRelaysForDisplay() {
return this.nostrData.activeWriteRelays.map((url) => {
return {
url,
isRequired: this.requiredWriteRelaySet.has(url),
};
});
}
@action
updateNewReadRelay(event) {
this.newReadRelay = event.target.value;
@@ -32,19 +102,48 @@ export default class AppMenuSettingsNostr extends Component {
const url = normalizeRelayUrl(this.newReadRelay);
if (!url) return;
const current =
this.settings.nostrReadRelays || this.nostrData.defaultReadRelays;
const set = new Set([...current, url]);
this.settings.update('nostrReadRelays', Array.from(set));
const merged = mergeRequiredRelays(this.nostrData.requiredReadRelays, [
...this.customReadRelays,
url,
]);
const custom = excludeRequiredRelays(merged, this.nostrData.requiredReadRelays);
const readExclusions = this.readRelayExclusions.filter((relay) => {
return normalizeRelayUrl(relay) !== url;
});
this.settings.update('nostrReadRelays', custom.length > 0 ? custom : null);
this.settings.update(
'nostrReadRelayExclusions',
readExclusions.length > 0 ? readExclusions : null
);
this.newReadRelay = '';
}
@action
removeReadRelay(url) {
const current =
this.settings.nostrReadRelays || this.nostrData.defaultReadRelays;
const filtered = current.filter((r) => r !== url);
this.settings.update('nostrReadRelays', filtered);
if (this.requiredReadRelaySet.has(url)) {
return;
}
const normalizedUrl = normalizeRelayUrl(url);
const remainingCustom = this.customReadRelays.filter((relay) => {
return normalizeRelayUrl(relay) !== normalizedUrl;
});
const nextExclusions = this.mailboxReadRelaySet.has(normalizedUrl)
? Array.from(new Set([...this.readRelayExclusions, normalizedUrl]))
: this.readRelayExclusions;
this.settings.update(
'nostrReadRelays',
remainingCustom.length > 0 ? remainingCustom : null
);
this.settings.update(
'nostrReadRelayExclusions',
nextExclusions.length > 0 ? nextExclusions : null
);
}
@action
@@ -64,6 +163,7 @@ export default class AppMenuSettingsNostr extends Component {
@action
resetReadRelays() {
this.settings.update('nostrReadRelays', null);
this.settings.update('nostrReadRelayExclusions', null);
}
@action
@@ -71,24 +171,57 @@ export default class AppMenuSettingsNostr extends Component {
const url = normalizeRelayUrl(this.newWriteRelay);
if (!url) return;
const current =
this.settings.nostrWriteRelays || this.nostrData.defaultWriteRelays;
const set = new Set([...current, url]);
this.settings.update('nostrWriteRelays', Array.from(set));
const merged = mergeRequiredRelays(this.nostrData.requiredWriteRelays, [
...this.customWriteRelays,
url,
]);
const custom = excludeRequiredRelays(
merged,
this.nostrData.requiredWriteRelays
);
const writeExclusions = this.writeRelayExclusions.filter((relay) => {
return normalizeRelayUrl(relay) !== url;
});
this.settings.update('nostrWriteRelays', custom.length > 0 ? custom : null);
this.settings.update(
'nostrWriteRelayExclusions',
writeExclusions.length > 0 ? writeExclusions : null
);
this.newWriteRelay = '';
}
@action
removeWriteRelay(url) {
const current =
this.settings.nostrWriteRelays || this.nostrData.defaultWriteRelays;
const filtered = current.filter((r) => r !== url);
this.settings.update('nostrWriteRelays', filtered);
if (this.requiredWriteRelaySet.has(url)) {
return;
}
const normalizedUrl = normalizeRelayUrl(url);
const remainingCustom = this.customWriteRelays.filter((relay) => {
return normalizeRelayUrl(relay) !== normalizedUrl;
});
const nextExclusions = this.mailboxWriteRelaySet.has(normalizedUrl)
? Array.from(new Set([...this.writeRelayExclusions, normalizedUrl]))
: this.writeRelayExclusions;
this.settings.update(
'nostrWriteRelays',
remainingCustom.length > 0 ? remainingCustom : null
);
this.settings.update(
'nostrWriteRelayExclusions',
nextExclusions.length > 0 ? nextExclusions : null
);
}
@action
resetWriteRelays() {
this.settings.update('nostrWriteRelays', null);
this.settings.update('nostrWriteRelayExclusions', null);
}
@action
@@ -112,18 +245,20 @@ export default class AppMenuSettingsNostr extends Component {
<div class="form-group">
<label for="new-read-relay">Read Relays</label>
<ul class="relay-list">
{{#each this.nostrData.activeReadRelays as |relay|}}
{{#each this.readRelaysForDisplay as |relay|}}
<li>
<span>{{stripProtocol relay}}</span>
<button
type="button"
class="btn-remove-relay"
title="Remove relay"
aria-label="Remove"
{{on "click" (fn this.removeReadRelay relay)}}
>
<Icon @name="x" @size={{14}} @color="currentColor" />
</button>
<span>{{stripProtocol relay.url}}</span>
{{#unless relay.isRequired}}
<button
type="button"
class="btn-remove-relay"
title="Remove relay"
aria-label="Remove"
{{on "click" (fn this.removeReadRelay relay.url)}}
>
<Icon @name="x" @size={{14}} @color="currentColor" />
</button>
{{/unless}}
</li>
{{/each}}
</ul>
@@ -143,7 +278,7 @@ export default class AppMenuSettingsNostr extends Component {
{{on "click" this.addReadRelay}}
>Add</button>
</div>
{{#if this.settings.nostrReadRelays}}
{{#if this.hasReadOverrides}}
<button
type="button"
class="btn-link reset-relays"
@@ -157,18 +292,20 @@ export default class AppMenuSettingsNostr extends Component {
<div class="form-group">
<label for="new-write-relay">Write Relays</label>
<ul class="relay-list">
{{#each this.nostrData.activeWriteRelays as |relay|}}
{{#each this.writeRelaysForDisplay as |relay|}}
<li>
<span>{{stripProtocol relay}}</span>
<button
type="button"
class="btn-remove-relay"
title="Remove relay"
aria-label="Remove"
{{on "click" (fn this.removeWriteRelay relay)}}
>
<Icon @name="x" @size={{14}} @color="currentColor" />
</button>
<span>{{stripProtocol relay.url}}</span>
{{#unless relay.isRequired}}
<button
type="button"
class="btn-remove-relay"
title="Remove relay"
aria-label="Remove"
{{on "click" (fn this.removeWriteRelay relay.url)}}
>
<Icon @name="x" @size={{14}} @color="currentColor" />
</button>
{{/unless}}
</li>
{{/each}}
</ul>
@@ -188,7 +325,7 @@ export default class AppMenuSettingsNostr extends Component {
{{on "click" this.addWriteRelay}}
>Add</button>
</div>
{{#if this.settings.nostrWriteRelays}}
{{#if this.hasWriteOverrides}}
<button
type="button"
class="btn-link reset-relays"

View File

@@ -6,7 +6,12 @@ import { MailboxesModel } from 'applesauce-core/models/mailboxes';
import { npubEncode } from 'applesauce-core/helpers/pointers';
import { persistEventsToCache } from 'applesauce-core/helpers/event-cache';
import { NostrIDB, openDB } from 'nostr-idb';
import { normalizeRelayUrl } from '../utils/nostr';
import {
excludeRequiredRelays,
mergeRequiredRelays,
normalizeRelayUrl,
uniqNormalizedRelays,
} from '../utils/nostr';
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
const DIRECTORY_RELAYS = [
@@ -83,43 +88,62 @@ export default class NostrDataService extends Service {
});
}
get defaultReadRelays() {
const mailboxes = (this.mailboxes?.inboxes || [])
.map(normalizeRelayUrl)
.filter(Boolean);
const defaults = DEFAULT_READ_RELAYS.map(normalizeRelayUrl).filter(Boolean);
return Array.from(new Set([...defaults, ...mailboxes]));
get requiredReadRelays() {
return DEFAULT_READ_RELAYS;
}
get defaultWriteRelays() {
const mailboxes = (this.mailboxes?.outboxes || [])
get requiredWriteRelays() {
return DEFAULT_WRITE_RELAYS;
}
get mailboxReadRelays() {
return (this.mailboxes?.inboxes || [])
.map(normalizeRelayUrl)
.filter(Boolean);
const defaults =
DEFAULT_WRITE_RELAYS.map(normalizeRelayUrl).filter(Boolean);
return Array.from(new Set([...defaults, ...mailboxes]));
}
get mailboxWriteRelays() {
return (this.mailboxes?.outboxes || [])
.map(normalizeRelayUrl)
.filter(Boolean);
}
get configuredReadRelays() {
const configured = uniqNormalizedRelays([
...this.mailboxReadRelays,
...(this.settings.nostrReadRelays || []),
]);
return excludeRequiredRelays(
configured,
this.settings.nostrReadRelayExclusions || []
);
}
get configuredWriteRelays() {
const configured = uniqNormalizedRelays([
...this.mailboxWriteRelays,
...(this.settings.nostrWriteRelays || []),
]);
return excludeRequiredRelays(
configured,
this.settings.nostrWriteRelayExclusions || []
);
}
get activeReadRelays() {
if (this.settings.nostrReadRelays) {
return Array.from(
new Set(
this.settings.nostrReadRelays.map(normalizeRelayUrl).filter(Boolean)
)
);
}
return this.defaultReadRelays;
return mergeRequiredRelays(
this.requiredReadRelays,
this.configuredReadRelays
);
}
get activeWriteRelays() {
if (this.settings.nostrWriteRelays) {
return Array.from(
new Set(
this.settings.nostrWriteRelays.map(normalizeRelayUrl).filter(Boolean)
)
);
}
return this.defaultWriteRelays;
return mergeRequiredRelays(
this.requiredWriteRelays,
this.configuredWriteRelays
);
}
async loadPlacesInBounds(bbox) {

View File

@@ -9,6 +9,8 @@ const DEFAULT_SETTINGS = {
nostrPhotoFallbackUploads: false,
nostrReadRelays: null,
nostrWriteRelays: null,
nostrReadRelayExclusions: null,
nostrWriteRelayExclusions: null,
experimentalEnablePhotoDeletion: false,
};
@@ -21,6 +23,9 @@ export default class SettingsService extends Service {
DEFAULT_SETTINGS.nostrPhotoFallbackUploads;
@tracked nostrReadRelays = DEFAULT_SETTINGS.nostrReadRelays;
@tracked nostrWriteRelays = DEFAULT_SETTINGS.nostrWriteRelays;
@tracked nostrReadRelayExclusions = DEFAULT_SETTINGS.nostrReadRelayExclusions;
@tracked nostrWriteRelayExclusions =
DEFAULT_SETTINGS.nostrWriteRelayExclusions;
@tracked experimentalEnablePhotoDeletion =
DEFAULT_SETTINGS.experimentalEnablePhotoDeletion;
@@ -111,6 +116,8 @@ export default class SettingsService extends Service {
this.nostrPhotoFallbackUploads = finalSettings.nostrPhotoFallbackUploads;
this.nostrReadRelays = finalSettings.nostrReadRelays;
this.nostrWriteRelays = finalSettings.nostrWriteRelays;
this.nostrReadRelayExclusions = finalSettings.nostrReadRelayExclusions;
this.nostrWriteRelayExclusions = finalSettings.nostrWriteRelayExclusions;
this.experimentalEnablePhotoDeletion =
finalSettings.experimentalEnablePhotoDeletion;
@@ -127,6 +134,8 @@ export default class SettingsService extends Service {
nostrPhotoFallbackUploads: this.nostrPhotoFallbackUploads,
nostrReadRelays: this.nostrReadRelays,
nostrWriteRelays: this.nostrWriteRelays,
nostrReadRelayExclusions: this.nostrReadRelayExclusions,
nostrWriteRelayExclusions: this.nostrWriteRelayExclusions,
experimentalEnablePhotoDeletion: this.experimentalEnablePhotoDeletion,
};
localStorage.setItem('marco:settings', JSON.stringify(settings));

View File

@@ -14,6 +14,30 @@ export function normalizeRelayUrl(url) {
return normalized;
}
export function uniqNormalizedRelays(relays = []) {
return Array.from(new Set(relays.map(normalizeRelayUrl).filter(Boolean)));
}
export function mergeRequiredRelays(requiredRelays = [], customRelays = []) {
const requiredSet = new Set(requiredRelays.filter(Boolean));
const merged = [...requiredRelays.filter(Boolean)];
for (const relay of uniqNormalizedRelays(customRelays)) {
if (!requiredSet.has(relay)) {
merged.push(relay);
}
}
return merged;
}
export function excludeRequiredRelays(customRelays = [], requiredRelays = []) {
const requiredSet = new Set(requiredRelays.filter(Boolean));
return uniqNormalizedRelays(customRelays).filter((relay) => {
return !requiredSet.has(relay);
});
}
/**
* Extracts and normalizes photo data from NIP-360 (Place Photos) events.
* Sorts chronologically and guarantees the first landscape photo (or first portrait) is at index 0.