Add media server selection to Nostr settings
CI / Lint (pull_request) Successful in 1m3s
CI / Test (pull_request) Successful in 1m16s
Release Drafter / Update release notes draft (pull_request) Successful in 5s

Also, hide the fallback option when there's no server to fall back to
This commit is contained in:
2026-08-21 11:27:03 -06:00
parent cdeb4f5d8c
commit 2746ec035d
5 changed files with 110 additions and 23 deletions
+56 -19
View File
@@ -4,12 +4,14 @@ import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { fn } from '@ember/helper'; import { fn } from '@ember/helper';
import eq from 'ember-truth-helpers/helpers/eq';
import Icon from '#components/icon'; import Icon from '#components/icon';
import { import {
excludeRequiredRelays, excludeRequiredRelays,
mergeRequiredRelays, mergeRequiredRelays,
normalizeRelayUrl, normalizeRelayUrl,
} from '../../../utils/nostr'; } from '../../../utils/nostr';
import { DEFAULT_BLOSSOM_SERVER } from '../../../services/blossom';
const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : ''); const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : '');
@@ -89,6 +91,15 @@ export default class AppMenuSettingsNostr extends Component {
}); });
} }
get mediaServerOptions() {
const servers = this.nostrData.blossomServers;
return servers.length ? servers : [DEFAULT_BLOSSOM_SERVER];
}
get hasMultipleMediaServers() {
return this.mediaServerOptions.length > 1;
}
@action @action
updateNewReadRelay(event) { updateNewReadRelay(event) {
this.newReadRelay = event.target.value; this.newReadRelay = event.target.value;
@@ -342,31 +353,57 @@ export default class AppMenuSettingsNostr extends Component {
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="nostr-photo-fallback-uploads">Upload photos to fallback <label for="nostr-media-server">Media server</label>
servers</label>
<select <select
id="nostr-photo-fallback-uploads" id="nostr-media-server"
class="form-control" class="form-control"
{{on "change" (fn @onChange "nostrPhotoFallbackUploads")}} {{on "change" (fn @onChange "nostrMediaServer")}}
> >
<option {{#each this.mediaServerOptions as |server|}}
value="true" <option
selected={{if this.settings.nostrPhotoFallbackUploads "selected"}} value={{server}}
> selected={{if
Yes (eq server this.settings.nostrMediaServer)
</option> "selected"
<option }}
value="false" >
selected={{unless {{server}}
this.settings.nostrPhotoFallbackUploads </option>
"selected" {{/each}}
}}
>
No
</option>
</select> </select>
</div> </div>
{{#if this.hasMultipleMediaServers}}
<div class="form-group">
<label for="nostr-photo-fallback-uploads">Upload photos to fallback
servers</label>
<select
id="nostr-photo-fallback-uploads"
class="form-control"
{{on "change" (fn @onChange "nostrPhotoFallbackUploads")}}
>
<option
value="true"
selected={{if
this.settings.nostrPhotoFallbackUploads
"selected"
}}
>
Yes
</option>
<option
value="false"
selected={{unless
this.settings.nostrPhotoFallbackUploads
"selected"
}}
>
No
</option>
</select>
</div>
{{/if}}
<div class="form-group"> <div class="form-group">
<label>Cached data</label> <label>Cached data</label>
<button <button
+5 -4
View File
@@ -24,14 +24,15 @@ export default class BlossomService extends Service {
@service settings; @service settings;
get servers() { get servers() {
const servers = this.nostrData.blossomServers; const discovered = this.nostrData.blossomServers;
const allServers = servers.length ? servers : [DEFAULT_BLOSSOM_SERVER]; const mainServer = this.settings.nostrMediaServer || DEFAULT_BLOSSOM_SERVER;
const fallbacks = discovered.filter((server) => server !== mainServer);
if (!this.settings.nostrPhotoFallbackUploads) { if (!this.settings.nostrPhotoFallbackUploads) {
return [allServers[0]]; return [mainServer];
} }
return allServers; return [mainServer, ...fallbacks];
} }
async _getAuthHeader(action, hash, serverUrl) { async _getAuthHeader(action, hash, serverUrl) {
+11
View File
@@ -15,6 +15,7 @@ import {
uniqNormalizedRelays, uniqNormalizedRelays,
} from '../utils/nostr'; } from '../utils/nostr';
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage'; import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
import { DEFAULT_BLOSSOM_SERVER } from './blossom';
const DIRECTORY_RELAYS = [ const DIRECTORY_RELAYS = [
'wss://relay.primal.net', 'wss://relay.primal.net',
@@ -437,6 +438,16 @@ export default class NostrDataService extends Service {
} else { } else {
this.blossomServers = []; this.blossomServers = [];
} }
if (this.blossomServers.length > 0) {
const current = this.settings.nostrMediaServer;
if (
current === DEFAULT_BLOSSOM_SERVER ||
!this.blossomServers.includes(current)
) {
this.settings.update('nostrMediaServer', this.blossomServers[0]);
}
}
}); });
// 1. Await cache initialization and populate the EventStore with local data // 1. Await cache initialization and populate the EventStore with local data
+6
View File
@@ -1,12 +1,15 @@
import Service from '@ember/service'; import Service from '@ember/service';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { DEFAULT_BLOSSOM_SERVER } from './blossom';
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
overpassApi: 'https://overpass-api.de/api/interpreter', overpassApi: 'https://overpass-api.de/api/interpreter',
mapKinetic: true, mapKinetic: true,
photonApi: 'https://photon.komoot.io/api/', photonApi: 'https://photon.komoot.io/api/',
showQuickSearchButtons: true, showQuickSearchButtons: true,
nostrPhotoFallbackUploads: false, nostrPhotoFallbackUploads: false,
nostrMediaServer: DEFAULT_BLOSSOM_SERVER,
nostrReadRelays: null, nostrReadRelays: null,
nostrWriteRelays: null, nostrWriteRelays: null,
nostrReadRelayExclusions: null, nostrReadRelayExclusions: null,
@@ -21,6 +24,7 @@ export default class SettingsService extends Service {
@tracked showQuickSearchButtons = DEFAULT_SETTINGS.showQuickSearchButtons; @tracked showQuickSearchButtons = DEFAULT_SETTINGS.showQuickSearchButtons;
@tracked nostrPhotoFallbackUploads = @tracked nostrPhotoFallbackUploads =
DEFAULT_SETTINGS.nostrPhotoFallbackUploads; DEFAULT_SETTINGS.nostrPhotoFallbackUploads;
@tracked nostrMediaServer = DEFAULT_SETTINGS.nostrMediaServer;
@tracked nostrReadRelays = DEFAULT_SETTINGS.nostrReadRelays; @tracked nostrReadRelays = DEFAULT_SETTINGS.nostrReadRelays;
@tracked nostrWriteRelays = DEFAULT_SETTINGS.nostrWriteRelays; @tracked nostrWriteRelays = DEFAULT_SETTINGS.nostrWriteRelays;
@tracked nostrReadRelayExclusions = DEFAULT_SETTINGS.nostrReadRelayExclusions; @tracked nostrReadRelayExclusions = DEFAULT_SETTINGS.nostrReadRelayExclusions;
@@ -114,6 +118,7 @@ export default class SettingsService extends Service {
this.photonApi = finalSettings.photonApi; this.photonApi = finalSettings.photonApi;
this.showQuickSearchButtons = finalSettings.showQuickSearchButtons; this.showQuickSearchButtons = finalSettings.showQuickSearchButtons;
this.nostrPhotoFallbackUploads = finalSettings.nostrPhotoFallbackUploads; this.nostrPhotoFallbackUploads = finalSettings.nostrPhotoFallbackUploads;
this.nostrMediaServer = finalSettings.nostrMediaServer;
this.nostrReadRelays = finalSettings.nostrReadRelays; this.nostrReadRelays = finalSettings.nostrReadRelays;
this.nostrWriteRelays = finalSettings.nostrWriteRelays; this.nostrWriteRelays = finalSettings.nostrWriteRelays;
this.nostrReadRelayExclusions = finalSettings.nostrReadRelayExclusions; this.nostrReadRelayExclusions = finalSettings.nostrReadRelayExclusions;
@@ -132,6 +137,7 @@ export default class SettingsService extends Service {
photonApi: this.photonApi, photonApi: this.photonApi,
showQuickSearchButtons: this.showQuickSearchButtons, showQuickSearchButtons: this.showQuickSearchButtons,
nostrPhotoFallbackUploads: this.nostrPhotoFallbackUploads, nostrPhotoFallbackUploads: this.nostrPhotoFallbackUploads,
nostrMediaServer: this.nostrMediaServer,
nostrReadRelays: this.nostrReadRelays, nostrReadRelays: this.nostrReadRelays,
nostrWriteRelays: this.nostrWriteRelays, nostrWriteRelays: this.nostrWriteRelays,
nostrReadRelayExclusions: this.nostrReadRelayExclusions, nostrReadRelayExclusions: this.nostrReadRelayExclusions,
@@ -8,6 +8,7 @@ import {
mergeRequiredRelays, mergeRequiredRelays,
uniqNormalizedRelays, uniqNormalizedRelays,
} from 'marco/utils/nostr'; } from 'marco/utils/nostr';
import { DEFAULT_BLOSSOM_SERVER } from 'marco/services/blossom';
class MockNostrDataService extends Service { class MockNostrDataService extends Service {
@service settings; @service settings;
@@ -18,6 +19,8 @@ class MockNostrDataService extends Service {
mailboxReadRelays = ['wss://mailbox.example.com']; mailboxReadRelays = ['wss://mailbox.example.com'];
mailboxWriteRelays = ['wss://mailbox-write.example.com']; mailboxWriteRelays = ['wss://mailbox-write.example.com'];
blossomServers = [];
get configuredReadRelays() { get configuredReadRelays() {
const configured = uniqNormalizedRelays([ const configured = uniqNormalizedRelays([
...this.mailboxReadRelays, ...this.mailboxReadRelays,
@@ -185,4 +188,33 @@ module('Integration | Component | app-menu/settings/nostr', function (hooks) {
'wss://mailbox-write.example.com', 'wss://mailbox-write.example.com',
]); ]);
}); });
test('media server dropdown shows only the default server when no blossom servers discovered', async function (assert) {
const element = await renderAndOpenDetails(this);
const select = element.querySelector('#nostr-media-server');
assert.dom(select).exists();
assert.strictEqual(select.options.length, 1);
assert.dom(select.options[0]).hasValue(DEFAULT_BLOSSOM_SERVER);
assert.dom(select.options[0]).hasText(DEFAULT_BLOSSOM_SERVER);
assert.strictEqual(select.value, DEFAULT_BLOSSOM_SERVER);
assert
.dom('#nostr-photo-fallback-uploads')
.doesNotExist('fallback toggle hidden with single media server');
});
test('media server dropdown lists discovered blossom servers', async function (assert) {
const servers = ['https://cdn.example.com', 'https://cdn.satellite.earth'];
this.owner.lookup('service:nostrData').blossomServers = servers;
const element = await renderAndOpenDetails(this);
const select = element.querySelector('#nostr-media-server');
assert.strictEqual(select.options.length, servers.length);
assert.dom(select.options[0]).hasValue(servers[0]);
assert.dom(select.options[1]).hasValue(servers[1]);
assert
.dom('#nostr-photo-fallback-uploads')
.exists('fallback toggle visible with multiple media servers');
});
}); });