Compare commits

...
5 Commits
Author SHA1 Message Date
raucao 46bb2efc70 1.30.0
CI / Lint (push) Successful in 40s
CI / Test (push) Successful in 54s
2026-08-21 11:35:31 -06:00
raucao 6feb776247 Merge pull request 'Add media server selection to Nostr settings' (#82) from feature/blossom_settings into master
CI / Lint (push) Successful in 1m1s
CI / Test (push) Successful in 1m14s
Reviewed-on: #82
2026-08-21 17:34:18 +00:00
raucao 2746ec035d 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
2026-08-21 11:27:03 -06:00
raucao cdeb4f5d8c 1.29.1
CI / Lint (push) Successful in 1m0s
CI / Test (push) Successful in 1m14s
2026-08-20 17:04:53 -06:00
raucao 8494f5a084 Fix button width 2026-08-20 17:04:04 -06:00
14 changed files with 122 additions and 30 deletions
+38 -1
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;
@@ -341,6 +352,28 @@ export default class AppMenuSettingsNostr extends Component {
{{/if}} {{/if}}
</div> </div>
<div class="form-group">
<label for="nostr-media-server">Media server</label>
<select
id="nostr-media-server"
class="form-control"
{{on "change" (fn @onChange "nostrMediaServer")}}
>
{{#each this.mediaServerOptions as |server|}}
<option
value={{server}}
selected={{if
(eq server this.settings.nostrMediaServer)
"selected"
}}
>
{{server}}
</option>
{{/each}}
</select>
</div>
{{#if this.hasMultipleMediaServers}}
<div class="form-group"> <div class="form-group">
<label for="nostr-photo-fallback-uploads">Upload photos to fallback <label for="nostr-photo-fallback-uploads">Upload photos to fallback
servers</label> servers</label>
@@ -351,7 +384,10 @@ export default class AppMenuSettingsNostr extends Component {
> >
<option <option
value="true" value="true"
selected={{if this.settings.nostrPhotoFallbackUploads "selected"}} selected={{if
this.settings.nostrPhotoFallbackUploads
"selected"
}}
> >
Yes Yes
</option> </option>
@@ -366,6 +402,7 @@ export default class AppMenuSettingsNostr extends Component {
</option> </option>
</select> </select>
</div> </div>
{{/if}}
<div class="form-group"> <div class="form-group">
<label>Cached data</label> <label>Cached data</label>
+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,
+5
View File
@@ -1093,6 +1093,11 @@ abbr[title] {
} }
.btn { .btn {
/* TODO: remove this scoped rule in favor of a global box-sizing reset
(e.g. `*, *::before, *::after { box-sizing: border-box; }`) at the top
of this file. Needed so `width: 100%` .btn elements like <a class="btn-full">
include padding within their width like <button> does. */
box-sizing: border-box;
padding: 0.6rem 1.2rem; padding: 0.6rem 1.2rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.29.0", "version": "1.30.0",
"private": true, "private": true,
"description": "Unhosted maps app", "description": "Unhosted maps app",
"repository": { "repository": {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -39,8 +39,8 @@
<meta name="msapplication-TileColor" content="#F6E9A6"> <meta name="msapplication-TileColor" content="#F6E9A6">
<meta name="msapplication-TileImage" content="/icons/icon-144.png"> <meta name="msapplication-TileImage" content="/icons/icon-144.png">
<script type="module" crossorigin src="/assets/main-Cx0KJENk.js"></script> <script type="module" crossorigin src="/assets/main-Dw0KrEfS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CHNrhL7t.css"> <link rel="stylesheet" crossorigin href="/assets/main-DWxZH-9E.css">
</head> </head>
<body> <body>
<div id="modal-portal"></div> <div id="modal-portal"></div>
@@ -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');
});
}); });