Fetch and cache photo events while browsing map and when opening place details

This commit is contained in:
2026-04-21 21:28:57 +04:00
parent 8d40b3bb35
commit 99cfd96ca1
4 changed files with 154 additions and 3 deletions

View File

@@ -27,6 +27,7 @@ export default class MapComponent extends Component {
@service mapUi; @service mapUi;
@service router; @service router;
@service settings; @service settings;
@service nostrData;
mapInstance; mapInstance;
bookmarkSource; bookmarkSource;
@@ -1078,6 +1079,7 @@ export default class MapComponent extends Component {
const bbox = { minLat, minLon, maxLat, maxLon }; const bbox = { minLat, minLon, maxLat, maxLon };
this.mapUi.updateBounds(bbox); this.mapUi.updateBounds(bbox);
await this.storage.loadPlacesInBounds(bbox); await this.storage.loadPlacesInBounds(bbox);
this.nostrData.loadPlacesInBounds(bbox);
this.loadBookmarks(this.storage.placesInView); this.loadBookmarks(this.storage.placesInView);
// Persist view to localStorage // Persist view to localStorage

View File

@@ -183,6 +183,7 @@ export default class PlacePhotoUpload extends Component {
const event = await factory.sign(template); const event = await factory.sign(template);
await this.nostrRelay.publish(this.nostrData.activeWriteRelays, event); await this.nostrRelay.publish(this.nostrData.activeWriteRelays, event);
this.nostrData.store.add(event);
this.toast.show('Photos published successfully'); this.toast.show('Photos published successfully');
this.status = ''; this.status = '';

View File

@@ -1,7 +1,9 @@
import Service from '@ember/service'; import Service, { service } from '@ember/service';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
export default class MapUiService extends Service { export default class MapUiService extends Service {
@service nostrData;
@tracked selectedPlace = null; @tracked selectedPlace = null;
@tracked isSearching = false; @tracked isSearching = false;
@tracked isCreating = false; @tracked isCreating = false;
@@ -19,12 +21,14 @@ export default class MapUiService extends Service {
selectPlace(place, options = {}) { selectPlace(place, options = {}) {
this.selectedPlace = place; this.selectedPlace = place;
this.selectionOptions = options; this.selectionOptions = options;
this.nostrData.loadPhotosForPlace(place);
} }
clearSelection() { clearSelection() {
this.selectedPlace = null; this.selectedPlace = null;
this.selectionOptions = {}; this.selectionOptions = {};
this.preventNextZoom = false; this.preventNextZoom = false;
this.nostrData.loadPhotosForPlace(null);
} }
setSearchResults(results) { setSearchResults(results) {

View File

@@ -7,6 +7,7 @@ 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 { normalizeRelayUrl } from '../utils/nostr';
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
const DIRECTORY_RELAYS = [ const DIRECTORY_RELAYS = [
'wss://purplepag.es', 'wss://purplepag.es',
@@ -27,13 +28,16 @@ export default class NostrDataService extends Service {
@tracked profile = null; @tracked profile = null;
@tracked mailboxes = null; @tracked mailboxes = null;
@tracked blossomServers = []; @tracked blossomServers = [];
@tracked placePhotos = [];
_profileSub = null; _profileSub = null;
_mailboxesSub = null; _mailboxesSub = null;
_blossomSub = null; _blossomSub = null;
_photosSub = null;
_requestSub = null; _requestSub = null;
_cachePromise = null; _cachePromise = null;
loadedGeohashPrefixes = new Set();
constructor() { constructor() {
super(...arguments); super(...arguments);
@@ -51,9 +55,13 @@ export default class NostrDataService extends Service {
this._stopPersisting = persistEventsToCache( this._stopPersisting = persistEventsToCache(
this.store, this.store,
async (events) => { async (events) => {
// Only cache profiles, mailboxes, and blossom servers // Only cache profiles, mailboxes, blossom servers, and place photos
const toCache = events.filter( const toCache = events.filter(
(e) => e.kind === 0 || e.kind === 10002 || e.kind === 10063 (e) =>
e.kind === 0 ||
e.kind === 10002 ||
e.kind === 10063 ||
e.kind === 360
); );
if (toCache.length > 0) { if (toCache.length > 0) {
@@ -113,6 +121,138 @@ export default class NostrDataService extends Service {
return this.defaultWriteRelays; return this.defaultWriteRelays;
} }
async loadPlacesInBounds(bbox) {
const requiredPrefixes = getGeohashPrefixesInBbox(bbox);
const missingPrefixes = requiredPrefixes.filter(
(p) => !this.loadedGeohashPrefixes.has(p)
);
if (missingPrefixes.length === 0) {
return;
}
console.debug(
'[nostr-data] Loading place photos for prefixes:',
missingPrefixes
);
try {
await this._cachePromise;
const cachedEvents = await this.cache.query([
{
kinds: [360],
'#g': missingPrefixes,
},
]);
if (cachedEvents && cachedEvents.length > 0) {
for (const event of cachedEvents) {
this.store.add(event);
}
}
} catch (e) {
console.warn(
'[nostr-data] Failed to read photos from local Nostr IDB cache',
e
);
}
// Fire network request for new prefixes
this.nostrRelay.pool
.request(this.activeReadRelays, [
{
kinds: [360],
'#g': missingPrefixes,
},
])
.subscribe({
next: (event) => {
this.store.add(event);
},
error: (err) => {
console.error(
'[nostr-data] Error fetching place photos by geohash:',
err
);
},
});
for (const p of missingPrefixes) {
this.loadedGeohashPrefixes.add(p);
}
}
async loadPhotosForPlace(place) {
if (this._photosSub) {
this._photosSub.unsubscribe();
this._photosSub = null;
}
this.placePhotos = [];
if (!place || !place.osmId || !place.osmType) {
return;
}
const entityId = `osm:${place.osmType}:${place.osmId}`;
// Setup reactive store query
this._photosSub = this.store
.timeline([
{
kinds: [360],
'#i': [entityId],
},
])
.subscribe((events) => {
this.placePhotos = events;
});
try {
await this._cachePromise;
const cachedEvents = await this.cache.query([
{
kinds: [360],
'#i': [entityId],
},
]);
if (cachedEvents && cachedEvents.length > 0) {
for (const event of cachedEvents) {
this.store.add(event);
}
}
} catch (e) {
console.warn(
'[nostr-data] Failed to read photos for place from local Nostr IDB cache',
e
);
}
// Fire network request specifically for this place
this.nostrRelay.pool
.request(this.activeReadRelays, [
{
kinds: [360],
'#i': [entityId],
},
])
.subscribe({
next: (event) => {
this.store.add(event);
},
error: (err) => {
console.error(
'[nostr-data] Error fetching place photos for place:',
err
);
},
});
}
async loadProfile(pubkey) { async loadProfile(pubkey) {
if (!pubkey) return; if (!pubkey) return;
@@ -233,6 +373,10 @@ export default class NostrDataService extends Service {
this._blossomSub.unsubscribe(); this._blossomSub.unsubscribe();
this._blossomSub = null; this._blossomSub = null;
} }
if (this._photosSub) {
this._photosSub.unsubscribe();
this._photosSub = null;
}
} }
willDestroy() { willDestroy() {