Render data and uploader name for current photo in gallery
This commit is contained in:
@@ -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 = <template>
|
||||
<div
|
||||
@@ -25,26 +27,41 @@ const GalleryContent = <template>
|
||||
class="photo-gallery-content"
|
||||
data-current-event-id={{@currentPhoto.eventId}}
|
||||
>
|
||||
<div class="actions-btn-container">
|
||||
<DropdownMenu
|
||||
@iconSize={{24}}
|
||||
@triggerIcon="more-horizontal"
|
||||
@iconColor="white"
|
||||
as |closeMenu|
|
||||
>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
type="button"
|
||||
{{on "click" (fn @copyEventId closeMenu)}}
|
||||
>Copy Photo Event ID</button>
|
||||
{{#if @canDeletePhoto}}
|
||||
<div class="photo-gallery-header">
|
||||
<div class="actions-btn-container">
|
||||
<DropdownMenu
|
||||
@iconSize={{24}}
|
||||
@triggerIcon={{@triggerIcon}}
|
||||
@iconColor="white"
|
||||
as |closeMenu|
|
||||
>
|
||||
<button
|
||||
class="dropdown-item text-danger"
|
||||
class="dropdown-item"
|
||||
type="button"
|
||||
{{on "click" (fn @deletePhotoTask.perform closeMenu)}}
|
||||
>Delete Photo</button>
|
||||
{{/if}}
|
||||
</DropdownMenu>
|
||||
{{on "click" (fn @copyEventId closeMenu)}}
|
||||
>Copy Photo Event ID</button>
|
||||
{{#if @canDeletePhoto}}
|
||||
<button
|
||||
class="dropdown-item text-danger"
|
||||
type="button"
|
||||
{{on "click" (fn @deletePhotoTask.perform closeMenu)}}
|
||||
>Delete Photo</button>
|
||||
{{/if}}
|
||||
</DropdownMenu>
|
||||
</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
|
||||
@@ -98,6 +115,13 @@ export default class PhotoGallery extends Component {
|
||||
|
||||
@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() {
|
||||
return (
|
||||
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]) => {
|
||||
document.addEventListener('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('.carousel-nav-btn') ||
|
||||
e.target.closest('.close-btn') ||
|
||||
e.target.closest('.actions-btn-container')
|
||||
e.target.closest('.photo-gallery-header')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -268,6 +305,9 @@ export default class PhotoGallery extends Component {
|
||||
@handleVisiblePhotoChange={{this.handleVisiblePhotoChange}}
|
||||
@placeName={{@placeName}}
|
||||
@selectPhoto={{this.selectPhoto}}
|
||||
@uploaderName={{this.uploaderName}}
|
||||
@photoDate={{this.photoDate}}
|
||||
@triggerIcon={{this.triggerIcon}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#in-element this.destinationElement}}
|
||||
@@ -284,6 +324,9 @@ export default class PhotoGallery extends Component {
|
||||
@handleVisiblePhotoChange={{this.handleVisiblePhotoChange}}
|
||||
@placeName={{@placeName}}
|
||||
@selectPhoto={{this.selectPhoto}}
|
||||
@uploaderName={{this.uploaderName}}
|
||||
@photoDate={{this.photoDate}}
|
||||
@triggerIcon={{this.triggerIcon}}
|
||||
/>
|
||||
{{/in-element}}
|
||||
{{/if}}
|
||||
|
||||
+32
-14
@@ -1,6 +1,6 @@
|
||||
import Service, { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { from } from 'rxjs';
|
||||
import { EMPTY, from } from 'rxjs';
|
||||
import { EventStore } from 'applesauce-core/event-store';
|
||||
import { ProfileModel } from 'applesauce-core/models/profile';
|
||||
import { MailboxesModel } from 'applesauce-core/models/mailboxes';
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
|
||||
|
||||
const DIRECTORY_RELAYS = [
|
||||
'wss://purplepag.es',
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.primal.net',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.damus.io',
|
||||
];
|
||||
|
||||
const DEFAULT_READ_RELAYS = ['wss://nostr.kosmos.org'];
|
||||
@@ -48,11 +48,25 @@ export default class NostrDataService extends Service {
|
||||
|
||||
_requestSub = null;
|
||||
_cachePromise = null;
|
||||
_currentPlaceEntityId = null;
|
||||
loadedGeohashPrefixes = new Set();
|
||||
|
||||
constructor() {
|
||||
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
|
||||
this._cachePromise = openDB('applesauce-events').then(async (db) => {
|
||||
this.cache = new NostrIDB(db, {
|
||||
@@ -85,13 +99,6 @@ 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
|
||||
@@ -223,6 +230,18 @@ export default class NostrDataService extends Service {
|
||||
}
|
||||
|
||||
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) {
|
||||
this._photosSub.unsubscribe();
|
||||
this._photosSub = null;
|
||||
@@ -230,13 +249,12 @@ export default class NostrDataService extends Service {
|
||||
|
||||
this.placePhotos = [];
|
||||
this._clearProfileSubs();
|
||||
this._currentPlaceEntityId = entityId;
|
||||
|
||||
if (!place || !place.osmId || !place.osmType) {
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityId = `osm:${place.osmType}:${place.osmId}`;
|
||||
|
||||
// Setup reactive store query
|
||||
this._photosSub = this.store
|
||||
.timeline([
|
||||
@@ -507,12 +525,12 @@ export default class NostrDataService extends Service {
|
||||
this._contributionsSub.unsubscribe();
|
||||
this._contributionsSub = null;
|
||||
}
|
||||
this._clearProfileSubs();
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this._cleanupSubscriptions();
|
||||
this._clearProfileSubs();
|
||||
|
||||
if (this._stopPersisting) {
|
||||
this._stopPersisting();
|
||||
|
||||
+62
-6
@@ -2151,17 +2151,73 @@ button.create-place {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Actions button in photo gallery */
|
||||
.photo-gallery-overlay .actions-btn-container {
|
||||
/* Photo gallery header (actions button + uploader info) */
|
||||
.photo-gallery-overlay .photo-gallery-header {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
left: 1rem;
|
||||
z-index: 10;
|
||||
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;
|
||||
justify-content: 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;
|
||||
}
|
||||
|
||||
.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) */
|
||||
|
||||
@@ -94,11 +94,14 @@ function parsePhotoFromEvent(event) {
|
||||
if (!url) return null;
|
||||
|
||||
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 {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
publishedAt: publishedAt && publishedAt > 0 ? publishedAt : null,
|
||||
url,
|
||||
thumbUrl,
|
||||
blurhash,
|
||||
|
||||
@@ -69,6 +69,10 @@ export function parsePlacePhotos(events) {
|
||||
let aspectRatio = 16 / 9; // default
|
||||
let altText = null;
|
||||
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)) {
|
||||
if (tag.startsWith('url ')) {
|
||||
@@ -98,6 +102,7 @@ export function parsePlacePhotos(events) {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
publishedAt: publishedAt && publishedAt > 0 ? publishedAt : null,
|
||||
url,
|
||||
thumbUrl,
|
||||
blurhash,
|
||||
|
||||
Reference in New Issue
Block a user