Compare commits

..
11 Commits
Author SHA1 Message Date
raucao 387fd6efed 1.26.0
CI / Lint (push) Successful in 53s
CI / Test (push) Successful in 1m4s
2026-08-07 07:33:52 -06:00
raucao aaf0d8c3e9 Merge pull request 'Add alt text to place photos' (#71) from bugix/photo_alt_text into master
CI / Lint (push) Successful in 53s
CI / Test (push) Successful in 1m2s
Reviewed-on: #71
2026-08-07 13:30:39 +00:00
raucao 9807a2b822 Add alt text to photo uploads
CI / Lint (pull_request) Successful in 53s
CI / Test (pull_request) Successful in 1m6s
Release Drafter / Update release notes draft (pull_request) Successful in 5s
- Form field for optionally adding a description
- Publish as `imeta` `alt`
- Render as `<img>` alt when set

Fixes a placeholder being published instead. Also strips the current
placeholder if set as alt text (for existing uploads).
2026-08-07 07:25:53 -06:00
raucao ac9870240f Merge pull request 'Update dependencies' (#70) from chore/update_dependencies into master
CI / Lint (push) Successful in 1m7s
CI / Test (push) Successful in 59s
Reviewed-on: #70
2026-08-06 22:00:01 +00:00
raucao 1b185d1033 Upgrade lint and dev tooling to latest majors
CI / Test (pull_request) Successful in 1m4s
CI / Lint (pull_request) Successful in 1m12s
Release Drafter / Update release notes draft (pull_request) Successful in 21s
Bump eslint to ^10, @eslint/js to ^10, eslint-plugin-ember to ^13,
eslint-plugin-n to ^18, globals to ^17, stylelint to ^17,
stylelint-config-standard to ^40, sinon to ^22, and concurrently
to ^10.

Fix three new lint violations surfaced by the newer versions:
- Remove redundant padding-left after padding shorthand (stylelint 17)
- Remove useless null assignment before find() (eslint 10
  no-useless-assignment)
- Attach caught error as cause on rethrown Error (eslint 10
  preserve-caught-error)
2026-08-06 16:40:54 -05:00
raucao 4d4ed8c1ea Upgrade to oauth2-pkce 3
Bump oauth2-pkce from ^2.1.3 to ^3.0.0. In v3, getTokens() throws
ErrorAccessTokenExpired when the token has expired and no
onAccessTokenExpiry handler is configured. Add the handler to the
OSM auth client config so it automatically refreshes via the refresh
token instead of throwing
2026-08-06 16:35:11 -05:00
raucao 1613ebf0bf Upgrade to Ember 7.1
Bump ember-source from ~6.12.0 to ~7.1.0 and ember-cli from ^6.12.0
to ^7.1.0. Ember 7 removes APIs deprecated during the 6.x cycle
(import Ember, AMD bundles, inject from @ember/service) — none of
which were used in this codebase, so no source changes were needed.
2026-08-06 16:32:18 -05:00
raucao 4cb8c94c9c Upgrade to Babel 8, Vite 8, and babel-plugin-ember-template-compilation 4
Bump @babel/core, @babel/eslint-parser, @babel/plugin-transform-runtime,
and @babel/runtime to ^8.x. Remove useESModules and regenerator options
from @babel/plugin-transform-runtime config (both removed in Babel 8).
Switch @babel/eslint-parser import from the removed /experimental-worker
entry point to the main entry point.

Bump vite to ^8.0.0 (now using Rolldown/Oxc instead of esbuild/Rollup).
Bump @rollup/plugin-babel to ^7.1.0 and
babel-plugin-ember-template-compilation to ^4.0.0.

Patch ember-concurrency@5.2.0 to remove the api.assertVersion(7) call
that is incompatible with Babel 8 (ember-concurrency has not yet
released a Babel 8-compatible version).
2026-08-06 16:26:23 -05:00
raucao 1f87c802e1 Upgrade to Ember 6.12 stable
Move ember-source from ~6.11.0-alpha.6 to ~6.12.0 and ember-cli from
^6.10.0 to ^6.12.0. This is the final stable release of the 6.x
series and an LTS, clearing the way for a clean Ember 7 upgrade.
2026-08-06 16:19:14 -05:00
raucao a6576fcda8 Migrate to applesauce v6
Remove applesauce-factory (deleted in v6) and bump applesauce-core,
applesauce-relay, and applesauce-signers to ^6.2.x. Rewrite all
EventFactory usage from the removed constructor API
(`new EventFactory({ signer }).sign(template)`) to the new chainable
builder (`EventFactory.fromKind(k).content(...).sign(signer)`).

Remove `relay.eoseTimeout` overrides in nostr-auth — the eoseTimeout
property was removed in v6; NostrConnectSigner now uses persistent
subscriptions that don't rely on EOSE-based timeouts.

Update photo-gallery test mock to use 64-char hex pubkeys/ids/sigs
because v6's sign() validates event structure via validateEvent().

Also includes safe semver bumps to all other dependencies.
2026-08-06 16:13:24 -05:00
raucao 1822c4893f Update pinhead icon paths for 15.25.0 2026-08-06 16:12:37 -05:00
31 changed files with 2933 additions and 2720 deletions
+4 -4
View File
@@ -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 -10
View File
@@ -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
+35 -21
View File
@@ -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"
+1 -3
View File
@@ -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
View File
@@ -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, '-')
+1 -1
View File
@@ -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 });
}
}
-6
View File
@@ -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],
+3
View File
@@ -45,6 +45,9 @@ export default class OsmAuthService extends Service {
clientId: clientId,
redirectUrl: redirectUrl,
storeRefreshToken: true,
onAccessTokenExpiry() {
return this.exchangeRefreshTokenForAccessToken();
},
},
new MarcoOsmAuthStorage()
);
+6 -9
View File
@@ -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
View File
@@ -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';
+6
View File
@@ -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,
});
}
}
-2
View File
@@ -37,8 +37,6 @@ export default {
'@babel/plugin-transform-runtime',
{
absoluteRuntime: dirname(fileURLToPath(import.meta.url)),
useESModules: true,
regenerator: false,
},
],
...macros.babelMacros,
+1 -1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "marco",
"version": "1.25.1",
"version": "1.26.0",
"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"
}
+13
View File
@@ -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',
+2507 -2555
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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})}}}();
+182
View File
@@ -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
View File
@@ -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-DNM-h3Gw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BGF-Udec.css">
<script type="module" crossorigin src="/assets/main-B2gk-G51.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-CIu0qcV7.css">
</head>
<body>
<div id="modal-portal"></div>
@@ -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');
});
});
+62
View File
@@ -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',