From 0f0874a07ac19bd02cdbe0906346bda8d3a8a38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 19 Aug 2026 15:11:31 -0600 Subject: [PATCH 1/2] Fetch, cache, and update profile data --- app/services/nostr-data.js | 42 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 15 ++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/app/services/nostr-data.js b/app/services/nostr-data.js index e6cdb6a..a10d1b3 100644 --- a/app/services/nostr-data.js +++ b/app/services/nostr-data.js @@ -1,10 +1,12 @@ import Service, { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; +import { from } from 'rxjs'; import { EventStore } from 'applesauce-core/event-store'; import { ProfileModel } from 'applesauce-core/models/profile'; import { MailboxesModel } from 'applesauce-core/models/mailboxes'; import { npubEncode } from 'applesauce-core/helpers/pointers'; import { persistEventsToCache } from 'applesauce-core/helpers/event-cache'; +import { createEventLoaderForStore } from 'applesauce-loaders/loaders'; import { NostrIDB, openDB } from 'nostr-idb'; import { excludeRequiredRelays, @@ -35,12 +37,14 @@ export default class NostrDataService extends Service { @tracked blossomServers = []; @tracked placePhotos = []; @tracked myContributionEvents = []; + @tracked profiles = {}; _profileSub = null; _mailboxesSub = null; _blossomSub = null; _photosSub = null; _contributionsSub = null; + _profileModelSubs = new Map(); _requestSub = null; _cachePromise = null; @@ -81,6 +85,13 @@ export default class NostrDataService extends Service { maxBatchSize: 100, } ); + + // Set up the event loader so replaceable/event subscriptions auto-fetch + // from cache → relay hints → lookup relays with batching and dedup + createEventLoaderForStore(this.store, this.nostrRelay.pool, { + cacheRequest: (filters) => from(this.cache.query(filters)), + lookupRelays: DIRECTORY_RELAYS, + }); }); // Feed events from the relay pool into the event store @@ -218,6 +229,7 @@ export default class NostrDataService extends Service { } this.placePhotos = []; + this._clearProfileSubs(); if (!place || !place.osmId || !place.osmType) { return; @@ -235,6 +247,8 @@ export default class NostrDataService extends Service { ]) .subscribe((events) => { this.placePhotos = events; + const pubkeys = [...new Set(events.map((e) => e.pubkey))]; + this.loadProfiles(pubkeys); }); try { @@ -331,6 +345,33 @@ export default class NostrDataService extends Service { }); } + loadProfiles(pubkeys) { + const newPubkeys = pubkeys.filter( + (pk) => pk && !this._profileModelSubs.has(pk) + ); + + for (const pubkey of newPubkeys) { + const sub = this.store + .model(ProfileModel, pubkey) + .subscribe((profileContent) => { + this.profiles = { ...this.profiles, [pubkey]: profileContent }; + }); + this._profileModelSubs.set(pubkey, sub); + } + } + + getProfile(pubkey) { + return this.profiles[pubkey]; + } + + _clearProfileSubs() { + for (const sub of this._profileModelSubs.values()) { + sub.unsubscribe(); + } + this._profileModelSubs.clear(); + this.profiles = {}; + } + async loadProfile(pubkey) { if (!pubkey) return; @@ -466,6 +507,7 @@ export default class NostrDataService extends Service { this._contributionsSub.unsubscribe(); this._contributionsSub = null; } + this._clearProfileSubs(); } willDestroy() { diff --git a/package.json b/package.json index 2210d05..88e4ac4 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "@noble/hashes": "^2.3.0", "@waysidemapping/pinhead": "^15.25.0", "applesauce-core": "^6.2.0", + "applesauce-loaders": "^6.2.0", "applesauce-relay": "^6.2.1", "applesauce-signers": "^6.2.2", "blurhash": "^2.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd88e9a..0c5d0a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: applesauce-core: specifier: ^6.2.0 version: 6.2.0(supports-color@10.2.2)(typescript@5.9.3) + applesauce-loaders: + specifier: ^6.2.0 + version: 6.2.0(supports-color@10.2.2)(typescript@5.9.3) applesauce-relay: specifier: ^6.2.1 version: 6.2.1(supports-color@10.2.2)(typescript@5.9.3) @@ -2636,6 +2639,9 @@ packages: applesauce-core@6.2.0: resolution: {integrity: sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw==} + applesauce-loaders@6.2.0: + resolution: {integrity: sha512-isU2BuoVFhugv4WxdbUFrqm7mCNgsGrMU4BYcNqMm8qAVpVb3L7K3I1hlkIZWwOvNTkhoYO/u0utgwnkQj4Hrg==} + applesauce-relay@6.2.1: resolution: {integrity: sha512-YIUHEtL2Fl5FZjeKYe/IuwL3OnMUNP2j4ydSr3iZDeHEZSEda4d1PoChefiNhtjE/U/F5OBHEbAX1zCHRZoxpg==} @@ -9354,6 +9360,15 @@ snapshots: - supports-color - typescript + applesauce-loaders@6.2.0(supports-color@10.2.2)(typescript@5.9.3): + dependencies: + applesauce-core: 6.2.0(supports-color@10.2.2)(typescript@5.9.3) + nanoid: 5.1.9 + rxjs: 7.8.2 + transitivePeerDependencies: + - supports-color + - typescript + applesauce-relay@6.2.1(supports-color@10.2.2)(typescript@5.9.3): dependencies: '@noble/hashes': 2.3.0 From bfcf855020de508701481d7fe4686060473a9448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 19 Aug 2026 16:03:40 -0600 Subject: [PATCH 2/2] Render data and uploader name for current photo in gallery --- app/components/photo-gallery.gjs | 81 +++++++--- app/services/nostr-data.js | 46 ++++-- app/styles/app.css | 68 ++++++++- app/utils/contributions.js | 3 + app/utils/nostr.js | 5 + tests/helpers/mock-nostr.js | 5 + .../components/photo-gallery-test.gjs | 139 ++++++++++++++++++ tests/unit/utils/nostr-test.js | 60 ++++++++ 8 files changed, 368 insertions(+), 39 deletions(-) diff --git a/app/components/photo-gallery.gjs b/app/components/photo-gallery.gjs index fd6af55..f8283f7 100644 --- a/app/components/photo-gallery.gjs +++ b/app/components/photo-gallery.gjs @@ -7,10 +7,12 @@ import { service } from '@ember/service'; import { modifier } from 'ember-modifier'; import { task } from 'ember-concurrency'; import { EventFactory } from 'applesauce-core'; +import or from 'ember-truth-helpers/helpers/or'; import config from 'marco/config/environment'; import DropdownMenu from './dropdown-menu'; import PhotoCarousel from './photo-carousel'; import Icon from './icon'; +import formatRelativeDate from '../helpers/format-relative-date'; const GalleryContent =