Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d547dde87
|
||
|
|
85334ec15d
|
||
|
|
bfcf855020
|
||
|
|
0f0874a07a
|
@@ -7,10 +7,12 @@ import { service } from '@ember/service';
|
|||||||
import { modifier } from 'ember-modifier';
|
import { modifier } from 'ember-modifier';
|
||||||
import { task } from 'ember-concurrency';
|
import { task } from 'ember-concurrency';
|
||||||
import { EventFactory } from 'applesauce-core';
|
import { EventFactory } from 'applesauce-core';
|
||||||
|
import or from 'ember-truth-helpers/helpers/or';
|
||||||
import config from 'marco/config/environment';
|
import config from 'marco/config/environment';
|
||||||
import DropdownMenu from './dropdown-menu';
|
import DropdownMenu from './dropdown-menu';
|
||||||
import PhotoCarousel from './photo-carousel';
|
import PhotoCarousel from './photo-carousel';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
import formatRelativeDate from '../helpers/format-relative-date';
|
||||||
|
|
||||||
const GalleryContent = <template>
|
const GalleryContent = <template>
|
||||||
<div
|
<div
|
||||||
@@ -25,10 +27,11 @@ const GalleryContent = <template>
|
|||||||
class="photo-gallery-content"
|
class="photo-gallery-content"
|
||||||
data-current-event-id={{@currentPhoto.eventId}}
|
data-current-event-id={{@currentPhoto.eventId}}
|
||||||
>
|
>
|
||||||
|
<div class="photo-gallery-header">
|
||||||
<div class="actions-btn-container">
|
<div class="actions-btn-container">
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
@iconSize={{24}}
|
@iconSize={{24}}
|
||||||
@triggerIcon="more-horizontal"
|
@triggerIcon={{@triggerIcon}}
|
||||||
@iconColor="white"
|
@iconColor="white"
|
||||||
as |closeMenu|
|
as |closeMenu|
|
||||||
>
|
>
|
||||||
@@ -47,6 +50,20 @@ const GalleryContent = <template>
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if (or @uploaderName @photoDate)}}
|
||||||
|
<div class="photo-gallery-uploader-info">
|
||||||
|
{{#if @uploaderName}}
|
||||||
|
<span class="photo-gallery-uploader-name">{{@uploaderName}}</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if @photoDate}}
|
||||||
|
<span class="photo-gallery-uploader-date">
|
||||||
|
{{formatRelativeDate @photoDate}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="close-btn btn-text"
|
class="close-btn btn-text"
|
||||||
@@ -98,6 +115,13 @@ export default class PhotoGallery extends Component {
|
|||||||
|
|
||||||
@tracked currentPhoto = this.args.selectedPhoto || this.args.photos?.[0];
|
@tracked currentPhoto = this.args.selectedPhoto || this.args.photos?.[0];
|
||||||
|
|
||||||
|
get triggerIcon() {
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth <= 768) {
|
||||||
|
return 'more-vertical';
|
||||||
|
}
|
||||||
|
return 'more-horizontal';
|
||||||
|
}
|
||||||
|
|
||||||
get isCreator() {
|
get isCreator() {
|
||||||
return (
|
return (
|
||||||
this.currentPhoto?.pubkey &&
|
this.currentPhoto?.pubkey &&
|
||||||
@@ -112,6 +136,19 @@ export default class PhotoGallery extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get uploaderName() {
|
||||||
|
const pubkey = this.currentPhoto?.pubkey;
|
||||||
|
if (!pubkey) return null;
|
||||||
|
const profile = this.nostrData.getProfile?.(pubkey);
|
||||||
|
if (!profile) return null;
|
||||||
|
return profile.displayName || profile.display_name || profile.name || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get photoDate() {
|
||||||
|
const ts = this.currentPhoto?.publishedAt || this.currentPhoto?.createdAt;
|
||||||
|
return ts || null;
|
||||||
|
}
|
||||||
|
|
||||||
bindKeyboard = modifier((element, [handler]) => {
|
bindKeyboard = modifier((element, [handler]) => {
|
||||||
document.addEventListener('keydown', handler);
|
document.addEventListener('keydown', handler);
|
||||||
return () => document.removeEventListener('keydown', handler);
|
return () => document.removeEventListener('keydown', handler);
|
||||||
@@ -131,7 +168,7 @@ export default class PhotoGallery extends Component {
|
|||||||
e.target.closest('.thumbnail-strip-container') ||
|
e.target.closest('.thumbnail-strip-container') ||
|
||||||
e.target.closest('.carousel-nav-btn') ||
|
e.target.closest('.carousel-nav-btn') ||
|
||||||
e.target.closest('.close-btn') ||
|
e.target.closest('.close-btn') ||
|
||||||
e.target.closest('.actions-btn-container')
|
e.target.closest('.photo-gallery-header')
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -268,6 +305,9 @@ export default class PhotoGallery extends Component {
|
|||||||
@handleVisiblePhotoChange={{this.handleVisiblePhotoChange}}
|
@handleVisiblePhotoChange={{this.handleVisiblePhotoChange}}
|
||||||
@placeName={{@placeName}}
|
@placeName={{@placeName}}
|
||||||
@selectPhoto={{this.selectPhoto}}
|
@selectPhoto={{this.selectPhoto}}
|
||||||
|
@uploaderName={{this.uploaderName}}
|
||||||
|
@photoDate={{this.photoDate}}
|
||||||
|
@triggerIcon={{this.triggerIcon}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#in-element this.destinationElement}}
|
{{#in-element this.destinationElement}}
|
||||||
@@ -284,6 +324,9 @@ export default class PhotoGallery extends Component {
|
|||||||
@handleVisiblePhotoChange={{this.handleVisiblePhotoChange}}
|
@handleVisiblePhotoChange={{this.handleVisiblePhotoChange}}
|
||||||
@placeName={{@placeName}}
|
@placeName={{@placeName}}
|
||||||
@selectPhoto={{this.selectPhoto}}
|
@selectPhoto={{this.selectPhoto}}
|
||||||
|
@uploaderName={{this.uploaderName}}
|
||||||
|
@photoDate={{this.photoDate}}
|
||||||
|
@triggerIcon={{this.triggerIcon}}
|
||||||
/>
|
/>
|
||||||
{{/in-element}}
|
{{/in-element}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Service, { service } from '@ember/service';
|
import Service, { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { EMPTY, from } from 'rxjs';
|
||||||
import { EventStore } from 'applesauce-core/event-store';
|
import { EventStore } from 'applesauce-core/event-store';
|
||||||
import { ProfileModel } from 'applesauce-core/models/profile';
|
import { ProfileModel } from 'applesauce-core/models/profile';
|
||||||
import { MailboxesModel } from 'applesauce-core/models/mailboxes';
|
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 { createEventLoaderForStore } from 'applesauce-loaders/loaders';
|
||||||
import { NostrIDB, openDB } from 'nostr-idb';
|
import { NostrIDB, openDB } from 'nostr-idb';
|
||||||
import {
|
import {
|
||||||
excludeRequiredRelays,
|
excludeRequiredRelays,
|
||||||
@@ -15,9 +17,9 @@ import {
|
|||||||
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
||||||
|
|
||||||
const DIRECTORY_RELAYS = [
|
const DIRECTORY_RELAYS = [
|
||||||
'wss://purplepag.es',
|
'wss://relay.primal.net',
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol',
|
'wss://nos.lol',
|
||||||
|
'wss://relay.damus.io',
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_READ_RELAYS = ['wss://nostr.kosmos.org'];
|
const DEFAULT_READ_RELAYS = ['wss://nostr.kosmos.org'];
|
||||||
@@ -35,20 +37,36 @@ export default class NostrDataService extends Service {
|
|||||||
@tracked blossomServers = [];
|
@tracked blossomServers = [];
|
||||||
@tracked placePhotos = [];
|
@tracked placePhotos = [];
|
||||||
@tracked myContributionEvents = [];
|
@tracked myContributionEvents = [];
|
||||||
|
@tracked profiles = {};
|
||||||
|
|
||||||
_profileSub = null;
|
_profileSub = null;
|
||||||
_mailboxesSub = null;
|
_mailboxesSub = null;
|
||||||
_blossomSub = null;
|
_blossomSub = null;
|
||||||
_photosSub = null;
|
_photosSub = null;
|
||||||
_contributionsSub = null;
|
_contributionsSub = null;
|
||||||
|
_profileModelSubs = new Map();
|
||||||
|
|
||||||
_requestSub = null;
|
_requestSub = null;
|
||||||
_cachePromise = null;
|
_cachePromise = null;
|
||||||
|
_currentPlaceEntityId = null;
|
||||||
loadedGeohashPrefixes = new Set();
|
loadedGeohashPrefixes = new Set();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
|
// Set up the event loader synchronously so that any subscription
|
||||||
|
// (e.g. loadProfiles from a route's afterModel) can auto-fetch even
|
||||||
|
// before the IndexedDB cache has finished opening. The cacheRequest
|
||||||
|
// is lazy — it returns EMPTY until `this.cache` is available, so the
|
||||||
|
// loader falls through to relay hints → lookup relays in the meantime.
|
||||||
|
createEventLoaderForStore(this.store, this.nostrRelay.pool, {
|
||||||
|
cacheRequest: (filters) => {
|
||||||
|
if (!this.cache) return EMPTY;
|
||||||
|
return from(this.cache.query(filters));
|
||||||
|
},
|
||||||
|
lookupRelays: DIRECTORY_RELAYS,
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize the IndexedDB cache
|
// Initialize the IndexedDB cache
|
||||||
this._cachePromise = openDB('applesauce-events').then(async (db) => {
|
this._cachePromise = openDB('applesauce-events').then(async (db) => {
|
||||||
this.cache = new NostrIDB(db, {
|
this.cache = new NostrIDB(db, {
|
||||||
@@ -212,19 +230,31 @@ export default class NostrDataService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadPhotosForPlace(place) {
|
async loadPhotosForPlace(place) {
|
||||||
|
const entityId =
|
||||||
|
place && place.osmId && place.osmType
|
||||||
|
? `osm:${place.osmType}:${place.osmId}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Skip the full reset if we're loading the same place again (e.g. from
|
||||||
|
// checkUpdates calling selectPlace a second time). This prevents tearing
|
||||||
|
// down timeline and profile subscriptions that are still in-flight.
|
||||||
|
if (entityId && entityId === this._currentPlaceEntityId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._photosSub) {
|
if (this._photosSub) {
|
||||||
this._photosSub.unsubscribe();
|
this._photosSub.unsubscribe();
|
||||||
this._photosSub = null;
|
this._photosSub = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.placePhotos = [];
|
this.placePhotos = [];
|
||||||
|
this._clearProfileSubs();
|
||||||
|
this._currentPlaceEntityId = entityId;
|
||||||
|
|
||||||
if (!place || !place.osmId || !place.osmType) {
|
if (!entityId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityId = `osm:${place.osmType}:${place.osmId}`;
|
|
||||||
|
|
||||||
// Setup reactive store query
|
// Setup reactive store query
|
||||||
this._photosSub = this.store
|
this._photosSub = this.store
|
||||||
.timeline([
|
.timeline([
|
||||||
@@ -235,6 +265,8 @@ export default class NostrDataService extends Service {
|
|||||||
])
|
])
|
||||||
.subscribe((events) => {
|
.subscribe((events) => {
|
||||||
this.placePhotos = events;
|
this.placePhotos = events;
|
||||||
|
const pubkeys = [...new Set(events.map((e) => e.pubkey))];
|
||||||
|
this.loadProfiles(pubkeys);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -331,6 +363,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) {
|
async loadProfile(pubkey) {
|
||||||
if (!pubkey) return;
|
if (!pubkey) return;
|
||||||
|
|
||||||
@@ -471,6 +530,7 @@ export default class NostrDataService extends Service {
|
|||||||
willDestroy() {
|
willDestroy() {
|
||||||
super.willDestroy(...arguments);
|
super.willDestroy(...arguments);
|
||||||
this._cleanupSubscriptions();
|
this._cleanupSubscriptions();
|
||||||
|
this._clearProfileSubs();
|
||||||
|
|
||||||
if (this._stopPersisting) {
|
if (this._stopPersisting) {
|
||||||
this._stopPersisting();
|
this._stopPersisting();
|
||||||
|
|||||||
+61
-5
@@ -2151,17 +2151,73 @@ button.create-place {
|
|||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Actions button in photo gallery */
|
/* Photo gallery header (actions button + uploader info) */
|
||||||
.photo-gallery-overlay .actions-btn-container {
|
.photo-gallery-overlay .photo-gallery-header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
left: 0.5rem;
|
left: 1rem;
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery-overlay .photo-gallery-header .actions-btn-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Uploader info (name + date) in photo gallery */
|
||||||
|
.photo-gallery-overlay .photo-gallery-uploader-info {
|
||||||
|
color: rgb(255 255 255 / 90%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
pointer-events: none;
|
||||||
|
text-shadow: 0 1px 2px rgb(0 0 0 / 60%);
|
||||||
|
max-width: calc(100vw - 4rem);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery-overlay .photo-gallery-uploader-name {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery-overlay .photo-gallery-uploader-date {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgb(255 255 255 / 70%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 768px) {
|
||||||
|
.photo-gallery-overlay .photo-gallery-header {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery-overlay .photo-gallery-header .actions-btn-container {
|
||||||
|
width: 48px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery-overlay .photo-gallery-uploader-info {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Snappy slide-in from left (Desktop) */
|
/* Snappy slide-in from left (Desktop) */
|
||||||
|
|||||||
@@ -94,11 +94,14 @@ function parsePhotoFromEvent(event) {
|
|||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
|
|
||||||
const placeIdentifier = tags.find((t) => t[0] === 'i')?.[1];
|
const placeIdentifier = tags.find((t) => t[0] === 'i')?.[1];
|
||||||
|
const publishedAtRaw = tags.find((t) => t[0] === 'published_at')?.[1];
|
||||||
|
const publishedAt = publishedAtRaw ? Number(publishedAtRaw) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
createdAt: event.created_at,
|
createdAt: event.created_at,
|
||||||
|
publishedAt: publishedAt && publishedAt > 0 ? publishedAt : null,
|
||||||
url,
|
url,
|
||||||
thumbUrl,
|
thumbUrl,
|
||||||
blurhash,
|
blurhash,
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ export function parsePlacePhotos(events) {
|
|||||||
let aspectRatio = 16 / 9; // default
|
let aspectRatio = 16 / 9; // default
|
||||||
let altText = null;
|
let altText = null;
|
||||||
let placeIdentifier = event.tags.find((t) => t[0] === 'i')?.[1];
|
let placeIdentifier = event.tags.find((t) => t[0] === 'i')?.[1];
|
||||||
|
const publishedAtRaw = event.tags.find(
|
||||||
|
(t) => t[0] === 'published_at'
|
||||||
|
)?.[1];
|
||||||
|
const publishedAt = publishedAtRaw ? Number(publishedAtRaw) : null;
|
||||||
|
|
||||||
for (const tag of imeta.slice(1)) {
|
for (const tag of imeta.slice(1)) {
|
||||||
if (tag.startsWith('url ')) {
|
if (tag.startsWith('url ')) {
|
||||||
@@ -98,6 +102,7 @@ export function parsePlacePhotos(events) {
|
|||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
createdAt: event.created_at,
|
createdAt: event.created_at,
|
||||||
|
publishedAt: publishedAt && publishedAt > 0 ? publishedAt : null,
|
||||||
url,
|
url,
|
||||||
thumbUrl,
|
thumbUrl,
|
||||||
blurhash,
|
blurhash,
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.27.1",
|
"version": "1.28.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -105,6 +105,7 @@
|
|||||||
"@noble/hashes": "^2.3.0",
|
"@noble/hashes": "^2.3.0",
|
||||||
"@waysidemapping/pinhead": "^15.25.0",
|
"@waysidemapping/pinhead": "^15.25.0",
|
||||||
"applesauce-core": "^6.2.0",
|
"applesauce-core": "^6.2.0",
|
||||||
|
"applesauce-loaders": "^6.2.0",
|
||||||
"applesauce-relay": "^6.2.1",
|
"applesauce-relay": "^6.2.1",
|
||||||
"applesauce-signers": "^6.2.2",
|
"applesauce-signers": "^6.2.2",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
|
|||||||
Generated
+15
@@ -20,6 +20,9 @@ importers:
|
|||||||
applesauce-core:
|
applesauce-core:
|
||||||
specifier: ^6.2.0
|
specifier: ^6.2.0
|
||||||
version: 6.2.0(supports-color@10.2.2)(typescript@5.9.3)
|
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:
|
applesauce-relay:
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.2.1(supports-color@10.2.2)(typescript@5.9.3)
|
version: 6.2.1(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
@@ -2636,6 +2639,9 @@ packages:
|
|||||||
applesauce-core@6.2.0:
|
applesauce-core@6.2.0:
|
||||||
resolution: {integrity: sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw==}
|
resolution: {integrity: sha512-O6AlVyzqcuIhTOIuexm6UWmx7mRIa2D98gJP7K7vFGf90YdtvSH78AsKQWZXJ0Pk/K14at+9zkwfCkFr7ghgNw==}
|
||||||
|
|
||||||
|
applesauce-loaders@6.2.0:
|
||||||
|
resolution: {integrity: sha512-isU2BuoVFhugv4WxdbUFrqm7mCNgsGrMU4BYcNqMm8qAVpVb3L7K3I1hlkIZWwOvNTkhoYO/u0utgwnkQj4Hrg==}
|
||||||
|
|
||||||
applesauce-relay@6.2.1:
|
applesauce-relay@6.2.1:
|
||||||
resolution: {integrity: sha512-YIUHEtL2Fl5FZjeKYe/IuwL3OnMUNP2j4ydSr3iZDeHEZSEda4d1PoChefiNhtjE/U/F5OBHEbAX1zCHRZoxpg==}
|
resolution: {integrity: sha512-YIUHEtL2Fl5FZjeKYe/IuwL3OnMUNP2j4ydSr3iZDeHEZSEda4d1PoChefiNhtjE/U/F5OBHEbAX1zCHRZoxpg==}
|
||||||
|
|
||||||
@@ -9354,6 +9360,15 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- 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):
|
applesauce-relay@6.2.1(supports-color@10.2.2)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 2.3.0
|
'@noble/hashes': 2.3.0
|
||||||
|
|||||||
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-TileColor" content="#F6E9A6">
|
||||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||||
|
|
||||||
<script type="module" crossorigin src="/assets/main-D3dQ-CGf.js"></script>
|
<script type="module" crossorigin src="/assets/main-C4QF_Hr3.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-P9lvOtv_.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-Cal_p7lY.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="modal-portal"></div>
|
<div id="modal-portal"></div>
|
||||||
|
|||||||
@@ -36,11 +36,16 @@ export class MockNostrDataService extends Service {
|
|||||||
@tracked mailboxes = null;
|
@tracked mailboxes = null;
|
||||||
@tracked blossomServers = [];
|
@tracked blossomServers = [];
|
||||||
@tracked placePhotos = [];
|
@tracked placePhotos = [];
|
||||||
|
@tracked profiles = {};
|
||||||
|
|
||||||
store = {
|
store = {
|
||||||
add: () => {},
|
add: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getProfile(pubkey) {
|
||||||
|
return this.profiles[pubkey];
|
||||||
|
}
|
||||||
|
|
||||||
get activeReadRelays() {
|
get activeReadRelays() {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,4 +339,143 @@ module('Integration | Component | photo-gallery', function (hooks) {
|
|||||||
await triggerKeyEvent(document, 'keydown', 'Escape');
|
await triggerKeyEvent(document, 'keydown', 'Escape');
|
||||||
assert.ok(closed, 'gallery was closed on escape key');
|
assert.ok(closed, 'gallery was closed on escape key');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it renders uploader name and date when profile is loaded', async function (assert) {
|
||||||
|
const displayName = 'Alice';
|
||||||
|
const publishedAt = Math.floor(Date.now() / 1000) - 60 * 60 * 24; // 1 day ago
|
||||||
|
|
||||||
|
this.nostrData.profiles = {
|
||||||
|
[USER_A]: { displayName, display_name: 'ignored', name: 'ignored' },
|
||||||
|
};
|
||||||
|
|
||||||
|
this.photos = [
|
||||||
|
{
|
||||||
|
eventId: 'event1',
|
||||||
|
pubkey: USER_A,
|
||||||
|
placeIdentifier: 'osm:node:12345',
|
||||||
|
url: 'https://example.com/photo.jpg',
|
||||||
|
publishedAt,
|
||||||
|
createdAt: publishedAt + 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this.selectedPhoto = this.photos[0];
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template>
|
||||||
|
<div id="test-container">
|
||||||
|
<div id="modal-portal"></div>
|
||||||
|
<PhotoGallery
|
||||||
|
@photos={{this.photos}}
|
||||||
|
@selectedPhoto={{this.selectedPhoto}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('.photo-gallery-uploader-name')
|
||||||
|
.hasText(displayName, 'uploader name is rendered');
|
||||||
|
assert
|
||||||
|
.dom('.photo-gallery-uploader-date')
|
||||||
|
.exists('date element is rendered');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it prefers published_at over created_at for the date', async function (assert) {
|
||||||
|
this.nostrData.profiles = {
|
||||||
|
[USER_A]: { displayName: 'Alice' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishedAt = Math.floor(Date.now() / 1000) - 60 * 60 * 24 * 3; // 3 days ago
|
||||||
|
const createdAt = Math.floor(Date.now() / 1000) - 60; // 1 min ago
|
||||||
|
|
||||||
|
this.photos = [
|
||||||
|
{
|
||||||
|
eventId: 'event1',
|
||||||
|
pubkey: USER_A,
|
||||||
|
url: 'https://example.com/photo.jpg',
|
||||||
|
publishedAt,
|
||||||
|
createdAt,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this.selectedPhoto = this.photos[0];
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template>
|
||||||
|
<div id="test-container">
|
||||||
|
<div id="modal-portal"></div>
|
||||||
|
<PhotoGallery
|
||||||
|
@photos={{this.photos}}
|
||||||
|
@selectedPhoto={{this.selectedPhoto}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show "3 days ago" (publishedAt), not "just now" (createdAt)
|
||||||
|
assert.dom('.photo-gallery-uploader-date').hasText('3 days ago');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it does not render uploader info when profile is missing', async function (assert) {
|
||||||
|
this.nostrData.profiles = {};
|
||||||
|
|
||||||
|
this.photos = [
|
||||||
|
{
|
||||||
|
eventId: 'event1',
|
||||||
|
pubkey: USER_A,
|
||||||
|
url: 'https://example.com/photo.jpg',
|
||||||
|
createdAt: Math.floor(Date.now() / 1000),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this.selectedPhoto = this.photos[0];
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template>
|
||||||
|
<div id="test-container">
|
||||||
|
<div id="modal-portal"></div>
|
||||||
|
<PhotoGallery
|
||||||
|
@photos={{this.photos}}
|
||||||
|
@selectedPhoto={{this.selectedPhoto}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('.photo-gallery-uploader-name')
|
||||||
|
.doesNotExist('uploader name is not rendered without profile');
|
||||||
|
// Date should still render since createdAt is present
|
||||||
|
assert.dom('.photo-gallery-uploader-date').exists('date is still rendered');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it falls back through displayName -> display_name -> name', async function (assert) {
|
||||||
|
this.nostrData.profiles = {
|
||||||
|
[USER_A]: { display_name: 'Bob', name: 'Robert' },
|
||||||
|
};
|
||||||
|
|
||||||
|
this.photos = [
|
||||||
|
{
|
||||||
|
eventId: 'event1',
|
||||||
|
pubkey: USER_A,
|
||||||
|
url: 'https://example.com/photo.jpg',
|
||||||
|
createdAt: Math.floor(Date.now() / 1000) - 60,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this.selectedPhoto = this.photos[0];
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template>
|
||||||
|
<div id="test-container">
|
||||||
|
<div id="modal-portal"></div>
|
||||||
|
<PhotoGallery
|
||||||
|
@photos={{this.photos}}
|
||||||
|
@selectedPhoto={{this.selectedPhoto}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('.photo-gallery-uploader-name')
|
||||||
|
.hasText('Bob', 'falls back to display_name when displayName is missing');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -210,6 +210,66 @@ module('Unit | Utility | nostr', function () {
|
|||||||
assert.strictEqual(photos[0].alt, null);
|
assert.strictEqual(photos[0].alt, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parsePlacePhotos extracts published_at when present', function (assert) {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
id: 'event-1',
|
||||||
|
pubkey: 'pubkey-1',
|
||||||
|
created_at: 200,
|
||||||
|
tags: [
|
||||||
|
['i', 'osm:node:123'],
|
||||||
|
['published_at', '100'],
|
||||||
|
['imeta', 'url https://example.com/photo.jpg', 'dim 800x600'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const photos = parsePlacePhotos(events);
|
||||||
|
|
||||||
|
assert.strictEqual(photos.length, 1);
|
||||||
|
assert.strictEqual(photos[0].publishedAt, 100);
|
||||||
|
assert.strictEqual(photos[0].createdAt, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parsePlacePhotos sets publishedAt to null when not present', function (assert) {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
id: 'event-1',
|
||||||
|
pubkey: 'pubkey-1',
|
||||||
|
created_at: 200,
|
||||||
|
tags: [
|
||||||
|
['i', 'osm:node:123'],
|
||||||
|
['imeta', 'url https://example.com/photo.jpg', 'dim 800x600'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const photos = parsePlacePhotos(events);
|
||||||
|
|
||||||
|
assert.strictEqual(photos.length, 1);
|
||||||
|
assert.strictEqual(photos[0].publishedAt, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parsePlacePhotos ignores invalid published_at values', function (assert) {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
id: 'event-1',
|
||||||
|
pubkey: 'pubkey-1',
|
||||||
|
created_at: 200,
|
||||||
|
tags: [
|
||||||
|
['i', 'osm:node:123'],
|
||||||
|
['published_at', 'not-a-number'],
|
||||||
|
['imeta', 'url https://example.com/photo.jpg', 'dim 800x600'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const photos = parsePlacePhotos(events);
|
||||||
|
|
||||||
|
assert.strictEqual(photos.length, 1);
|
||||||
|
assert.strictEqual(photos[0].publishedAt, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('uniqNormalizedRelays returns normalized unique relays', function (assert) {
|
test('uniqNormalizedRelays returns normalized unique relays', function (assert) {
|
||||||
const relays = uniqNormalizedRelays([
|
const relays = uniqNormalizedRelays([
|
||||||
'Relay.example.com',
|
'Relay.example.com',
|
||||||
|
|||||||
Reference in New Issue
Block a user