Compare commits

..

13 Commits

Author SHA1 Message Date
raucao f01730fef5 Add icon and quick search results for tourist information
CI / Lint (pull_request) Successful in 33s
CI / Test (pull_request) Successful in 58s
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2026-06-29 17:32:26 +02:00
raucao 448c51bab6 Add icons for city gates and historic towers 2026-06-29 17:28:57 +02:00
raucao 0bcbae374b Use "yes" tag values only as fallbacks if there isn't a more specific
OSM key
2026-06-29 17:28:00 +02:00
raucao c33fe3b268 Add icon for car repair shops 2026-06-29 17:08:55 +02:00
raucao 18bda60310 Add guest houses to hotel quick search 2026-06-29 17:02:43 +02:00
raucao 86d25dc6ba Add OSM links for custom saved places
Link to the OSM search route, so we get a pin when opening OSM from
Marco
2026-06-29 16:45:49 +02:00
raucao 5b8bec6a00 Cut off overlong sidebar link texts with ellipses 2026-06-29 16:06:55 +02:00
raucao f2c2eb1fdc Add icon for amenity=townhall 2026-06-29 15:55:28 +02:00
raucao b42c4881f6 Merge pull request 'Add missing icons, test for missing icons in rules' (#62) from bugfix/icons into master
CI / Lint (push) Successful in 34s
CI / Test (push) Successful in 58s
Reviewed-on: #62
2026-06-29 13:38:34 +00:00
raucao b18e299eca Add missing icons, test for missing icons in rules
CI / Lint (pull_request) Successful in 34s
CI / Test (pull_request) Successful in 59s
Release Drafter / Update release notes draft (pull_request) Successful in 5s
2026-06-29 15:32:48 +02:00
raucao 401ed41fcd Merge pull request 'Turn default relays into required relays' (#61) from nostr/required_relays into master
CI / Lint (push) Successful in 31s
CI / Test (push) Successful in 56s
Reviewed-on: #61
2026-06-07 12:30:38 +00:00
raucao 504e8fab94 Fix lint errors
CI / Lint (pull_request) Successful in 31s
CI / Test (pull_request) Successful in 56s
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2026-06-07 16:28:09 +04:00
raucao 76897c9e69 Turn default relays into required relays
CI / Lint (pull_request) Failing after 31s
CI / Test (pull_request) Successful in 55s
2026-06-07 16:21:26 +04:00
18 changed files with 724 additions and 101 deletions
+183 -41
View File
@@ -5,7 +5,11 @@ 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 Icon from '#components/icon'; import Icon from '#components/icon';
import { normalizeRelayUrl } from '../../../utils/nostr'; import {
excludeRequiredRelays,
mergeRequiredRelays,
normalizeRelayUrl,
} from '../../../utils/nostr';
const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : ''); const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : '');
@@ -17,6 +21,74 @@ export default class AppMenuSettingsNostr extends Component {
@tracked newReadRelay = ''; @tracked newReadRelay = '';
@tracked newWriteRelay = ''; @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 @action
updateNewReadRelay(event) { updateNewReadRelay(event) {
this.newReadRelay = event.target.value; this.newReadRelay = event.target.value;
@@ -32,19 +104,51 @@ export default class AppMenuSettingsNostr extends Component {
const url = normalizeRelayUrl(this.newReadRelay); const url = normalizeRelayUrl(this.newReadRelay);
if (!url) return; if (!url) return;
const current = const merged = mergeRequiredRelays(this.nostrData.requiredReadRelays, [
this.settings.nostrReadRelays || this.nostrData.defaultReadRelays; ...this.customReadRelays,
const set = new Set([...current, url]); url,
this.settings.update('nostrReadRelays', Array.from(set)); ]);
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 = ''; this.newReadRelay = '';
} }
@action @action
removeReadRelay(url) { removeReadRelay(url) {
const current = if (this.requiredReadRelaySet.has(url)) {
this.settings.nostrReadRelays || this.nostrData.defaultReadRelays; return;
const filtered = current.filter((r) => r !== url); }
this.settings.update('nostrReadRelays', filtered);
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 @action
@@ -64,6 +168,7 @@ export default class AppMenuSettingsNostr extends Component {
@action @action
resetReadRelays() { resetReadRelays() {
this.settings.update('nostrReadRelays', null); this.settings.update('nostrReadRelays', null);
this.settings.update('nostrReadRelayExclusions', null);
} }
@action @action
@@ -71,24 +176,57 @@ export default class AppMenuSettingsNostr extends Component {
const url = normalizeRelayUrl(this.newWriteRelay); const url = normalizeRelayUrl(this.newWriteRelay);
if (!url) return; if (!url) return;
const current = const merged = mergeRequiredRelays(this.nostrData.requiredWriteRelays, [
this.settings.nostrWriteRelays || this.nostrData.defaultWriteRelays; ...this.customWriteRelays,
const set = new Set([...current, url]); url,
this.settings.update('nostrWriteRelays', Array.from(set)); ]);
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 = ''; this.newWriteRelay = '';
} }
@action @action
removeWriteRelay(url) { removeWriteRelay(url) {
const current = if (this.requiredWriteRelaySet.has(url)) {
this.settings.nostrWriteRelays || this.nostrData.defaultWriteRelays; return;
const filtered = current.filter((r) => r !== url); }
this.settings.update('nostrWriteRelays', filtered);
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 @action
resetWriteRelays() { resetWriteRelays() {
this.settings.update('nostrWriteRelays', null); this.settings.update('nostrWriteRelays', null);
this.settings.update('nostrWriteRelayExclusions', null);
} }
@action @action
@@ -112,18 +250,20 @@ export default class AppMenuSettingsNostr extends Component {
<div class="form-group"> <div class="form-group">
<label for="new-read-relay">Read Relays</label> <label for="new-read-relay">Read Relays</label>
<ul class="relay-list"> <ul class="relay-list">
{{#each this.nostrData.activeReadRelays as |relay|}} {{#each this.readRelaysForDisplay as |relay|}}
<li> <li>
<span>{{stripProtocol relay}}</span> <span>{{stripProtocol relay.url}}</span>
<button {{#unless relay.isRequired}}
type="button" <button
class="btn-remove-relay" type="button"
title="Remove relay" class="btn-remove-relay"
aria-label="Remove" title="Remove relay"
{{on "click" (fn this.removeReadRelay relay)}} aria-label="Remove"
> {{on "click" (fn this.removeReadRelay relay.url)}}
<Icon @name="x" @size={{14}} @color="currentColor" /> >
</button> <Icon @name="x" @size={{14}} @color="currentColor" />
</button>
{{/unless}}
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
@@ -143,7 +283,7 @@ export default class AppMenuSettingsNostr extends Component {
{{on "click" this.addReadRelay}} {{on "click" this.addReadRelay}}
>Add</button> >Add</button>
</div> </div>
{{#if this.settings.nostrReadRelays}} {{#if this.hasReadOverrides}}
<button <button
type="button" type="button"
class="btn-link reset-relays" class="btn-link reset-relays"
@@ -157,18 +297,20 @@ export default class AppMenuSettingsNostr extends Component {
<div class="form-group"> <div class="form-group">
<label for="new-write-relay">Write Relays</label> <label for="new-write-relay">Write Relays</label>
<ul class="relay-list"> <ul class="relay-list">
{{#each this.nostrData.activeWriteRelays as |relay|}} {{#each this.writeRelaysForDisplay as |relay|}}
<li> <li>
<span>{{stripProtocol relay}}</span> <span>{{stripProtocol relay.url}}</span>
<button {{#unless relay.isRequired}}
type="button" <button
class="btn-remove-relay" type="button"
title="Remove relay" class="btn-remove-relay"
aria-label="Remove" title="Remove relay"
{{on "click" (fn this.removeWriteRelay relay)}} aria-label="Remove"
> {{on "click" (fn this.removeWriteRelay relay.url)}}
<Icon @name="x" @size={{14}} @color="currentColor" /> >
</button> <Icon @name="x" @size={{14}} @color="currentColor" />
</button>
{{/unless}}
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
@@ -188,7 +330,7 @@ export default class AppMenuSettingsNostr extends Component {
{{on "click" this.addWriteRelay}} {{on "click" this.addWriteRelay}}
>Add</button> >Add</button>
</div> </div>
{{#if this.settings.nostrWriteRelays}} {{#if this.hasWriteOverrides}}
<button <button
type="button" type="button"
class="btn-link reset-relays" class="btn-link reset-relays"
+3 -21
View File
@@ -20,7 +20,6 @@ import { Style, Circle, Fill, Stroke, Icon } from 'ol/style.js';
import { apply } from 'ol-mapbox-style'; import { apply } from 'ol-mapbox-style';
import { getIcon } from '../utils/icons'; import { getIcon } from '../utils/icons';
import { getIconNameForTags } from '../utils/osm-icons'; import { getIconNameForTags } from '../utils/osm-icons';
import config from 'marco/config/environment';
export default class MapComponent extends Component { export default class MapComponent extends Component {
@service osm; @service osm;
@@ -287,26 +286,9 @@ export default class MapComponent extends Component {
this.mapUi.updateCenter(initialCenter[1], initialCenter[0]); this.mapUi.updateCenter(initialCenter[1], initialCenter[0]);
this.mapUi.updateZoom(view.getZoom()); this.mapUi.updateZoom(view.getZoom());
if (config.environment === 'test') { apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty', {
apply(this.mapInstance, { webfonts: 'data:text/css,',
version: 8, });
name: 'Test Style',
sources: {},
layers: [
{
id: 'background',
type: 'background',
paint: {
'background-color': '#f8f4f0',
},
},
],
});
} else {
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty', {
webfonts: 'data:text/css,',
});
}
this.searchOverlayElement = document.createElement('div'); this.searchOverlayElement = document.createElement('div');
this.searchOverlayElement.className = 'search-pulse'; this.searchOverlayElement.className = 'search-pulse';
+17 -4
View File
@@ -23,6 +23,7 @@ export default class PlaceDetails extends Component {
@service storage; @service storage;
@service nostrAuth; @service nostrAuth;
@service nostrData; @service nostrData;
@service mapUi;
@tracked isEditing = false; @tracked isEditing = false;
@tracked showLists = false; @tracked showLists = false;
@tracked isPhotoUploadActive = false; @tracked isPhotoUploadActive = false;
@@ -345,9 +346,21 @@ export default class PlaceDetails extends Component {
get osmUrl() { get osmUrl() {
const id = this.place.osmId; const id = this.place.osmId;
if (!id) return null; if (id) {
const type = this.place.osmType || 'node'; const type = this.place.osmType || 'node';
return `https://www.openstreetmap.org/${type}/${id}`; return `https://www.openstreetmap.org/${type}/${id}`;
}
const lat = this.place.lat;
const lon = this.place.lon;
if (!lat || !lon) return null;
const viewLat = this.mapUi.currentCenter?.lat ?? lat;
const viewLon = this.mapUi.currentCenter?.lon ?? lon;
const zoom = this.mapUi.currentZoom ?? 17;
const roundedZoom = Math.round(zoom);
return `https://www.openstreetmap.org/search?lat=${lat}&lon=${lon}&zoom=${roundedZoom}#map=${roundedZoom}/${Number(viewLat).toFixed(5)}/${Number(viewLon).toFixed(5)}`;
} }
get gmapsUrl() { get gmapsUrl() {
@@ -591,7 +604,7 @@ export default class PlaceDetails extends Component {
</div> </div>
{{#if this.osmUrl}} {{#if this.place.osmId}}
<div class="meta-info"> <div class="meta-info">
<p class="content-with-icon"> <p class="content-with-icon">
<Icon @name="feather-camera" /> <Icon @name="feather-camera" />
+52 -28
View File
@@ -6,7 +6,12 @@ import { MailboxesModel } from 'applesauce-core/models/mailboxes';
import { npubEncode } from 'applesauce-core/helpers/pointers'; import { npubEncode } from 'applesauce-core/helpers/pointers';
import { persistEventsToCache } from 'applesauce-core/helpers/event-cache'; import { persistEventsToCache } from 'applesauce-core/helpers/event-cache';
import { NostrIDB, openDB } from 'nostr-idb'; 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'; import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
const DIRECTORY_RELAYS = [ const DIRECTORY_RELAYS = [
@@ -83,43 +88,62 @@ export default class NostrDataService extends Service {
}); });
} }
get defaultReadRelays() { get requiredReadRelays() {
const mailboxes = (this.mailboxes?.inboxes || []) return DEFAULT_READ_RELAYS;
.map(normalizeRelayUrl)
.filter(Boolean);
const defaults = DEFAULT_READ_RELAYS.map(normalizeRelayUrl).filter(Boolean);
return Array.from(new Set([...defaults, ...mailboxes]));
} }
get defaultWriteRelays() { get requiredWriteRelays() {
const mailboxes = (this.mailboxes?.outboxes || []) return DEFAULT_WRITE_RELAYS;
}
get mailboxReadRelays() {
return (this.mailboxes?.inboxes || [])
.map(normalizeRelayUrl) .map(normalizeRelayUrl)
.filter(Boolean); .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() { get activeReadRelays() {
if (this.settings.nostrReadRelays) { return mergeRequiredRelays(
return Array.from( this.requiredReadRelays,
new Set( this.configuredReadRelays
this.settings.nostrReadRelays.map(normalizeRelayUrl).filter(Boolean) );
)
);
}
return this.defaultReadRelays;
} }
get activeWriteRelays() { get activeWriteRelays() {
if (this.settings.nostrWriteRelays) { return mergeRequiredRelays(
return Array.from( this.requiredWriteRelays,
new Set( this.configuredWriteRelays
this.settings.nostrWriteRelays.map(normalizeRelayUrl).filter(Boolean) );
)
);
}
return this.defaultWriteRelays;
} }
async loadPlacesInBounds(bbox) { async loadPlacesInBounds(bbox) {
+9
View File
@@ -9,6 +9,8 @@ const DEFAULT_SETTINGS = {
nostrPhotoFallbackUploads: false, nostrPhotoFallbackUploads: false,
nostrReadRelays: null, nostrReadRelays: null,
nostrWriteRelays: null, nostrWriteRelays: null,
nostrReadRelayExclusions: null,
nostrWriteRelayExclusions: null,
experimentalEnablePhotoDeletion: false, experimentalEnablePhotoDeletion: false,
}; };
@@ -21,6 +23,9 @@ export default class SettingsService extends Service {
DEFAULT_SETTINGS.nostrPhotoFallbackUploads; DEFAULT_SETTINGS.nostrPhotoFallbackUploads;
@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 nostrWriteRelayExclusions =
DEFAULT_SETTINGS.nostrWriteRelayExclusions;
@tracked experimentalEnablePhotoDeletion = @tracked experimentalEnablePhotoDeletion =
DEFAULT_SETTINGS.experimentalEnablePhotoDeletion; DEFAULT_SETTINGS.experimentalEnablePhotoDeletion;
@@ -111,6 +116,8 @@ export default class SettingsService extends Service {
this.nostrPhotoFallbackUploads = finalSettings.nostrPhotoFallbackUploads; this.nostrPhotoFallbackUploads = finalSettings.nostrPhotoFallbackUploads;
this.nostrReadRelays = finalSettings.nostrReadRelays; this.nostrReadRelays = finalSettings.nostrReadRelays;
this.nostrWriteRelays = finalSettings.nostrWriteRelays; this.nostrWriteRelays = finalSettings.nostrWriteRelays;
this.nostrReadRelayExclusions = finalSettings.nostrReadRelayExclusions;
this.nostrWriteRelayExclusions = finalSettings.nostrWriteRelayExclusions;
this.experimentalEnablePhotoDeletion = this.experimentalEnablePhotoDeletion =
finalSettings.experimentalEnablePhotoDeletion; finalSettings.experimentalEnablePhotoDeletion;
@@ -127,6 +134,8 @@ export default class SettingsService extends Service {
nostrPhotoFallbackUploads: this.nostrPhotoFallbackUploads, nostrPhotoFallbackUploads: this.nostrPhotoFallbackUploads,
nostrReadRelays: this.nostrReadRelays, nostrReadRelays: this.nostrReadRelays,
nostrWriteRelays: this.nostrWriteRelays, nostrWriteRelays: this.nostrWriteRelays,
nostrReadRelayExclusions: this.nostrReadRelayExclusions,
nostrWriteRelayExclusions: this.nostrWriteRelayExclusions,
experimentalEnablePhotoDeletion: this.experimentalEnablePhotoDeletion, experimentalEnablePhotoDeletion: this.experimentalEnablePhotoDeletion,
}; };
localStorage.setItem('marco:settings', JSON.stringify(settings)); localStorage.setItem('marco:settings', JSON.stringify(settings));
+14
View File
@@ -1266,6 +1266,20 @@ span.icon {
gap: 0.5rem; gap: 0.5rem;
} }
.content-with-icon > span:not(.icon) {
min-width: 0;
flex: 1;
}
.content-with-icon > span:not(.icon) a {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.content-with-icon .icon { .content-with-icon .icon {
margin-top: 0.15rem; margin-top: 0.15rem;
} }
+27
View File
@@ -1,4 +1,7 @@
// AGENT: Keep imports sorted alphabetically, grouped by feather-icons → pinhead → custom/local // AGENT: Keep imports sorted alphabetically, grouped by feather-icons → pinhead → custom/local
/*
* Feather icons
*/
import activity from 'feather-icons/dist/icons/activity.svg?raw'; import activity from 'feather-icons/dist/icons/activity.svg?raw';
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw'; import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw'; import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
@@ -40,6 +43,9 @@ import check from 'feather-icons/dist/icons/check.svg?raw';
import alertCircle from 'feather-icons/dist/icons/alert-circle.svg?raw'; import alertCircle from 'feather-icons/dist/icons/alert-circle.svg?raw';
import zap from 'feather-icons/dist/icons/zap.svg?raw'; import zap from 'feather-icons/dist/icons/zap.svg?raw';
/*
* Pinhead icons
*/
import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.svg?raw'; import angelfish from '@waysidemapping/pinhead/dist/icons/angelfish.svg?raw';
import barbell from '@waysidemapping/pinhead/dist/icons/barbell.svg?raw'; import barbell from '@waysidemapping/pinhead/dist/icons/barbell.svg?raw';
import climbingWall from '@waysidemapping/pinhead/dist/icons/climbing_wall.svg?raw'; import climbingWall from '@waysidemapping/pinhead/dist/icons/climbing_wall.svg?raw';
@@ -53,8 +59,12 @@ import bus from '@waysidemapping/pinhead/dist/icons/bus.svg?raw';
import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw'; import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw';
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw'; import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw'; import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw';
import carAndWrench from '@waysidemapping/pinhead/dist/icons/car_and_wrench.svg?raw';
import castleKeep from '@waysidemapping/pinhead/dist/icons/castle_keep.svg?raw';
import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw'; import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw';
import cityGate from '@waysidemapping/pinhead/dist/icons/city_gate.svg?raw';
import classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw'; import classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw';
import classicalBuildingWithClock from '@waysidemapping/pinhead/dist/icons/classical_building_with_clock.svg?raw';
import classicalBuildingWithDomeAndFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_dome_and_flag.svg?raw'; import classicalBuildingWithDomeAndFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_dome_and_flag.svg?raw';
import classicalBuildingWithFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_flag.svg?raw'; import classicalBuildingWithFlag from '@waysidemapping/pinhead/dist/icons/classical_building_with_flag.svg?raw';
import commercialBuilding from '@waysidemapping/pinhead/dist/icons/commercial_building.svg?raw'; import commercialBuilding from '@waysidemapping/pinhead/dist/icons/commercial_building.svg?raw';
@@ -82,8 +92,13 @@ import grecianVase from '@waysidemapping/pinhead/dist/icons/grecian_vase.svg?raw
import greekCross from '@waysidemapping/pinhead/dist/icons/greek_cross.svg?raw'; import greekCross from '@waysidemapping/pinhead/dist/icons/greek_cross.svg?raw';
import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw'; import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
import industrialBuilding from '@waysidemapping/pinhead/dist/icons/industrial_building.svg?raw'; import industrialBuilding from '@waysidemapping/pinhead/dist/icons/industrial_building.svg?raw';
import infoI from '@waysidemapping/pinhead/dist/icons/info_i.svg?raw';
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw'; import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw'; import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
import marketStall from '@waysidemapping/pinhead/dist/icons/market_stall.svg?raw';
import memorialStoneWithInscription from '@waysidemapping/pinhead/dist/icons/memorial_stone_with_inscription.svg?raw';
import mobilePhoneWithKeypadAndAntenna from '@waysidemapping/pinhead/dist/icons/mobile_phone_with_keypad_and_antenna.svg?raw';
import molarTooth from '@waysidemapping/pinhead/dist/icons/molar_tooth.svg?raw';
import needleAndSpoolOfThread from '@waysidemapping/pinhead/dist/icons/needle_and_spool_of_thread.svg?raw'; import needleAndSpoolOfThread from '@waysidemapping/pinhead/dist/icons/needle_and_spool_of_thread.svg?raw';
import openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw'; import openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw';
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw'; import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
@@ -118,6 +133,9 @@ import wallHangingWithMountainsAndSun from '@waysidemapping/pinhead/dist/icons/w
import windingWayWide from '@waysidemapping/pinhead/dist/icons/winding_way_wide.svg?raw'; import windingWayWide from '@waysidemapping/pinhead/dist/icons/winding_way_wide.svg?raw';
import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/womens_and_mens_restroom_symbol.svg?raw'; import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/womens_and_mens_restroom_symbol.svg?raw';
/*
* Custom/local icons
*/
import loadingRing from '../icons/270-ring.svg?raw'; import loadingRing from '../icons/270-ring.svg?raw';
import nostrich from '../icons/nostrich-2.svg?raw'; import nostrich from '../icons/nostrich-2.svg?raw';
import remotestorage from '../icons/remotestorage.svg?raw'; import remotestorage from '../icons/remotestorage.svg?raw';
@@ -144,11 +162,13 @@ const ICONS = {
'chevron-left': chevronLeft, 'chevron-left': chevronLeft,
'chevron-right': chevronRight, 'chevron-right': chevronRight,
'cigarette-with-smoke-curl': cigaretteWithSmokeCurl, 'cigarette-with-smoke-curl': cigaretteWithSmokeCurl,
'city-gate': cityGate,
climbing_wall: climbingWall, climbing_wall: climbingWall,
check, check,
'alert-circle': alertCircle, 'alert-circle': alertCircle,
'alert-triangle': alertTriangle, 'alert-triangle': alertTriangle,
'classical-building': classicalBuilding, 'classical-building': classicalBuilding,
'classical-building-with-clock': classicalBuildingWithClock,
'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag, 'classical-building-with-dome-and-flag': classicalBuildingWithDomeAndFlag,
'classical-building-with-flag': classicalBuildingWithFlag, 'classical-building-with-flag': classicalBuildingWithFlag,
'commercial-building': commercialBuilding, 'commercial-building': commercialBuilding,
@@ -185,6 +205,7 @@ const ICONS = {
'ice-cream-on-cone': iceCreamOnCone, 'ice-cream-on-cone': iceCreamOnCone,
'industrial-building': industrialBuilding, 'industrial-building': industrialBuilding,
info, info,
'info-i': infoI,
instagram, instagram,
jewel, jewel,
'log-in': logIn, 'log-in': logIn,
@@ -193,7 +214,11 @@ const ICONS = {
mail, mail,
map, map,
'map-pin': mapPin, 'map-pin': mapPin,
'market-stall': marketStall,
'memorial-stone-with-inscription': memorialStoneWithInscription,
menu, menu,
'mobile-phone-with-keypad-and-antenna': mobilePhoneWithKeypadAndAntenna,
'molar-tooth': molarTooth,
'more-horizontal': moreHorizontal, 'more-horizontal': moreHorizontal,
'more-vertical': moreVertical, 'more-vertical': moreVertical,
navigation, navigation,
@@ -245,6 +270,8 @@ const ICONS = {
winding_way_wide: windingWayWide, winding_way_wide: windingWayWide,
parking_p: parkingP, parking_p: parkingP,
car, car,
'car-and-wrench': carAndWrench,
'castle-keep': castleKeep,
x, x,
zap, zap,
'loading-ring': loadingRing, 'loading-ring': loadingRing,
+24
View File
@@ -14,6 +14,30 @@ export function normalizeRelayUrl(url) {
return normalized; 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. * 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. * Sorts chronologically and guarantees the first landscape photo (or first portrait) is at index 0.
+6
View File
@@ -27,6 +27,8 @@ export const POI_ICON_RULES = [
{ tags: { amenity: 'bank' }, icon: 'banknote' }, { tags: { amenity: 'bank' }, icon: 'banknote' },
{ tags: { amenity: 'place_of_worship' }, icon: 'place-of-worship-building' }, { tags: { amenity: 'place_of_worship' }, icon: 'place-of-worship-building' },
{ tags: { amenity: 'townhall' }, icon: 'classical-building-with-clock' },
{ tags: { building: 'townhall' }, icon: 'classical-building-with-clock' },
{ tags: { amenity: 'fire_station' }, icon: 'badge-shield-with-fire' }, { tags: { amenity: 'fire_station' }, icon: 'badge-shield-with-fire' },
{ tags: { amenity: 'police' }, icon: 'police-officer-with-stop-arm' }, { tags: { amenity: 'police' }, icon: 'police-officer-with-stop-arm' },
{ tags: { amenity: 'toilets' }, icon: 'womens-and-mens-restroom-symbol' }, { tags: { amenity: 'toilets' }, icon: 'womens-and-mens-restroom-symbol' },
@@ -72,6 +74,7 @@ export const POI_ICON_RULES = [
tags: { shop: 'beauty' }, tags: { shop: 'beauty' },
icon: 'fancy-mirror-with-reflection-and-stars', icon: 'fancy-mirror-with-reflection-and-stars',
}, },
{ tags: { shop: 'car_repair' }, icon: 'car-and-wrench' },
{ tags: { craft: 'tailor' }, icon: 'needle-and-spool-of-thread' }, { tags: { craft: 'tailor' }, icon: 'needle-and-spool-of-thread' },
{ tags: { office: 'estate_agent' }, icon: 'village-buildings' }, { tags: { office: 'estate_agent' }, icon: 'village-buildings' },
{ tags: { office: true }, icon: 'commercial-building' }, { tags: { office: true }, icon: 'commercial-building' },
@@ -103,6 +106,7 @@ export const POI_ICON_RULES = [
{ tags: { tourism: 'viewpoint' }, icon: 'camera' }, { tags: { tourism: 'viewpoint' }, icon: 'camera' },
{ tags: { tourism: 'zoo' }, icon: 'camera' }, { tags: { tourism: 'zoo' }, icon: 'camera' },
{ tags: { tourism: 'artwork' }, icon: 'camera' }, { tags: { tourism: 'artwork' }, icon: 'camera' },
{ tags: { tourism: 'information' }, icon: 'info-i' },
{ tags: { amenity: 'cinema' }, icon: 'film' }, { tags: { amenity: 'cinema' }, icon: 'film' },
{ tags: { amenity: 'theatre' }, icon: 'camera' }, { tags: { amenity: 'theatre' }, icon: 'camera' },
{ tags: { amenity: 'arts_centre' }, icon: 'comedy-mask-and-tragedy-mask' }, { tags: { amenity: 'arts_centre' }, icon: 'comedy-mask-and-tragedy-mask' },
@@ -113,7 +117,9 @@ export const POI_ICON_RULES = [
{ tags: { historic: 'bridge' }, icon: 'bridge' }, { tags: { historic: 'bridge' }, icon: 'bridge' },
{ tags: { historic: 'bridge_site' }, icon: 'bridge' }, { tags: { historic: 'bridge_site' }, icon: 'bridge' },
{ tags: { historic: 'fort' }, icon: 'fort' }, { tags: { historic: 'fort' }, icon: 'fort' },
{ tags: { historic: 'city_gate' }, icon: 'city-gate' },
{ tags: { historic: 'castle' }, icon: 'palace' }, { tags: { historic: 'castle' }, icon: 'palace' },
{ tags: { building: 'tower', historic: 'yes' }, icon: 'castle-keep' },
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' }, { tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
{ tags: { historic: 'archaeological_site' }, icon: 'grecian-vase' }, { tags: { historic: 'archaeological_site' }, icon: 'grecian-vase' },
{ tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' }, { tags: { historic: 'memorial' }, icon: 'memorial-stone-with-inscription' },
+10 -1
View File
@@ -56,15 +56,24 @@ const PLACE_TYPE_KEYS = [
export function getPlaceType(tags) { export function getPlaceType(tags) {
if (!tags) return null; if (!tags) return null;
let fallbackKey = null;
for (const key of PLACE_TYPE_KEYS) { for (const key of PLACE_TYPE_KEYS) {
const value = tags[key]; const value = tags[key];
if (value) { if (value) {
if (value === 'yes') { if (value === 'yes') {
return humanizeOsmTag(key); if (!fallbackKey) {
fallbackKey = key;
}
continue;
} }
return humanizeOsmTag(value); return humanizeOsmTag(value);
} }
} }
if (fallbackKey) {
return humanizeOsmTag(fallbackKey);
}
return null; return null;
} }
+2 -2
View File
@@ -43,7 +43,7 @@ export const POI_CATEGORIES = [
label: 'Things to do', label: 'Things to do',
icon: 'feather-camera', icon: 'feather-camera',
filter: [ filter: [
'["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork)$"]', '["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork|information)$"]',
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]', '["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
'["leisure"~"^(sports_centre|stadium|water_park)$"]', '["leisure"~"^(sports_centre|stadium|water_park)$"]',
'["historic"]', '["historic"]',
@@ -55,7 +55,7 @@ export const POI_CATEGORIES = [
id: 'accommodation', id: 'accommodation',
label: 'Hotels', label: 'Hotels',
icon: 'person-sleeping-in-bed', icon: 'person-sleeping-in-bed',
filter: ['["tourism"~"^(hotel|hostel|motel|chalet)$"]'], filter: ['["tourism"~"^(hotel|hostel|motel|chalet|guest_house)$"]'],
types: ['node', 'way', 'relation'], types: ['node', 'way', 'relation'],
}, },
]; ];
+49
View File
@@ -2,6 +2,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, waitFor, triggerEvent } from '@ember/test-helpers'; import { visit, currentURL, waitFor, triggerEvent } from '@ember/test-helpers';
import { setupApplicationTest } from 'marco/tests/helpers'; import { setupApplicationTest } from 'marco/tests/helpers';
import Service from '@ember/service'; import Service from '@ember/service';
import sinon from 'sinon';
module('Acceptance | map search reset', function (hooks) { module('Acceptance | map search reset', function (hooks) {
setupApplicationTest(hooks); setupApplicationTest(hooks);
@@ -16,10 +17,58 @@ module('Acceptance | map search reset', function (hooks) {
'marco:map-view', 'marco:map-view',
JSON.stringify(highZoomState) JSON.stringify(highZoomState)
); );
// Stub window.fetch using Sinon
// We want to intercept map style requests and let everything else through
this.fetchStub = sinon.stub(window, 'fetch');
this.fetchStub.callsFake(async (input, init) => {
let url = input;
if (typeof input === 'object' && input !== null && 'url' in input) {
url = input.url;
}
if (
typeof url === 'string' &&
url.includes('tiles.openfreemap.org/styles/liberty')
) {
return {
ok: true,
status: 200,
json: async () => ({
version: 8,
name: 'Liberty',
sources: {
openmaptiles: {
type: 'vector',
url: 'https://tiles.openfreemap.org/planet',
},
},
layers: [
{
id: 'background',
type: 'background',
paint: {
'background-color': '#123456',
},
},
],
glyphs:
'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
sprite: 'https://tiles.openfreemap.org/sprites/liberty',
}),
};
}
// Pass through to the original implementation
return this.fetchStub.wrappedMethod(input, init);
});
}); });
hooks.afterEach(function () { hooks.afterEach(function () {
window.localStorage.removeItem('marco:map-view'); window.localStorage.removeItem('marco:map-view');
// Restore the original fetch
this.fetchStub.restore();
}); });
test('clicking the map clears the category search parameter', async function (assert) { test('clicking the map clears the category search parameter', async function (assert) {
+10 -2
View File
@@ -49,11 +49,19 @@ export class MockNostrDataService extends Service {
return []; return [];
} }
get defaultReadRelays() { get requiredReadRelays() {
return []; return [];
} }
get defaultWriteRelays() { get requiredWriteRelays() {
return [];
}
get mailboxReadRelays() {
return [];
}
get mailboxWriteRelays() {
return []; return [];
} }
@@ -0,0 +1,188 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'marco/tests/helpers';
import { click, fillIn, render } from '@ember/test-helpers';
import Service, { service } from '@ember/service';
import AppMenuSettingsNostr from 'marco/components/app-menu/settings/nostr';
import {
excludeRequiredRelays,
mergeRequiredRelays,
uniqNormalizedRelays,
} from 'marco/utils/nostr';
class MockNostrDataService extends Service {
@service settings;
requiredReadRelays = ['wss://nostr.kosmos.org'];
requiredWriteRelays = [];
mailboxReadRelays = ['wss://mailbox.example.com'];
mailboxWriteRelays = ['wss://mailbox-write.example.com'];
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() {
return mergeRequiredRelays(
this.requiredReadRelays,
this.configuredReadRelays
);
}
get activeWriteRelays() {
return mergeRequiredRelays(
this.requiredWriteRelays,
this.configuredWriteRelays
);
}
async clearCache() {}
}
function readRows(element) {
const list = element.querySelectorAll('.relay-list')[0];
return [...list.querySelectorAll('li')];
}
function writeRows(element) {
const list = element.querySelectorAll('.relay-list')[1];
return [...list.querySelectorAll('li')];
}
function rowByText(rows, text) {
return rows.find((row) => row.textContent.includes(text));
}
module('Integration | Component | app-menu/settings/nostr', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
localStorage.removeItem('marco:settings');
this.owner.register('service:nostrData', MockNostrDataService);
this.settings = this.owner.lookup('service:settings');
this.onChange = () => {};
});
hooks.afterEach(function () {
localStorage.removeItem('marco:settings');
});
async function renderAndOpenDetails(context) {
await render(
<template><AppMenuSettingsNostr @onChange={{this.onChange}} /></template>
);
await click('summary');
return context.element;
}
test('required read relay is first and non-removable', async function (assert) {
const element = await renderAndOpenDetails(this);
const rows = readRows(element);
assert.dom(rows[0]).includesText('nostr.kosmos.org');
const requiredRow = rowByText(rows, 'nostr.kosmos.org');
const mailboxRow = rowByText(rows, 'mailbox.example.com');
assert.dom(requiredRow.querySelector('.btn-remove-relay')).doesNotExist();
assert.dom(mailboxRow.querySelector('.btn-remove-relay')).exists();
});
test('removing mailbox read relay stores exclusion override', async function (assert) {
const element = await renderAndOpenDetails(this);
const mailboxRow = rowByText(readRows(element), 'mailbox.example.com');
await click(mailboxRow.querySelector('.btn-remove-relay'));
assert.deepEqual(this.settings.nostrReadRelayExclusions, [
'wss://mailbox.example.com',
]);
assert.strictEqual(this.settings.nostrReadRelays, null);
});
test('removing custom read relay updates custom list without exclusions', async function (assert) {
this.settings.update('nostrReadRelays', ['wss://custom.example.com']);
const element = await renderAndOpenDetails(this);
const customRow = rowByText(readRows(element), 'custom.example.com');
await click(customRow.querySelector('.btn-remove-relay'));
assert.strictEqual(this.settings.nostrReadRelays, null);
assert.strictEqual(this.settings.nostrReadRelayExclusions, null);
});
test('adding read relay clears existing exclusion for same relay', async function (assert) {
this.settings.update('nostrReadRelayExclusions', [
'wss://mailbox.example.com',
]);
const element = await renderAndOpenDetails(this);
await fillIn('#new-read-relay', 'Mailbox.EXAMPLE.com/');
await click(element.querySelector('#new-read-relay').nextElementSibling);
assert.deepEqual(this.settings.nostrReadRelays, [
'wss://mailbox.example.com',
]);
assert.strictEqual(this.settings.nostrReadRelayExclusions, null);
});
test('reset read relays clears additions and exclusions', async function (assert) {
this.settings.update('nostrReadRelays', ['wss://custom.example.com']);
this.settings.update('nostrReadRelayExclusions', [
'wss://mailbox.example.com',
]);
const element = await renderAndOpenDetails(this);
await click(element.querySelectorAll('.reset-relays')[0]);
assert.strictEqual(this.settings.nostrReadRelays, null);
assert.strictEqual(this.settings.nostrReadRelayExclusions, null);
const requiredRow = rowByText(readRows(element), 'nostr.kosmos.org');
assert.dom(requiredRow).exists();
});
test('write relays are removable and mailbox delete stores exclusion', async function (assert) {
this.settings.update('nostrWriteRelays', [
'wss://custom-write.example.com',
]);
const element = await renderAndOpenDetails(this);
const rows = writeRows(element);
assert.true(
rows.every((row) => row.querySelector('.btn-remove-relay')),
'all write relays can be removed'
);
const mailboxRow = rowByText(rows, 'mailbox-write.example.com');
await click(mailboxRow.querySelector('.btn-remove-relay'));
assert.deepEqual(this.settings.nostrWriteRelayExclusions, [
'wss://mailbox-write.example.com',
]);
});
});
@@ -334,4 +334,54 @@ module('Integration | Component | place-details', function (hooks) {
assert.dom(links[0]).hasText('+44 987 654 321'); assert.dom(links[0]).hasText('+44 987 654 321');
assert.dom(links[1]).hasText('+1 234-567 8900'); assert.dom(links[1]).hasText('+1 234-567 8900');
}); });
test('it renders correct OpenStreetMap link for an OSM place', async function (assert) {
const place = {
title: 'OSM Place',
osmId: '12345',
osmType: 'node',
lat: 52.520008,
lon: 13.404954,
};
await render(<template><PlaceDetails @place={{place}} /></template>);
const osmLink = this.element.querySelector(
'.meta-info a[href^="https://www.openstreetmap.org/node/12345"]'
);
assert.ok(osmLink, 'OpenStreetMap link is rendered');
assert.strictEqual(
osmLink.getAttribute('href'),
'https://www.openstreetmap.org/node/12345'
);
assert.dom('button.btn-link').hasText('Add a photo');
});
test('it renders correct search-based OpenStreetMap link for a custom saved place', async function (assert) {
class MockMapUi extends Service {
currentCenter = { lat: 52.5, lon: 13.4 };
currentZoom = 15.6;
}
this.owner.register('service:map-ui', MockMapUi);
const place = {
title: 'Custom Place',
lat: 52.520008,
lon: 13.404954,
};
await render(<template><PlaceDetails @place={{place}} /></template>);
const osmLink = this.element.querySelector(
'.meta-info a[href^="https://www.openstreetmap.org/search"]'
);
assert.ok(osmLink, 'OpenStreetMap search link is rendered');
assert.strictEqual(
osmLink.getAttribute('href'),
'https://www.openstreetmap.org/search?lat=52.520008&lon=13.404954&zoom=16#map=16/52.50000/13.40000'
);
assert.dom('button.btn-link').doesNotExist();
});
}); });
+62 -1
View File
@@ -1,5 +1,11 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { normalizeRelayUrl, parsePlacePhotos } from 'marco/utils/nostr'; import {
excludeRequiredRelays,
mergeRequiredRelays,
normalizeRelayUrl,
parsePlacePhotos,
uniqNormalizedRelays,
} from 'marco/utils/nostr';
module('Unit | Utility | nostr', function () { module('Unit | Utility | nostr', function () {
test('normalizeRelayUrl normalizes protocol, case, and slashes', function (assert) { test('normalizeRelayUrl normalizes protocol, case, and slashes', function (assert) {
@@ -141,4 +147,59 @@ module('Unit | Utility | nostr', function () {
assert.strictEqual(photos[0].placeIdentifier, 'osm:node:123'); assert.strictEqual(photos[0].placeIdentifier, 'osm:node:123');
assert.strictEqual(photos[1].placeIdentifier, 'osm:node:456'); assert.strictEqual(photos[1].placeIdentifier, 'osm:node:456');
}); });
test('uniqNormalizedRelays returns normalized unique relays', function (assert) {
const relays = uniqNormalizedRelays([
'Relay.example.com',
'wss://relay.example.com/',
'wss://other.example.com',
]);
assert.deepEqual(relays, [
'wss://relay.example.com',
'wss://other.example.com',
]);
});
test('mergeRequiredRelays keeps required relays as-is and merges normalized custom relays', function (assert) {
const relays = mergeRequiredRelays(
['wss://required.example.com', 'required-2.example.com'],
['required-2.example.com/', 'wss://custom.example.com']
);
assert.deepEqual(relays, [
'wss://required.example.com',
'required-2.example.com',
'wss://required-2.example.com',
'wss://custom.example.com',
]);
});
test('excludeRequiredRelays removes required relays from normalized custom list', function (assert) {
const relays = excludeRequiredRelays(
[
'wss://required.example.com',
'custom.example.com',
'ws://custom2.example.com',
],
['wss://required.example.com']
);
assert.deepEqual(relays, [
'wss://custom.example.com',
'ws://custom2.example.com',
]);
});
test('excludeRequiredRelays trusts required list without normalizing it', function (assert) {
const relays = excludeRequiredRelays(
['Required.example.com', 'custom.example.com'],
['required.example.com']
);
assert.deepEqual(relays, [
'wss://required.example.com',
'wss://custom.example.com',
]);
});
}); });
+12 -1
View File
@@ -1,4 +1,5 @@
import { getIconNameForTags } from 'marco/utils/osm-icons'; import { getIconNameForTags, POI_ICON_RULES } from 'marco/utils/osm-icons';
import { getIcon } from 'marco/utils/icons';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
module('Unit | Utility | osm-icons', function () { module('Unit | Utility | osm-icons', function () {
@@ -36,4 +37,14 @@ module('Unit | Utility | osm-icons', function () {
let result = getIconNameForTags({ foo: 'bar' }); let result = getIconNameForTags({ foo: 'bar' });
assert.strictEqual(result, null); assert.strictEqual(result, null);
}); });
test('all icons used in POI_ICON_RULES exist in the icons utility', function (assert) {
for (let rule of POI_ICON_RULES) {
let icon = getIcon(rule.icon);
assert.ok(
icon,
`Icon "${rule.icon}" specified in POI_ICON_RULES should be imported and available in getIcon`
);
}
});
}); });
+6
View File
@@ -89,6 +89,12 @@ module('Unit | Utility | osm', function (hooks) {
assert.strictEqual(result, 'Building'); assert.strictEqual(result, 'Building');
}); });
test('getPlaceType ignores generic "yes" values if a more specific tag is present', function (assert) {
const tags = { historic: 'yes', building: 'tower' };
const result = getPlaceType(tags);
assert.strictEqual(result, 'Tower');
});
test('getPlaceType prioritizes order (amenity > shop > building)', function (assert) { test('getPlaceType prioritizes order (amenity > shop > building)', function (assert) {
// If something is both a shop and a building, it should be a shop // If something is both a shop and a building, it should be a shop
const tags = { building: 'yes', shop: 'supermarket' }; const tags = { building: 'yes', shop: 'supermarket' };