Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a457e1521b
|
||
|
|
39ab5e2cd1
|
||
|
|
ce53d42581
|
||
|
|
387fd6efed
|
||
|
|
aaf0d8c3e9
|
||
|
|
9807a2b822
|
||
|
|
ac9870240f
|
||
|
|
1b185d1033
|
||
|
|
4d4ed8c1ea
|
||
|
|
1613ebf0bf
|
||
|
|
4cb8c94c9c
|
||
|
|
1f87c802e1
|
||
|
|
a6576fcda8
|
||
|
|
1822c4893f
|
||
|
|
39dca446ac
|
||
|
|
2e6827f0d0
|
||
|
|
8c3a805684
|
@@ -237,7 +237,7 @@ export default class PhotoCarousel extends Component {
|
||||
data-src={{photo.url}}
|
||||
class="place-header-photo
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage photo.url}}
|
||||
/>
|
||||
{{else if this.isGalleryThumbnails}}
|
||||
@@ -245,7 +245,7 @@ export default class PhotoCarousel extends Component {
|
||||
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
|
||||
class="place-header-photo
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
|
||||
/>
|
||||
{{else}}
|
||||
@@ -260,7 +260,7 @@ export default class PhotoCarousel extends Component {
|
||||
<img
|
||||
data-src={{photo.url}}
|
||||
class="place-header-photo landscape"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage photo.url}}
|
||||
/>
|
||||
</picture>
|
||||
@@ -269,7 +269,7 @@ export default class PhotoCarousel extends Component {
|
||||
<img
|
||||
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
|
||||
class="place-header-photo portrait"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fn } from '@ember/helper';
|
||||
import { service } from '@ember/service';
|
||||
import { modifier } from 'ember-modifier';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { EventFactory } from 'applesauce-factory';
|
||||
import { EventFactory } from 'applesauce-core';
|
||||
import config from 'marco/config/environment';
|
||||
import DropdownMenu from './dropdown-menu';
|
||||
import PhotoCarousel from './photo-carousel';
|
||||
@@ -201,21 +201,17 @@ export default class PhotoGallery extends Component {
|
||||
const eventId = this.currentPhoto.eventId;
|
||||
|
||||
// Publish Nostr kind: 5 deletion event first so we don't end up with dead blossom links on a failure
|
||||
const factory = new EventFactory({ signer: this.nostrAuth.signer });
|
||||
const tags = [['e', eventId]];
|
||||
|
||||
if (this.currentPhoto.placeIdentifier) {
|
||||
tags.push(['i', this.currentPhoto.placeIdentifier]);
|
||||
}
|
||||
|
||||
const template = {
|
||||
kind: 5,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: 'Deleted photo',
|
||||
tags,
|
||||
};
|
||||
|
||||
const event = await factory.sign(template);
|
||||
const event = await EventFactory.fromKind(5)
|
||||
.content('Deleted photo')
|
||||
.modifyPublicTags(() => tags)
|
||||
.as(this.nostrAuth.signer)
|
||||
.sign();
|
||||
await this.nostrRelay.publish(this.nostrData.activeWriteRelays, event);
|
||||
|
||||
// Remove from local store by adding the kind 5 to it
|
||||
|
||||
@@ -27,6 +27,7 @@ export default class PlacePhotoUpload extends Component {
|
||||
@tracked isPublishing = false;
|
||||
@tracked isDragging = false;
|
||||
@tracked selectedTags = [];
|
||||
@tracked altText = '';
|
||||
|
||||
get place() {
|
||||
return this.args.place || {};
|
||||
@@ -103,6 +104,7 @@ export default class PlacePhotoUpload extends Component {
|
||||
this.file = null;
|
||||
this.uploadedPhoto = null;
|
||||
this.selectedTags = [];
|
||||
this.altText = '';
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(false);
|
||||
}
|
||||
@@ -118,6 +120,11 @@ export default class PlacePhotoUpload extends Component {
|
||||
this.selectedTags = [tag];
|
||||
}
|
||||
|
||||
@action
|
||||
updateAltText(event) {
|
||||
this.altText = event.target.value;
|
||||
}
|
||||
|
||||
deletePhotoTask = task(async (photoData) => {
|
||||
try {
|
||||
if (photoData.hash) {
|
||||
@@ -155,8 +162,6 @@ export default class PlacePhotoUpload extends Component {
|
||||
this.isPublishing = true;
|
||||
|
||||
try {
|
||||
const factory = new EventFactory({ signer: this.nostrAuth.signer });
|
||||
|
||||
const tags = [['i', `osm:${osmType}:${osmId}`]];
|
||||
|
||||
for (const tag of this.selectedTags) {
|
||||
@@ -179,7 +184,10 @@ export default class PlacePhotoUpload extends Component {
|
||||
imeta.push(`dim ${photo.dim}`);
|
||||
}
|
||||
|
||||
imeta.push('alt A photo of a place');
|
||||
const alt = this.altText.trim();
|
||||
if (alt) {
|
||||
imeta.push(`alt ${alt}`);
|
||||
}
|
||||
|
||||
if (photo.fallbackUrls && photo.fallbackUrls.length > 0) {
|
||||
for (const fallbackUrl of photo.fallbackUrls) {
|
||||
@@ -198,17 +206,11 @@ export default class PlacePhotoUpload extends Component {
|
||||
tags.push(imeta);
|
||||
|
||||
// NIP-XX draft Place Photo event
|
||||
const template = {
|
||||
kind: 360,
|
||||
content: '',
|
||||
tags,
|
||||
};
|
||||
|
||||
if (!template.created_at) {
|
||||
template.created_at = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const event = await factory.sign(template);
|
||||
const event = await EventFactory.fromKind(360)
|
||||
.content('')
|
||||
.modifyPublicTags(() => tags)
|
||||
.as(this.nostrAuth.signer)
|
||||
.sign();
|
||||
await this.nostrRelay.publish(this.nostrData.activeWriteRelays, event);
|
||||
this.nostrData.store.add(event);
|
||||
|
||||
@@ -217,6 +219,7 @@ export default class PlacePhotoUpload extends Component {
|
||||
// Clear out the file so user can upload more or be done
|
||||
this.file = null;
|
||||
this.uploadedPhoto = null;
|
||||
this.altText = '';
|
||||
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(false);
|
||||
@@ -243,13 +246,11 @@ export default class PlacePhotoUpload extends Component {
|
||||
{{/if}}
|
||||
|
||||
{{#if this.file}}
|
||||
<div class="photo-grid">
|
||||
<PlacePhotoUploadItem
|
||||
@file={{this.file}}
|
||||
@onSuccess={{this.handleUploadSuccess}}
|
||||
@onRemove={{this.removeFile}}
|
||||
/>
|
||||
</div>
|
||||
<PlacePhotoUploadItem
|
||||
@file={{this.file}}
|
||||
@onSuccess={{this.handleUploadSuccess}}
|
||||
@onRemove={{this.removeFile}}
|
||||
/>
|
||||
|
||||
{{#if this.suggestedTags.length}}
|
||||
<div class="photo-tag-suggestions">
|
||||
@@ -271,6 +272,19 @@ export default class PlacePhotoUpload extends Component {
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="photo-alt-input">Description (optional):</label>
|
||||
<input
|
||||
id="photo-alt-input"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Describe this photo"
|
||||
value={{this.altText}}
|
||||
disabled={{this.isPublishing}}
|
||||
{{on "input" this.updateAltText}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-publish"
|
||||
|
||||
@@ -4,6 +4,16 @@ import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
function getPlaceTime(place) {
|
||||
const dateVal = place.createdAt;
|
||||
if (!dateVal) return 0;
|
||||
if (typeof dateVal === 'number') {
|
||||
return dateVal;
|
||||
}
|
||||
const parsed = Date.parse(dateVal);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
export default class ListsListController extends Controller {
|
||||
@service router;
|
||||
@service mapUi;
|
||||
@@ -73,7 +83,7 @@ export default class ListsListController extends Controller {
|
||||
}
|
||||
});
|
||||
|
||||
return merged;
|
||||
return merged.sort((a, b) => getPlaceTime(b) - getPlaceTime(a));
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -179,10 +179,8 @@ export default class SearchController extends Controller {
|
||||
const targetName = params.selected || params.q;
|
||||
|
||||
if (targetName && pois.length > 0) {
|
||||
let matchedPlace = null;
|
||||
|
||||
// 1. Exact Name Match
|
||||
matchedPlace = pois.find(
|
||||
let matchedPlace = pois.find(
|
||||
(p) =>
|
||||
p.osmTags &&
|
||||
(p.osmTags.name === targetName || p.osmTags['name:en'] === targetName)
|
||||
|
||||
+7
-10
@@ -35,23 +35,20 @@ export default class BlossomService extends Service {
|
||||
}
|
||||
|
||||
async _getAuthHeader(action, hash, serverUrl) {
|
||||
const factory = new EventFactory({ signer: this.nostrAuth.signer });
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const serverHostname = new URL(serverUrl).hostname;
|
||||
|
||||
const authTemplate = {
|
||||
kind: 24242,
|
||||
created_at: now,
|
||||
content: action === 'upload' ? 'Upload photo for place' : 'Delete photo',
|
||||
tags: [
|
||||
const authEvent = await EventFactory.fromKind(24242)
|
||||
.content(action === 'upload' ? 'Upload photo for place' : 'Delete photo')
|
||||
.modifyPublicTags(() => [
|
||||
['t', action],
|
||||
['x', hash],
|
||||
['expiration', String(now + 3600)],
|
||||
['server', serverHostname],
|
||||
],
|
||||
};
|
||||
|
||||
const authEvent = await factory.sign(authTemplate);
|
||||
])
|
||||
.created(now)
|
||||
.as(this.nostrAuth.signer)
|
||||
.sign();
|
||||
const base64 = btoa(JSON.stringify(authEvent));
|
||||
const base64url = base64
|
||||
.replace(/\+/g, '-')
|
||||
|
||||
@@ -114,7 +114,7 @@ export default class ImageProcessorService extends Service {
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to process image: ${e.message}`);
|
||||
throw new Error(`Failed to process image: ${e.message}`, { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const STORAGE_KEY_CONNECT_LOCAL_KEY = 'marco:nostr_connect_local_key';
|
||||
const STORAGE_KEY_CONNECT_REMOTE_PUBKEY = 'marco:nostr_connect_remote_pubkey';
|
||||
const STORAGE_KEY_CONNECT_RELAY = 'marco:nostr_connect_relay';
|
||||
|
||||
const DEFAULT_CONNECT_RELAY = 'wss://relay.nsec.app';
|
||||
const DEFAULT_CONNECT_RELAY = 'wss://nostr.kosmos.org';
|
||||
|
||||
import { isMobile } from '../utils/device';
|
||||
|
||||
@@ -150,9 +150,6 @@ export default class NostrAuthService extends Service {
|
||||
const relay = DEFAULT_CONNECT_RELAY;
|
||||
localStorage.setItem(STORAGE_KEY_CONNECT_RELAY, relay);
|
||||
|
||||
// Override aggressive 10s EOSE timeout to allow time for QR scanning
|
||||
this.nostrRelay.pool.relay(relay).eoseTimeout = 180000; // 3 minutes
|
||||
|
||||
this._signerInstance = new NostrConnectSigner({
|
||||
pool: this.nostrRelay.pool,
|
||||
relays: [relay],
|
||||
@@ -235,9 +232,6 @@ export default class NostrAuthService extends Service {
|
||||
|
||||
const localSigner = this._getLocalSigner();
|
||||
|
||||
// Override aggressive 10s EOSE timeout to allow time for QR scanning
|
||||
this.nostrRelay.pool.relay(relay).eoseTimeout = 180000; // 3 minutes
|
||||
|
||||
this._signerInstance = new NostrConnectSigner({
|
||||
pool: this.nostrRelay.pool,
|
||||
relays: [relay],
|
||||
|
||||
@@ -45,6 +45,9 @@ export default class OsmAuthService extends Service {
|
||||
clientId: clientId,
|
||||
redirectUrl: redirectUrl,
|
||||
storeRefreshToken: true,
|
||||
onAccessTokenExpiry() {
|
||||
return this.exchangeRefreshTokenForAccessToken();
|
||||
},
|
||||
},
|
||||
new MarcoOsmAuthStorage()
|
||||
);
|
||||
|
||||
+6
-9
@@ -245,13 +245,6 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.photo-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.photo-upload-item {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
@@ -259,6 +252,7 @@ body {
|
||||
overflow: hidden;
|
||||
background: #1e262e;
|
||||
width: 100%;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.photo-upload-item img {
|
||||
@@ -1530,7 +1524,6 @@ button.create-place {
|
||||
color: #5f6368;
|
||||
border-radius: 50%;
|
||||
margin-left: 4px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.search-submit-btn:hover {
|
||||
@@ -1867,8 +1860,12 @@ button.create-place {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.place-photo-upload .form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.photo-tag-suggestions {
|
||||
margin: 1rem 0 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.photo-tag-suggestions-title {
|
||||
|
||||
+2
-2
@@ -102,7 +102,7 @@ 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 openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw';
|
||||
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
|
||||
import parkingP from '@waysidemapping/pinhead/dist/icons/parking_p.svg?raw';
|
||||
import parkingP from '@waysidemapping/pinhead/dist/icons/p_wide.svg?raw';
|
||||
import personCricketBattingAtCricketBall from '@waysidemapping/pinhead/dist/icons/person_cricket_batting_at_cricket_ball.svg?raw';
|
||||
import personBoardingTramWithDestinationDisplayAndPantographOnTramTrack from '@waysidemapping/pinhead/dist/icons/person_boarding_tram_with_destination_display_and_pantograph_on_tram_track.svg?raw';
|
||||
import personJockeyingRacehorse from '@waysidemapping/pinhead/dist/icons/person_jockeying_racehorse.svg?raw';
|
||||
@@ -118,7 +118,7 @@ import policeOfficerWithStopArm from '@waysidemapping/pinhead/dist/icons/police_
|
||||
import planeTopRight from '@waysidemapping/pinhead/dist/icons/plane_top_right.svg?raw';
|
||||
import roundStructureWithFlag from '@waysidemapping/pinhead/dist/icons/round_structure_with_flag.svg?raw';
|
||||
import sailingShipInWater from '@waysidemapping/pinhead/dist/icons/sailing_ship_in_water.svg?raw';
|
||||
import scissorsOpen from '@waysidemapping/pinhead/dist/icons/scissors_open.svg?raw';
|
||||
import scissorsOpen from '@waysidemapping/pinhead/dist/icons/open_scissors.svg?raw';
|
||||
import shipwreckInWater from '@waysidemapping/pinhead/dist/icons/shipwreck_in_water.svg?raw';
|
||||
import steamTrainOnRailwayTrack from '@waysidemapping/pinhead/dist/icons/steam_train_on_railway_track.svg?raw';
|
||||
import shoppingBag from '@waysidemapping/pinhead/dist/icons/shopping_bag.svg?raw';
|
||||
|
||||
@@ -67,6 +67,7 @@ export function parsePlacePhotos(events) {
|
||||
let blurhash = null;
|
||||
let isLandscape = false;
|
||||
let aspectRatio = 16 / 9; // default
|
||||
let altText = null;
|
||||
let placeIdentifier = event.tags.find((t) => t[0] === 'i')?.[1];
|
||||
|
||||
for (const tag of imeta.slice(1)) {
|
||||
@@ -85,6 +86,10 @@ export function parsePlacePhotos(events) {
|
||||
isLandscape = true;
|
||||
}
|
||||
}
|
||||
} else if (tag.startsWith('alt ')) {
|
||||
const alt = tag.substring(4).trim();
|
||||
// Strip the legacy placeholder we used to write on every event
|
||||
altText = alt === 'A photo of a place' ? null : alt || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +105,7 @@ export function parsePlacePhotos(events) {
|
||||
aspectRatio,
|
||||
placeIdentifier,
|
||||
tags: eventTagValues,
|
||||
alt: altText,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ export default {
|
||||
'@babel/plugin-transform-runtime',
|
||||
{
|
||||
absoluteRuntime: dirname(fileURLToPath(import.meta.url)),
|
||||
useESModules: true,
|
||||
regenerator: false,
|
||||
},
|
||||
],
|
||||
...macros.babelMacros,
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import qunit from 'eslint-plugin-qunit';
|
||||
import n from 'eslint-plugin-n';
|
||||
|
||||
import babelParser from '@babel/eslint-parser/experimental-worker';
|
||||
import babelParser from '@babel/eslint-parser';
|
||||
|
||||
const esmParserOptions = {
|
||||
ecmaFeatures: { modules: true },
|
||||
|
||||
+52
-53
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.25.0",
|
||||
"version": "1.26.1",
|
||||
"private": true,
|
||||
"description": "Unhosted maps app",
|
||||
"repository": {
|
||||
@@ -39,61 +39,61 @@
|
||||
"version": "pnpm build && git add release/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.5",
|
||||
"@babel/eslint-parser": "^7.28.5",
|
||||
"@babel/plugin-transform-runtime": "^7.28.5",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@ember/test-helpers": "^5.4.1",
|
||||
"@ember/test-waiters": "^4.1.1",
|
||||
"@embroider/core": "^4.4.2",
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/eslint-parser": "^8.0.1",
|
||||
"@babel/plugin-transform-runtime": "^8.0.1",
|
||||
"@babel/runtime": "^8.0.0",
|
||||
"@ember/test-helpers": "^5.4.3",
|
||||
"@ember/test-waiters": "^4.1.2",
|
||||
"@embroider/core": "^4.6.3",
|
||||
"@embroider/legacy-inspector-support": "^0.1.3",
|
||||
"@embroider/macros": "^1.19.6",
|
||||
"@embroider/macros": "^1.20.6",
|
||||
"@embroider/router": "^3.0.6",
|
||||
"@embroider/vite": "^1.5.0",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@glimmer/component": "^2.0.0",
|
||||
"@embroider/vite": "^1.7.9",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@glimmer/component": "^2.1.1",
|
||||
"@remotestorage/module-places": "~1.3.0",
|
||||
"@rollup/plugin-babel": "^6.1.0",
|
||||
"@warp-drive/core": "~5.8.0",
|
||||
"@warp-drive/ember": "~5.8.0",
|
||||
"@warp-drive/json-api": "~5.8.0",
|
||||
"@warp-drive/legacy": "~5.8.0",
|
||||
"@warp-drive/utilities": "~5.8.0",
|
||||
"babel-plugin-ember-template-compilation": "^3.0.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"decorator-transforms": "^2.3.1",
|
||||
"ember-cli": "^6.10.0",
|
||||
"ember-cli-deprecation-workflow": "^4.0.0",
|
||||
"ember-modifier": "^4.2.2",
|
||||
"@rollup/plugin-babel": "^7.1.0",
|
||||
"@warp-drive/core": "~5.8.2",
|
||||
"@warp-drive/ember": "~5.8.2",
|
||||
"@warp-drive/json-api": "~5.8.2",
|
||||
"@warp-drive/legacy": "~5.8.2",
|
||||
"@warp-drive/utilities": "~5.8.2",
|
||||
"babel-plugin-ember-template-compilation": "^4.0.0",
|
||||
"concurrently": "^10.0.4",
|
||||
"decorator-transforms": "^2.4.0",
|
||||
"ember-cli": "^7.1.0",
|
||||
"ember-cli-deprecation-workflow": "^4.0.1",
|
||||
"ember-modifier": "^4.3.0",
|
||||
"ember-page-title": "^9.0.3",
|
||||
"ember-qunit": "^9.0.4",
|
||||
"ember-resolver": "^13.1.1",
|
||||
"ember-source": "~6.11.0-alpha.6",
|
||||
"ember-qunit": "^9.1.0",
|
||||
"ember-resolver": "^13.2.0",
|
||||
"ember-source": "~7.1.0",
|
||||
"ember-template-lint": "^7.9.3",
|
||||
"ember-truth-helpers": "^5.0.0",
|
||||
"ember-welcome-page": "^8.0.4",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-ember": "^12.7.5",
|
||||
"eslint-plugin-n": "^17.23.1",
|
||||
"eslint-plugin-qunit": "^8.2.5",
|
||||
"eslint-plugin-warp-drive": "^5.8.0",
|
||||
"eslint-plugin-ember": "^13.5.0",
|
||||
"eslint-plugin-n": "^18.2.2",
|
||||
"eslint-plugin-qunit": "^8.2.6",
|
||||
"eslint-plugin-warp-drive": "^5.8.2",
|
||||
"feather-icons": "^4.29.2",
|
||||
"globals": "^16.5.0",
|
||||
"globals": "^17.9.0",
|
||||
"latlon-geohash": "^2.0.0",
|
||||
"ol": "^10.7.0",
|
||||
"ol-mapbox-style": "^13.2.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-ember-template-tag": "^2.1.2",
|
||||
"qunit": "^2.25.0",
|
||||
"qunit-dom": "^3.5.0",
|
||||
"ol": "^10.10.0",
|
||||
"ol-mapbox-style": "^13.4.2",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier-plugin-ember-template-tag": "^2.1.7",
|
||||
"qunit": "^2.26.0",
|
||||
"qunit-dom": "^3.6.0",
|
||||
"remotestorage-widget": "^1.8.1",
|
||||
"remotestoragejs": "2.0.0-beta.9",
|
||||
"sinon": "^21.0.1",
|
||||
"stylelint": "^16.26.1",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"testem": "^3.17.0",
|
||||
"vite": "^7.3.0"
|
||||
"sinon": "^22.1.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"stylelint-config-standard": "^40.0.0",
|
||||
"testem": "^3.20.1",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
@@ -102,17 +102,16 @@
|
||||
"edition": "octane"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"@waysidemapping/pinhead": "^15.20.0",
|
||||
"applesauce-core": "^5.2.0",
|
||||
"applesauce-factory": "^4.0.0",
|
||||
"applesauce-relay": "^5.2.0",
|
||||
"applesauce-signers": "^5.2.0",
|
||||
"@noble/hashes": "^2.3.0",
|
||||
"@waysidemapping/pinhead": "^15.25.0",
|
||||
"applesauce-core": "^6.2.0",
|
||||
"applesauce-relay": "^6.2.1",
|
||||
"applesauce-signers": "^6.2.2",
|
||||
"blurhash": "^2.0.5",
|
||||
"ember-concurrency": "^5.2.0",
|
||||
"ember-lifeline": "^7.0.0",
|
||||
"nostr-idb": "^5.0.0",
|
||||
"oauth2-pkce": "^2.1.3",
|
||||
"ember-lifeline": "^7.1.0",
|
||||
"nostr-idb": "^5.1.0",
|
||||
"oauth2-pkce": "^3.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"rxjs": "^7.8.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/async-arrow-task-transform.js b/async-arrow-task-transform.js
|
||||
index 4eb856a2a7753ef583e10e07f2b602c0c1c0c21d..6a808d035834567dcfc491ecaa7f59e77d344887 100644
|
||||
--- a/async-arrow-task-transform.js
|
||||
+++ b/async-arrow-task-transform.js
|
||||
@@ -331,7 +331,7 @@ function cleanupUnusedTaskFactoryImports(programPath, usedTaskFactories) {
|
||||
}
|
||||
|
||||
module.exports = declare((api) => {
|
||||
- api.assertVersion(7);
|
||||
+ // api.assertVersion(7);
|
||||
|
||||
return {
|
||||
name: 'transform-ember-concurrency-async-function-tasks',
|
||||
Generated
+2507
-2555
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
patchedDependencies:
|
||||
ember-concurrency@5.2.0: patches/ember-concurrency@5.2.0.patch
|
||||
@@ -1 +0,0 @@
|
||||
!function(){"use strict";var t=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","#","$","%","*","+",",","-",".",":",";","=","?","@","[","]","^","_","{","|","}","~"],e=(e,a)=>{var o="";for(let r=1;r<=a;r++){let h=Math.floor(e)/Math.pow(83,a-r)%83;o+=t[Math.floor(h)]}return o},a=t=>{let e=t/255;return e<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4)},o=t=>{let e=Math.max(0,Math.min(1,t));return e<=.0031308?Math.trunc(12.92*e*255+.5):Math.trunc(255*(1.055*Math.pow(e,.4166666666666667)-.055)+.5)},r=(t,e)=>(t=>t<0?-1:1)(t)*Math.pow(Math.abs(t),e),h=class extends Error{constructor(t){super(t),this.name="ValidationError",this.message=t}},i=(t,e,o,r)=>{let h=0,i=0,n=0,s=4*e;for(let g=0;g<e;g++){let e=4*g;for(let l=0;l<o;l++){let o=e+l*s,c=r(g,l);h+=c*a(t[o]),i+=c*a(t[o+1]),n+=c*a(t[o+2])}}let l=1/(e*o);return[h*l,i*l,n*l]};self.onmessage=async t=>{if("PROCESS_IMAGE"!==t.data?.type)return;const{id:a,file:n,targetWidth:s,targetHeight:l,quality:g,computeBlurhash:c}=t.data;try{let t,M;try{const e=await createImageBitmap(n,{resizeWidth:s,resizeHeight:l,resizeQuality:"high"});if(t=new OffscreenCanvas(s,l),M=t.getContext("2d"),!M)throw new Error("Failed to get 2d context from OffscreenCanvas");M.drawImage(e,0,0,s,l),e.close()}catch(f){console.warn("Hardware resize failed, falling back to stepped software scaling:",f);const e=await n.arrayBuffer(),a=new Blob([e],{type:n.type}),o=await createImageBitmap(a);let r=o.width,h=o.height,i=new OffscreenCanvas(r,h),g=i.getContext("2d");for(g.imageSmoothingEnabled=!0,g.imageSmoothingQuality="high",g.drawImage(o,0,0);.5*i.width>s&&.5*i.height>l;){const t=new OffscreenCanvas(Math.floor(.5*i.width),Math.floor(.5*i.height)),e=t.getContext("2d");e.imageSmoothingEnabled=!0,e.imageSmoothingQuality="high",e.drawImage(i,0,0,t.width,t.height),i=t}t=new OffscreenCanvas(s,l),M=t.getContext("2d"),M.imageSmoothingEnabled=!0,M.imageSmoothingQuality="high",M.drawImage(i,0,0,s,l),o.close()}let d=null;if(c)try{d=((t,a,n)=>{if(a*n*4!==t.length)throw new h("Width and height must match the pixels array");let s=[];for(let e=0;e<3;e++)for(let o=0;o<4;o++){let r=0==o&&0==e?1:2,h=i(t,a,n,(t,h)=>r*Math.cos(Math.PI*o*t/a)*Math.cos(Math.PI*e*h/n));s.push(h)}let l,g=s[0],c=s.slice(1),f="";if(f+=e(21,1),c.length>0){let t=Math.max(...c.map(t=>Math.max(...t))),a=Math.floor(Math.max(0,Math.min(82,Math.floor(166*t-.5))));l=(a+1)/166,f+=e(a,1)}else l=1,f+=e(0,1);return f+=e((t=>(o(t[0])<<16)+(o(t[1])<<8)+o(t[2]))(g),4),c.forEach(t=>{f+=e(((t,e)=>19*Math.floor(Math.max(0,Math.min(18,Math.floor(9*r(t[0]/e,.5)+9.5))))*19+19*Math.floor(Math.max(0,Math.min(18,Math.floor(9*r(t[1]/e,.5)+9.5))))+Math.floor(Math.max(0,Math.min(18,Math.floor(9*r(t[2]/e,.5)+9.5)))))(t,l),2)}),f})(M.getImageData(0,0,s,l).data,s,l)}catch(m){console.warn("Could not generate blurhash (possible canvas fingerprinting protection):",m)}const u=await t.convertToBlob({type:"image/jpeg",quality:g}),w=`${s}x${l}`;self.postMessage({id:a,success:!0,blob:u,dim:w,blurhash:d})}catch(M){self.postMessage({id:a,success:!1,error:M.message})}}}();
|
||||
@@ -0,0 +1,182 @@
|
||||
var q = [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"#",
|
||||
"$",
|
||||
"%",
|
||||
"*",
|
||||
"+",
|
||||
",",
|
||||
"-",
|
||||
".",
|
||||
":",
|
||||
";",
|
||||
"=",
|
||||
"?",
|
||||
"@",
|
||||
"[",
|
||||
"]",
|
||||
"^",
|
||||
"_",
|
||||
"{",
|
||||
"|",
|
||||
"}",
|
||||
"~"
|
||||
], p = (t, e) => {
|
||||
var a = "";
|
||||
for (let o = 1; o <= e; o++) {
|
||||
let h = Math.floor(t) / Math.pow(83, e - o) % 83;
|
||||
a += q[Math.floor(h)];
|
||||
}
|
||||
return a;
|
||||
}, f = (t) => {
|
||||
let e = t / 255;
|
||||
return e <= .04045 ? e / 12.92 : Math.pow((e + .055) / 1.055, 2.4);
|
||||
}, h = (t) => {
|
||||
let e = Math.max(0, Math.min(1, t));
|
||||
return e <= .0031308 ? Math.trunc(12.92 * e * 255 + .5) : Math.trunc(255 * (1.055 * Math.pow(e, .4166666666666667) - .055) + .5);
|
||||
}, M = (t, e) => ((t) => t < 0 ? -1 : 1)(t) * Math.pow(Math.abs(t), e), d = class extends Error {
|
||||
constructor(t) {
|
||||
super(t), this.name = "ValidationError", this.message = t;
|
||||
}
|
||||
}, D = (t, e, a, o) => {
|
||||
let h = 0, r = 0, n = 0, i = 4 * e;
|
||||
for (let l = 0; l < e; l++) {
|
||||
let e = 4 * l;
|
||||
for (let s = 0; s < a; s++) {
|
||||
let a = e + s * i, g = o(l, s);
|
||||
h += g * f(t[a]), r += g * f(t[a + 1]), n += g * f(t[a + 2]);
|
||||
}
|
||||
}
|
||||
let s = 1 / (e * a);
|
||||
return [
|
||||
h * s,
|
||||
r * s,
|
||||
n * s
|
||||
];
|
||||
}, S = (t, e, a, o, r) => {
|
||||
if (o < 1 || o > 9 || r < 1 || r > 9) throw new d("BlurHash must have between 1 and 9 components");
|
||||
if (e * a * 4 !== t.length) throw new d("Width and height must match the pixels array");
|
||||
let n = [];
|
||||
for (let h = 0; h < r; h++) for (let r = 0; r < o; r++) {
|
||||
let o = 0 == r && 0 == h ? 1 : 2, i = D(t, e, a, (t, n) => o * Math.cos(Math.PI * r * t / e) * Math.cos(Math.PI * h * n / a));
|
||||
n.push(i);
|
||||
}
|
||||
let i, s = n[0], l = n.slice(1), g = "";
|
||||
if (g += p(o - 1 + 9 * (r - 1), 1), l.length > 0) {
|
||||
let t = Math.max(...l.map((t) => Math.max(...t))), e = Math.floor(Math.max(0, Math.min(82, Math.floor(166 * t - .5))));
|
||||
i = (e + 1) / 166, g += p(e, 1);
|
||||
} else i = 1, g += p(0, 1);
|
||||
return g += p(((t) => (h(t[0]) << 16) + (h(t[1]) << 8) + h(t[2]))(s), 4), l.forEach((t) => {
|
||||
g += p(((t, e) => 19 * Math.floor(Math.max(0, Math.min(18, Math.floor(9 * M(t[0] / e, .5) + 9.5)))) * 19 + 19 * Math.floor(Math.max(0, Math.min(18, Math.floor(9 * M(t[1] / e, .5) + 9.5)))) + Math.floor(Math.max(0, Math.min(18, Math.floor(9 * M(t[2] / e, .5) + 9.5)))))(t, i), 2);
|
||||
}), g;
|
||||
};
|
||||
self.onmessage = async (t) => {
|
||||
if ("PROCESS_IMAGE" !== t.data?.type) return;
|
||||
const { id: e, file: a, targetWidth: o, targetHeight: h, quality: r, computeBlurhash: n } = t.data;
|
||||
try {
|
||||
let t, l;
|
||||
try {
|
||||
const e = await createImageBitmap(a, {
|
||||
resizeWidth: o,
|
||||
resizeHeight: h,
|
||||
resizeQuality: "high"
|
||||
});
|
||||
if (t = new OffscreenCanvas(o, h), l = t.getContext("2d"), !l) throw new Error("Failed to get 2d context from OffscreenCanvas");
|
||||
l.drawImage(e, 0, 0, o, h), e.close();
|
||||
} catch (i) {
|
||||
console.warn("Hardware resize failed, falling back to stepped software scaling:", i);
|
||||
const e = await a.arrayBuffer(), r = new Blob([e], { type: a.type }), n = await createImageBitmap(r);
|
||||
let s = n.width, g = n.height, f = new OffscreenCanvas(s, g), c = f.getContext("2d");
|
||||
for (c.imageSmoothingEnabled = !0, c.imageSmoothingQuality = "high", c.drawImage(n, 0, 0); .5 * f.width > o && .5 * f.height > h;) {
|
||||
const t = new OffscreenCanvas(Math.floor(.5 * f.width), Math.floor(.5 * f.height)), e = t.getContext("2d");
|
||||
e.imageSmoothingEnabled = !0, e.imageSmoothingQuality = "high", e.drawImage(f, 0, 0, t.width, t.height), f = t;
|
||||
}
|
||||
t = new OffscreenCanvas(o, h), l = t.getContext("2d"), l.imageSmoothingEnabled = !0, l.imageSmoothingQuality = "high", l.drawImage(f, 0, 0, o, h), n.close();
|
||||
}
|
||||
let g = null;
|
||||
if (n) try {
|
||||
const t = l.getImageData(0, 0, o, h);
|
||||
g = S(t.data, o, h, 4, 3);
|
||||
} catch (s) {
|
||||
console.warn("Could not generate blurhash (possible canvas fingerprinting protection):", s);
|
||||
}
|
||||
const f = await t.convertToBlob({
|
||||
type: "image/jpeg",
|
||||
quality: r
|
||||
}), c = `${o}x${h}`;
|
||||
self.postMessage({
|
||||
id: e,
|
||||
success: !0,
|
||||
blob: f,
|
||||
dim: c,
|
||||
blurhash: g
|
||||
});
|
||||
} catch (l) {
|
||||
self.postMessage({
|
||||
id: e,
|
||||
success: !1,
|
||||
error: l.message
|
||||
});
|
||||
}
|
||||
};
|
||||
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -39,8 +39,8 @@
|
||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-BVNM87jL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BGF-Udec.css">
|
||||
<script type="module" crossorigin src="/assets/main-BsW6Y6yU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CIu0qcV7.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="modal-portal"></div>
|
||||
|
||||
@@ -135,4 +135,85 @@ module('Acceptance | collections navigation', function (hooks) {
|
||||
'Returns gracefully back to lists/to-go list view'
|
||||
);
|
||||
});
|
||||
|
||||
test('places inside a collection are sorted by createdAt descending', async function (assert) {
|
||||
class SortedMockStorageService extends Service {
|
||||
initialSyncDone = true;
|
||||
savedPlaces = [
|
||||
{
|
||||
id: 'place-oldest',
|
||||
title: 'Oldest Place',
|
||||
geohash: 'u33dc0',
|
||||
createdAt: '2023-01-01T12:00:00.000Z',
|
||||
osmTags: { name: 'Oldest Place' },
|
||||
},
|
||||
{
|
||||
id: 'place-newest',
|
||||
title: 'Newest Place',
|
||||
geohash: 'u33dc0',
|
||||
createdAt: '2023-01-03T12:00:00.000Z',
|
||||
osmTags: { name: 'Newest Place' },
|
||||
},
|
||||
{
|
||||
id: 'place-middle',
|
||||
title: 'Middle Place',
|
||||
geohash: 'u33dc0',
|
||||
createdAt: '2023-01-02T12:00:00.000Z',
|
||||
updatedAt: '2023-01-04T12:00:00.000Z',
|
||||
osmTags: { name: 'Middle Place' },
|
||||
},
|
||||
];
|
||||
lists = [
|
||||
{
|
||||
id: 'to-go',
|
||||
title: 'Want to go',
|
||||
color: '#2e9e4f',
|
||||
placeRefs: [
|
||||
{ id: 'place-oldest', geohash: 'u33dc0' },
|
||||
{ id: 'place-newest', geohash: 'u33dc0' },
|
||||
{ id: 'place-middle', geohash: 'u33dc0' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
findPlaceById(id) {
|
||||
return this.savedPlaces.find((p) => p.id === id) || null;
|
||||
}
|
||||
|
||||
isPlaceSaved() {
|
||||
return true;
|
||||
}
|
||||
|
||||
loadPlacesInBounds() {
|
||||
return [];
|
||||
}
|
||||
|
||||
getPlacesInList(listId) {
|
||||
if (listId === 'to-go') {
|
||||
return Promise.resolve(this.savedPlaces);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
rs = {
|
||||
on: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
this.owner.unregister('service:storage');
|
||||
this.owner.register('service:storage', SortedMockStorageService);
|
||||
|
||||
await visit('/lists/to-go');
|
||||
await waitFor('.places-list');
|
||||
|
||||
const placeNames = Array.from(
|
||||
document.querySelectorAll('.places-list .place-name')
|
||||
).map((el) => el.textContent.trim());
|
||||
|
||||
assert.deepEqual(
|
||||
placeNames,
|
||||
['Newest Place', 'Middle Place', 'Oldest Place'],
|
||||
'Places are ordered by createdAt in descending order'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,9 @@ import PhotoGallery from 'marco/components/photo-gallery';
|
||||
import { setupNostrMocks } from 'marco/tests/helpers/mock-nostr';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const USER_A = 'a'.repeat(64);
|
||||
const USER_B = 'b'.repeat(64);
|
||||
|
||||
class MockBlossomService extends Service {
|
||||
async delete() {
|
||||
return true;
|
||||
@@ -34,7 +37,7 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
this.photos = [
|
||||
{
|
||||
eventId: 'event1',
|
||||
pubkey: 'userA',
|
||||
pubkey: USER_A,
|
||||
placeIdentifier: 'osm:node:12345',
|
||||
url: 'https://example.com/a3b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1.jpg',
|
||||
thumbUrl:
|
||||
@@ -42,7 +45,7 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
},
|
||||
{
|
||||
eventId: 'event2',
|
||||
pubkey: 'userB',
|
||||
pubkey: USER_B,
|
||||
placeIdentifier: 'osm:node:12345',
|
||||
url: 'photo2.jpg',
|
||||
},
|
||||
@@ -55,7 +58,7 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
});
|
||||
|
||||
test('it does not show delete button if user is not creator', async function (assert) {
|
||||
this.nostrAuth.pubkey = 'userB'; // Different from photo1's pubkey
|
||||
this.nostrAuth.pubkey = USER_B; // Different from photo1's pubkey
|
||||
this.selectedPhoto = this.photos[0];
|
||||
|
||||
await render(
|
||||
@@ -80,7 +83,7 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
});
|
||||
|
||||
test('it shows delete button if user is creator and setting is enabled', async function (assert) {
|
||||
this.nostrAuth.pubkey = 'userA'; // Matches photo1's pubkey
|
||||
this.nostrAuth.pubkey = USER_A; // Matches photo1's pubkey
|
||||
this.settings.update('experimentalEnablePhotoDeletion', true); // Enable the setting
|
||||
this.selectedPhoto = this.photos[0];
|
||||
|
||||
@@ -106,7 +109,7 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
});
|
||||
|
||||
test('it handles cancellation of deletion', async function (assert) {
|
||||
this.nostrAuth.pubkey = 'userA';
|
||||
this.nostrAuth.pubkey = USER_A;
|
||||
this.settings.update('experimentalEnablePhotoDeletion', true);
|
||||
this.selectedPhoto = this.photos[0];
|
||||
|
||||
@@ -133,7 +136,7 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
});
|
||||
|
||||
test('it performs full deletion flow when confirmed', async function (assert) {
|
||||
this.nostrAuth.pubkey = 'userA';
|
||||
this.nostrAuth.pubkey = USER_A;
|
||||
this.settings.update('experimentalEnablePhotoDeletion', true);
|
||||
// Override the mock's getter just for this test
|
||||
Object.defineProperty(this.nostrAuth, 'signer', {
|
||||
@@ -141,11 +144,11 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
get: () => ({
|
||||
signEvent: async (e) => ({
|
||||
...e,
|
||||
id: 'signed-id',
|
||||
sig: 'sig',
|
||||
pubkey: 'userA',
|
||||
id: 'a3b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1',
|
||||
sig: 'b3b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1',
|
||||
pubkey: USER_A,
|
||||
}),
|
||||
getPublicKey: async () => 'userA',
|
||||
getPublicKey: async () => USER_A,
|
||||
}),
|
||||
});
|
||||
this.selectedPhoto = this.photos[0];
|
||||
@@ -227,7 +230,7 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
||||
});
|
||||
|
||||
test('it copies event id to clipboard', async function (assert) {
|
||||
this.nostrAuth.pubkey = 'userA';
|
||||
this.nostrAuth.pubkey = USER_A;
|
||||
this.selectedPhoto = this.photos[0];
|
||||
|
||||
const clipboardStub = sinon
|
||||
|
||||
@@ -97,4 +97,27 @@ module('Integration | Component | place-photo-upload', function (hooks) {
|
||||
|
||||
assert.dom('.photo-tag-suggestions').doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders an optional alt text input after upload selection', async function (assert) {
|
||||
this.place = {
|
||||
title: 'Cafe Alpha',
|
||||
osmId: '123',
|
||||
osmType: 'node',
|
||||
osmTags: { amenity: 'cafe' },
|
||||
};
|
||||
|
||||
await render(
|
||||
<template><PlacePhotoUpload @place={{this.place}} /></template>
|
||||
);
|
||||
|
||||
assert.dom('#photo-alt-input').doesNotExist();
|
||||
|
||||
const file = new File(['test'], 'photo.jpg', { type: 'image/jpeg' });
|
||||
await selectFile(this.element, file);
|
||||
|
||||
assert.dom('#photo-alt-input').exists();
|
||||
assert
|
||||
.dom('#photo-alt-input')
|
||||
.hasAttribute('placeholder', 'Describe this photo');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +148,68 @@ module('Unit | Utility | nostr', function () {
|
||||
assert.strictEqual(photos[1].placeIdentifier, 'osm:node:456');
|
||||
});
|
||||
|
||||
test('parsePlacePhotos extracts alt text from imeta', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [
|
||||
[
|
||||
'imeta',
|
||||
'url https://example.com/photo.jpg',
|
||||
'dim 800x600',
|
||||
'alt A sunny terrace overlooking the bay',
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 1);
|
||||
assert.strictEqual(photos[0].alt, 'A sunny terrace overlooking the bay');
|
||||
});
|
||||
|
||||
test('parsePlacePhotos strips the legacy placeholder alt text', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [
|
||||
[
|
||||
'imeta',
|
||||
'url https://example.com/photo.jpg',
|
||||
'dim 800x600',
|
||||
'alt A photo of a place',
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 1);
|
||||
assert.strictEqual(photos[0].alt, null);
|
||||
});
|
||||
|
||||
test('parsePlacePhotos leaves alt null when not provided', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [['imeta', 'url https://example.com/photo.jpg', 'dim 800x600']],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 1);
|
||||
assert.strictEqual(photos[0].alt, null);
|
||||
});
|
||||
|
||||
test('uniqNormalizedRelays returns normalized unique relays', function (assert) {
|
||||
const relays = uniqNormalizedRelays([
|
||||
'Relay.example.com',
|
||||
|
||||
Reference in New Issue
Block a user