Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1d022b21bd
|
|||
|
3e831a7686
|
|||
|
2943125dbd
|
|||
|
a32ad7572b
|
|||
|
a1b3957c83
|
|||
|
9f2f233c22
|
|||
|
1ba4afdf08
|
|||
|
d764134513
|
|||
|
e38f540c79
|
|||
|
73ad5b4eb1
|
|||
|
b4a70233cf
|
|||
|
cb4b9c6b40
|
|||
|
98dcb4f25b
|
|||
|
7709634a9a
|
|||
|
3ddc85669f
|
|||
|
95961e680f
|
|||
|
9468a6a0cc
|
@@ -12,6 +12,7 @@ const stripProtocol = (url) => (url ? url.replace(/^wss?:\/\//, '') : '');
|
|||||||
export default class AppMenuSettingsNostr extends Component {
|
export default class AppMenuSettingsNostr extends Component {
|
||||||
@service settings;
|
@service settings;
|
||||||
@service nostrData;
|
@service nostrData;
|
||||||
|
@service toast;
|
||||||
|
|
||||||
@tracked newReadRelay = '';
|
@tracked newReadRelay = '';
|
||||||
@tracked newWriteRelay = '';
|
@tracked newWriteRelay = '';
|
||||||
@@ -90,6 +91,16 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
this.settings.update('nostrWriteRelays', null);
|
this.settings.update('nostrWriteRelays', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async clearCache() {
|
||||||
|
try {
|
||||||
|
await this.nostrData.clearCache();
|
||||||
|
this.toast.show('Nostr cache cleared');
|
||||||
|
} catch (e) {
|
||||||
|
this.toast.show(`Failed to clear Nostr cache: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{! template-lint-disable no-nested-interactive }}
|
{{! template-lint-disable no-nested-interactive }}
|
||||||
<details>
|
<details>
|
||||||
@@ -213,6 +224,18 @@ export default class AppMenuSettingsNostr extends Component {
|
|||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Cached data</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-full"
|
||||||
|
{{on "click" this.clearCache}}
|
||||||
|
>
|
||||||
|
<Icon @name="database" @size={{18}} @color="var(--danger-color)" />
|
||||||
|
Clear profiles, photos, and reviews
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default class Modal extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
close() {
|
close() {
|
||||||
|
if (this.args.disableClose) return;
|
||||||
if (this.args.onClose) {
|
if (this.args.onClose) {
|
||||||
this.args.onClose();
|
this.args.onClose();
|
||||||
}
|
}
|
||||||
@@ -31,10 +32,11 @@ export default class Modal extends Component {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="close-modal-btn btn-text"
|
class="close-modal-btn btn-text {{if @disableClose 'disabled'}}"
|
||||||
|
disabled={{@disableClose}}
|
||||||
{{on "click" this.close}}
|
{{on "click" this.close}}
|
||||||
>
|
>
|
||||||
<Icon @name="x" @size={{24}} />
|
<Icon @name="x" @size={{24}} @color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
{{yield}}
|
{{yield}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export default class PlaceDetails extends Component {
|
|||||||
@tracked isPhotoUploadModalOpen = false;
|
@tracked isPhotoUploadModalOpen = false;
|
||||||
@tracked isNostrConnectModalOpen = false;
|
@tracked isNostrConnectModalOpen = false;
|
||||||
@tracked newlyUploadedPhotoId = null;
|
@tracked newlyUploadedPhotoId = null;
|
||||||
|
@tracked isPhotoUploadActive = false;
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleUploadStateChange(isActive) {
|
||||||
|
this.isPhotoUploadActive = isActive;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
openPhotoUploadModal(e) {
|
openPhotoUploadModal(e) {
|
||||||
@@ -42,6 +48,7 @@ export default class PlaceDetails extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
closePhotoUploadModal(eventId) {
|
closePhotoUploadModal(eventId) {
|
||||||
|
if (this.isPhotoUploadActive) return;
|
||||||
this.isPhotoUploadModalOpen = false;
|
this.isPhotoUploadModalOpen = false;
|
||||||
if (typeof eventId === 'string') {
|
if (typeof eventId === 'string') {
|
||||||
this.newlyUploadedPhotoId = eventId;
|
this.newlyUploadedPhotoId = eventId;
|
||||||
@@ -569,7 +576,7 @@ export default class PlaceDetails extends Component {
|
|||||||
{{#if this.osmUrl}}
|
{{#if this.osmUrl}}
|
||||||
<div class="meta-info">
|
<div class="meta-info">
|
||||||
<p class="content-with-icon">
|
<p class="content-with-icon">
|
||||||
<Icon @name="camera" />
|
<Icon @name="feather-camera" />
|
||||||
<span>
|
<span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -585,10 +592,14 @@ export default class PlaceDetails extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if this.isPhotoUploadModalOpen}}
|
{{#if this.isPhotoUploadModalOpen}}
|
||||||
<Modal @onClose={{this.closePhotoUploadModal}}>
|
<Modal
|
||||||
|
@onClose={{this.closePhotoUploadModal}}
|
||||||
|
@disableClose={{this.isPhotoUploadActive}}
|
||||||
|
>
|
||||||
<PlacePhotoUpload
|
<PlacePhotoUpload
|
||||||
@place={{this.saveablePlace}}
|
@place={{this.saveablePlace}}
|
||||||
@onClose={{this.closePhotoUploadModal}}
|
@onClose={{this.closePhotoUploadModal}}
|
||||||
|
@onUploadStateChange={{this.handleUploadStateChange}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { isMobile } from '../utils/device';
|
|||||||
import Blurhash from './blurhash';
|
import Blurhash from './blurhash';
|
||||||
|
|
||||||
const MAX_IMAGE_DIMENSION = 1920;
|
const MAX_IMAGE_DIMENSION = 1920;
|
||||||
const IMAGE_QUALITY = 0.94;
|
const IMAGE_QUALITY = 0.9;
|
||||||
const MAX_THUMBNAIL_DIMENSION = 350;
|
const MAX_THUMBNAIL_DIMENSION = 350;
|
||||||
const THUMBNAIL_QUALITY = 0.9;
|
const THUMBNAIL_QUALITY = 0.9;
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ export default class PlacePhotoUploadItem extends Component {
|
|||||||
@tracked thumbnailUrl = '';
|
@tracked thumbnailUrl = '';
|
||||||
@tracked blurhash = '';
|
@tracked blurhash = '';
|
||||||
@tracked error = '';
|
@tracked error = '';
|
||||||
|
@tracked statusText = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
@@ -47,6 +48,7 @@ export default class PlacePhotoUploadItem extends Component {
|
|||||||
|
|
||||||
uploadTask = task(async (file) => {
|
uploadTask = task(async (file) => {
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
this.statusText = 'Processing';
|
||||||
try {
|
try {
|
||||||
// 1. Process main image and generate blurhash in worker
|
// 1. Process main image and generate blurhash in worker
|
||||||
const mainData = await this.imageProcessor.process(
|
const mainData = await this.imageProcessor.process(
|
||||||
@@ -71,18 +73,34 @@ export default class PlacePhotoUploadItem extends Component {
|
|||||||
let mainResult, thumbResult;
|
let mainResult, thumbResult;
|
||||||
const isMobileDevice = isMobile();
|
const isMobileDevice = isMobile();
|
||||||
|
|
||||||
|
const mainProgress = (status) => {
|
||||||
|
if (status === 'signing') this.statusText = 'Signing photo upload';
|
||||||
|
if (status === 'uploading') this.statusText = 'Uploading photo';
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbProgress = (status) => {
|
||||||
|
if (status === 'signing') this.statusText = 'Signing thumbnail upload';
|
||||||
|
if (status === 'uploading') this.statusText = 'Uploading thumbnail';
|
||||||
|
};
|
||||||
|
|
||||||
if (isMobileDevice) {
|
if (isMobileDevice) {
|
||||||
// Mobile: sequential uploads to preserve bandwidth and memory
|
// Mobile: sequential uploads to preserve bandwidth and memory
|
||||||
mainResult = await this.blossom.upload(mainData.blob, {
|
mainResult = await this.blossom.upload(mainData.blob, {
|
||||||
sequential: true,
|
sequential: true,
|
||||||
|
onProgress: mainProgress,
|
||||||
});
|
});
|
||||||
thumbResult = await this.blossom.upload(thumbData.blob, {
|
thumbResult = await this.blossom.upload(thumbData.blob, {
|
||||||
sequential: true,
|
sequential: true,
|
||||||
|
onProgress: thumbProgress,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Desktop: concurrent uploads
|
// Desktop: concurrent uploads
|
||||||
const mainUploadPromise = this.blossom.upload(mainData.blob);
|
const mainUploadPromise = this.blossom.upload(mainData.blob, {
|
||||||
const thumbUploadPromise = this.blossom.upload(thumbData.blob);
|
onProgress: mainProgress,
|
||||||
|
});
|
||||||
|
const thumbUploadPromise = this.blossom.upload(thumbData.blob, {
|
||||||
|
onProgress: thumbProgress,
|
||||||
|
});
|
||||||
|
|
||||||
[mainResult, thumbResult] = await Promise.all([
|
[mainResult, thumbResult] = await Promise.all([
|
||||||
mainUploadPromise,
|
mainUploadPromise,
|
||||||
@@ -127,6 +145,9 @@ export default class PlacePhotoUploadItem extends Component {
|
|||||||
@color="white"
|
@color="white"
|
||||||
class="spin-animation"
|
class="spin-animation"
|
||||||
/>
|
/>
|
||||||
|
{{#if this.statusText}}
|
||||||
|
<span class="upload-status-text">{{this.statusText}}</span>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
|
|
||||||
@tracked file = null;
|
@tracked file = null;
|
||||||
@tracked uploadedPhoto = null;
|
@tracked uploadedPhoto = null;
|
||||||
@tracked status = '';
|
|
||||||
@tracked error = '';
|
@tracked error = '';
|
||||||
@tracked isPublishing = false;
|
@tracked isPublishing = false;
|
||||||
@tracked isDragging = false;
|
@tracked isDragging = false;
|
||||||
@@ -77,6 +76,9 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
}
|
}
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.uploadedPhoto = null;
|
this.uploadedPhoto = null;
|
||||||
|
if (this.args.onUploadStateChange) {
|
||||||
|
this.args.onUploadStateChange(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -91,6 +93,9 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
}
|
}
|
||||||
this.file = null;
|
this.file = null;
|
||||||
this.uploadedPhoto = null;
|
this.uploadedPhoto = null;
|
||||||
|
if (this.args.onUploadStateChange) {
|
||||||
|
this.args.onUploadStateChange(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePhotoTask = task(async (photoData) => {
|
deletePhotoTask = task(async (photoData) => {
|
||||||
@@ -126,7 +131,6 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.status = 'Publishing event...';
|
|
||||||
this.error = '';
|
this.error = '';
|
||||||
this.isPublishing = true;
|
this.isPublishing = true;
|
||||||
|
|
||||||
@@ -185,18 +189,20 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
this.nostrData.store.add(event);
|
this.nostrData.store.add(event);
|
||||||
|
|
||||||
this.toast.show('Photo published successfully');
|
this.toast.show('Photo published successfully');
|
||||||
this.status = '';
|
|
||||||
|
|
||||||
// Clear out the file so user can upload more or be done
|
// Clear out the file so user can upload more or be done
|
||||||
this.file = null;
|
this.file = null;
|
||||||
this.uploadedPhoto = null;
|
this.uploadedPhoto = null;
|
||||||
|
|
||||||
|
if (this.args.onUploadStateChange) {
|
||||||
|
this.args.onUploadStateChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.args.onClose) {
|
if (this.args.onClose) {
|
||||||
this.args.onClose(event.id);
|
this.args.onClose(event.id);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = 'Failed to publish: ' + e.message;
|
this.error = 'Failed to publish: ' + e.message;
|
||||||
this.status = '';
|
|
||||||
} finally {
|
} finally {
|
||||||
this.isPublishing = false;
|
this.isPublishing = false;
|
||||||
}
|
}
|
||||||
@@ -212,12 +218,6 @@ export default class PlacePhotoUpload extends Component {
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.status}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
{{this.status}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.file}}
|
{{#if this.file}}
|
||||||
<div class="photo-grid">
|
<div class="photo-grid">
|
||||||
<PlacePhotoUploadItem
|
<PlacePhotoUploadItem
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ export default class PlacePhotosCarousel extends Component {
|
|||||||
{{#each this.photos as |photo|}}
|
{{#each this.photos as |photo|}}
|
||||||
{{! template-lint-disable no-inline-styles }}
|
{{! template-lint-disable no-inline-styles }}
|
||||||
<div
|
<div
|
||||||
class="carousel-slide"
|
class="carousel-slide
|
||||||
|
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||||
style={{photo.style}}
|
style={{photo.style}}
|
||||||
data-event-id={{photo.eventId}}
|
data-event-id={{photo.eventId}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -60,10 +60,13 @@ export default class BlossomService extends Service {
|
|||||||
return `Nostr ${base64url}`;
|
return `Nostr ${base64url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _uploadToServer(file, hash, serverUrl) {
|
async _uploadToServer(file, hash, serverUrl, onProgress) {
|
||||||
const uploadUrl = getBlossomUrl(serverUrl, 'upload');
|
const uploadUrl = getBlossomUrl(serverUrl, 'upload');
|
||||||
|
|
||||||
|
if (onProgress) onProgress('signing');
|
||||||
const authHeader = await this._getAuthHeader('upload', hash, serverUrl);
|
const authHeader = await this._getAuthHeader('upload', hash, serverUrl);
|
||||||
|
|
||||||
|
if (onProgress) onProgress('uploading');
|
||||||
// eslint-disable-next-line warp-drive/no-external-request-patterns
|
// eslint-disable-next-line warp-drive/no-external-request-patterns
|
||||||
const response = await fetch(uploadUrl, {
|
const response = await fetch(uploadUrl, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -109,14 +112,20 @@ export default class BlossomService extends Service {
|
|||||||
|
|
||||||
if (options.sequential) {
|
if (options.sequential) {
|
||||||
// Sequential upload logic
|
// Sequential upload logic
|
||||||
mainResult = await this._uploadToServer(file, payloadHash, mainServer);
|
mainResult = await this._uploadToServer(
|
||||||
|
file,
|
||||||
|
payloadHash,
|
||||||
|
mainServer,
|
||||||
|
options.onProgress
|
||||||
|
);
|
||||||
|
|
||||||
for (const serverUrl of fallbackServers) {
|
for (const serverUrl of fallbackServers) {
|
||||||
try {
|
try {
|
||||||
const result = await this._uploadToServer(
|
const result = await this._uploadToServer(
|
||||||
file,
|
file,
|
||||||
payloadHash,
|
payloadHash,
|
||||||
serverUrl
|
serverUrl,
|
||||||
|
options.onProgress
|
||||||
);
|
);
|
||||||
fallbackUrls.push(result.url);
|
fallbackUrls.push(result.url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -125,9 +134,14 @@ export default class BlossomService extends Service {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Concurrent upload logic
|
// Concurrent upload logic
|
||||||
const mainPromise = this._uploadToServer(file, payloadHash, mainServer);
|
const mainPromise = this._uploadToServer(
|
||||||
|
file,
|
||||||
|
payloadHash,
|
||||||
|
mainServer,
|
||||||
|
options.onProgress
|
||||||
|
);
|
||||||
const fallbackPromises = fallbackServers.map((serverUrl) =>
|
const fallbackPromises = fallbackServers.map((serverUrl) =>
|
||||||
this._uploadToServer(file, payloadHash, serverUrl)
|
this._uploadToServer(file, payloadHash, serverUrl, options.onProgress)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Main server MUST succeed
|
// Main server MUST succeed
|
||||||
|
|||||||
@@ -356,6 +356,13 @@ export default class NostrDataService extends Service {
|
|||||||
return 'Not connected';
|
return 'Not connected';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearCache() {
|
||||||
|
await this._cachePromise;
|
||||||
|
if (this.cache) {
|
||||||
|
await this.cache.deleteAllEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_cleanupSubscriptions() {
|
_cleanupSubscriptions() {
|
||||||
if (this._requestSub) {
|
if (this._requestSub) {
|
||||||
this._requestSub.unsubscribe();
|
this._requestSub.unsubscribe();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Service from '@ember/service';
|
|||||||
import { RelayPool } from 'applesauce-relay';
|
import { RelayPool } from 'applesauce-relay';
|
||||||
|
|
||||||
export default class NostrRelayService extends Service {
|
export default class NostrRelayService extends Service {
|
||||||
pool = new RelayPool();
|
pool = new RelayPool({ enablePing: true });
|
||||||
|
|
||||||
async publish(relays, event) {
|
async publish(relays, event) {
|
||||||
if (!relays || relays.length === 0) {
|
if (!relays || relays.length === 0) {
|
||||||
|
|||||||
@@ -285,10 +285,20 @@ body {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgb(0 0 0 / 60%);
|
background: rgb(0 0 0 / 60%);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-status-text {
|
||||||
|
color: white;
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-shadow: 0 1px 3px rgb(0 0 0 / 80%);
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.photo-upload-item .error-overlay {
|
.photo-upload-item .error-overlay {
|
||||||
background: rgb(224 108 117 / 80%);
|
background: rgb(224 108 117 / 80%);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1006,10 +1016,17 @@ abbr[title] {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
width: auto;
|
width: auto;
|
||||||
aspect-ratio: var(--slide-ratio, 16 / 9);
|
|
||||||
scroll-snap-align: none;
|
scroll-snap-align: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.carousel-slide.landscape {
|
||||||
|
aspect-ratio: var(--slide-ratio, 16 / 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-slide.portrait {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
.carousel-placeholder {
|
.carousel-placeholder {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: var(--hover-bg);
|
background-color: var(--hover-bg);
|
||||||
@@ -1798,6 +1815,12 @@ button.create-place {
|
|||||||
top: 1rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
color: #898989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal-btn.disabled {
|
||||||
|
color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-photo-upload h2 {
|
.place-photo-upload h2 {
|
||||||
@@ -1816,11 +1839,6 @@ button.create-place {
|
|||||||
color: #c00;
|
color: #c00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info {
|
|
||||||
background: #eef;
|
|
||||||
color: #00c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-group {
|
.preview-group {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
||||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||||
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||||
import camera from 'feather-icons/dist/icons/camera.svg?raw';
|
import featherCamera from 'feather-icons/dist/icons/camera.svg?raw';
|
||||||
import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
||||||
import chevronLeft from 'feather-icons/dist/icons/chevron-left.svg?raw';
|
import chevronLeft from 'feather-icons/dist/icons/chevron-left.svg?raw';
|
||||||
import chevronRight from 'feather-icons/dist/icons/chevron-right.svg?raw';
|
import chevronRight from 'feather-icons/dist/icons/chevron-right.svg?raw';
|
||||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||||
|
import database from 'feather-icons/dist/icons/database.svg?raw';
|
||||||
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
||||||
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
|
import facebook from 'feather-icons/dist/icons/facebook.svg?raw';
|
||||||
import gift from 'feather-icons/dist/icons/gift.svg?raw';
|
import gift from 'feather-icons/dist/icons/gift.svg?raw';
|
||||||
@@ -44,7 +45,9 @@ import badgeShieldWithFire from '@waysidemapping/pinhead/dist/icons/badge_shield
|
|||||||
import beachUmbrellaInGround from '@waysidemapping/pinhead/dist/icons/beach_umbrella_in_ground.svg?raw';
|
import beachUmbrellaInGround from '@waysidemapping/pinhead/dist/icons/beach_umbrella_in_ground.svg?raw';
|
||||||
import beerMugWithFoam from '@waysidemapping/pinhead/dist/icons/beer_mug_with_foam.svg?raw';
|
import beerMugWithFoam from '@waysidemapping/pinhead/dist/icons/beer_mug_with_foam.svg?raw';
|
||||||
import burgerAndDrinkCupWithStraw from '@waysidemapping/pinhead/dist/icons/burger_and_drink_cup_with_straw.svg?raw';
|
import burgerAndDrinkCupWithStraw from '@waysidemapping/pinhead/dist/icons/burger_and_drink_cup_with_straw.svg?raw';
|
||||||
|
import bridge from '@waysidemapping/pinhead/dist/icons/bridge.svg?raw';
|
||||||
import bus from '@waysidemapping/pinhead/dist/icons/bus.svg?raw';
|
import bus from '@waysidemapping/pinhead/dist/icons/bus.svg?raw';
|
||||||
|
import camera from '@waysidemapping/pinhead/dist/icons/camera.svg?raw';
|
||||||
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
|
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
|
||||||
import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw';
|
import car from '@waysidemapping/pinhead/dist/icons/car.svg?raw';
|
||||||
import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw';
|
import cigaretteWithSmokeCurl from '@waysidemapping/pinhead/dist/icons/cigarette_with_smoke_curl.svg?raw';
|
||||||
@@ -75,6 +78,7 @@ import gravestone from '@waysidemapping/pinhead/dist/icons/gravestone.svg?raw';
|
|||||||
import grecianVase from '@waysidemapping/pinhead/dist/icons/grecian_vase.svg?raw';
|
import grecianVase from '@waysidemapping/pinhead/dist/icons/grecian_vase.svg?raw';
|
||||||
import greekCross from '@waysidemapping/pinhead/dist/icons/greek_cross.svg?raw';
|
import greekCross from '@waysidemapping/pinhead/dist/icons/greek_cross.svg?raw';
|
||||||
import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
|
import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone.svg?raw';
|
||||||
|
import industrialBuilding from '@waysidemapping/pinhead/dist/icons/industrial_building.svg?raw';
|
||||||
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
|
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
|
||||||
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
|
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
|
||||||
import marketStall from '@waysidemapping/pinhead/dist/icons/market_stall.svg?raw';
|
import marketStall from '@waysidemapping/pinhead/dist/icons/market_stall.svg?raw';
|
||||||
@@ -102,6 +106,7 @@ import roundStructureWithFlag from '@waysidemapping/pinhead/dist/icons/round_str
|
|||||||
import sailingShipInWater from '@waysidemapping/pinhead/dist/icons/sailing_ship_in_water.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/scissors_open.svg?raw';
|
||||||
import shipwreckInWater from '@waysidemapping/pinhead/dist/icons/shipwreck_in_water.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';
|
import shoppingBag from '@waysidemapping/pinhead/dist/icons/shopping_bag.svg?raw';
|
||||||
import shoppingBasket from '@waysidemapping/pinhead/dist/icons/shopping_basket.svg?raw';
|
import shoppingBasket from '@waysidemapping/pinhead/dist/icons/shopping_basket.svg?raw';
|
||||||
import shoppingCart from '@waysidemapping/pinhead/dist/icons/shopping_cart.svg?raw';
|
import shoppingCart from '@waysidemapping/pinhead/dist/icons/shopping_cart.svg?raw';
|
||||||
@@ -131,8 +136,10 @@ const ICONS = {
|
|||||||
bookmark,
|
bookmark,
|
||||||
'boxing-glove-up': boxingGloveUp,
|
'boxing-glove-up': boxingGloveUp,
|
||||||
'burger-and-drink-cup-with-straw': burgerAndDrinkCupWithStraw,
|
'burger-and-drink-cup-with-straw': burgerAndDrinkCupWithStraw,
|
||||||
|
bridge,
|
||||||
bus,
|
bus,
|
||||||
camera,
|
camera,
|
||||||
|
'feather-camera': featherCamera,
|
||||||
'check-square': checkSquare,
|
'check-square': checkSquare,
|
||||||
'chevron-left': chevronLeft,
|
'chevron-left': chevronLeft,
|
||||||
'chevron-right': chevronRight,
|
'chevron-right': chevronRight,
|
||||||
@@ -153,6 +160,7 @@ const ICONS = {
|
|||||||
'comedy-mask-and-tragedy-mask': comedyMaskAndTragedyMask,
|
'comedy-mask-and-tragedy-mask': comedyMaskAndTragedyMask,
|
||||||
croissant,
|
croissant,
|
||||||
'cup-and-saucer': cupAndSaucer,
|
'cup-and-saucer': cupAndSaucer,
|
||||||
|
database,
|
||||||
donut,
|
donut,
|
||||||
edit,
|
edit,
|
||||||
eyeglasses,
|
eyeglasses,
|
||||||
@@ -174,6 +182,7 @@ const ICONS = {
|
|||||||
heart,
|
heart,
|
||||||
home,
|
home,
|
||||||
'ice-cream-on-cone': iceCreamOnCone,
|
'ice-cream-on-cone': iceCreamOnCone,
|
||||||
|
'industrial-building': industrialBuilding,
|
||||||
info,
|
info,
|
||||||
instagram,
|
instagram,
|
||||||
jewel,
|
jewel,
|
||||||
@@ -214,6 +223,7 @@ const ICONS = {
|
|||||||
'sailing-ship-in-water': sailingShipInWater,
|
'sailing-ship-in-water': sailingShipInWater,
|
||||||
'scissors-open': scissorsOpen,
|
'scissors-open': scissorsOpen,
|
||||||
'shipwreck-in-water': shipwreckInWater,
|
'shipwreck-in-water': shipwreckInWater,
|
||||||
|
'steam-train-on-railway-track': steamTrainOnRailwayTrack,
|
||||||
'shopping-bag': shoppingBag,
|
'shopping-bag': shoppingBag,
|
||||||
search,
|
search,
|
||||||
server,
|
server,
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export const POI_ICON_RULES = [
|
|||||||
{ tags: { amenity: 'arts_center' }, icon: 'comedy-mask-and-tragedy-mask' },
|
{ tags: { amenity: 'arts_center' }, icon: 'comedy-mask-and-tragedy-mask' },
|
||||||
|
|
||||||
// Historic
|
// Historic
|
||||||
|
{ tags: { historic: 'bridge' }, icon: 'bridge' },
|
||||||
{ tags: { historic: 'fort' }, icon: 'fort' },
|
{ tags: { historic: 'fort' }, icon: 'fort' },
|
||||||
{ tags: { historic: 'castle' }, icon: 'palace' },
|
{ tags: { historic: 'castle' }, icon: 'palace' },
|
||||||
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
|
{ tags: { historic: 'building' }, icon: 'classical-building-with-flag' },
|
||||||
@@ -119,6 +120,12 @@ export const POI_ICON_RULES = [
|
|||||||
tags: { historic: 'monument' },
|
tags: { historic: 'monument' },
|
||||||
icon: 'classical-building-with-dome-and-flag',
|
icon: 'classical-building-with-dome-and-flag',
|
||||||
},
|
},
|
||||||
|
{ tags: { historic: 'folly' }, icon: 'classical-building' },
|
||||||
|
{ tags: { historic: 'industrial' }, icon: 'industrial-building' },
|
||||||
|
{
|
||||||
|
tags: { historic: 'railway_station' },
|
||||||
|
icon: 'steam-train-on-railway-track',
|
||||||
|
},
|
||||||
{ tags: { historic: 'ship' }, icon: 'sailing-ship-in-water' },
|
{ tags: { historic: 'ship' }, icon: 'sailing-ship-in-water' },
|
||||||
{ tags: { historic: 'wreck' }, icon: 'shipwreck-in-water' },
|
{ tags: { historic: 'wreck' }, icon: 'shipwreck-in-water' },
|
||||||
{ tags: { historic: 'ruins' }, icon: 'camera' },
|
{ tags: { historic: 'ruins' }, icon: 'camera' },
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const POI_CATEGORIES = [
|
|||||||
{
|
{
|
||||||
id: 'things-to-do',
|
id: 'things-to-do',
|
||||||
label: 'Things to do',
|
label: 'Things to do',
|
||||||
icon: 'camera',
|
icon: 'feather-camera',
|
||||||
filter: [
|
filter: [
|
||||||
'["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork)$"]',
|
'["tourism"~"^(museum|gallery|attraction|viewpoint|zoo|theme_park|aquarium|artwork)$"]',
|
||||||
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
|
'["amenity"~"^(cinema|theatre|arts_centre|planetarium)$"]',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ While NIP-68 (Picture-first feeds) caters to general visual feeds, this NIP spec
|
|||||||
|
|
||||||
## Content
|
## Content
|
||||||
|
|
||||||
The `.content` of the event SHOULD generally be empty. If a user wishes to provide a detailed description, summary, or caption for a place, clients SHOULD encourage them to create a Place Review event (`kind: 30360`) instead.
|
The `.content` of the event SHOULD generally be empty. If a user wishes to provide a detailed description for a place, clients SHOULD encourage them to create a Place Review event (`kind: 30360`) instead.
|
||||||
|
|
||||||
## Tags
|
## Tags
|
||||||
|
|
||||||
@@ -45,17 +45,19 @@ Used for spatial indexing and discovery. Events MUST include at least one high-p
|
|||||||
|
|
||||||
#### 3. `imeta` — Inline Media Metadata
|
#### 3. `imeta` — Inline Media Metadata
|
||||||
|
|
||||||
An event MUST contain exactly one `imeta` tag representing a single media item. The primary `url` SHOULD also be appended to the event's `.content` for backwards compatibility with clients that do not parse `imeta` tags.
|
An event MUST contain exactly one `imeta` tag representing a single media item. The primary `url` MAY also be appended to the event's `.content` for backwards compatibility with clients that do not parse `imeta` tags.
|
||||||
|
|
||||||
Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `m` (MIME type), and `blurhash` where possible.
|
Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `m` (MIME type), `thumb` (URL to a smaller thumbnail image), and `blurhash` where possible. Clients MAY also include `fallback` URLs if the media is hosted on multiple servers.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
"imeta",
|
"imeta",
|
||||||
"url https://example.com/photo.jpg",
|
"url https://blossom.example.com/8e2e28a503fa37482de5b0959ee38b2bb4de4e0a752db24c568981c2ab410260.jpg",
|
||||||
"m image/jpeg",
|
"m image/jpeg",
|
||||||
"dim 3024x4032",
|
"dim 1440x1920",
|
||||||
"alt A steaming bowl of ramen on a wooden table at the restaurant.",
|
"alt A steaming bowl of ramen on a wooden table at the restaurant.",
|
||||||
|
"fallback https://mirror.example.com/8e2e28a503fa37482de5b0959ee38b2bb4de4e0a752db24c568981c2ab410260.jpg",
|
||||||
|
"thumb https://example.com/7a1f592f6ea8e932b1de9568285b01851e4cf708466b0a03010b91e92c6c8135.jpg",
|
||||||
"blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$"
|
"blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$"
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -83,10 +85,12 @@ Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `
|
|||||||
|
|
||||||
[
|
[
|
||||||
"imeta",
|
"imeta",
|
||||||
"url https://example.com/ramen.jpg",
|
"url https://blossom.example.com/a9c84e183789a74288b8e05d04cc61230e74f386925a953e6b29f957e8cc3a61.jpg",
|
||||||
"m image/jpeg",
|
"m image/jpeg",
|
||||||
"dim 1080x1080",
|
"dim 1920x1920",
|
||||||
"alt A close-up of spicy miso ramen with chashu pork, soft boiled egg, and scallions.",
|
"alt A close-up of spicy miso ramen with chashu pork, soft boiled egg, and scallions.",
|
||||||
|
"fallback https://mirror.example.com/a9c84e183789a74288b8e05d04cc61230e74f386925a953e6b29f957e8cc3a61.jpg",
|
||||||
|
"thumb https://example.com/c5a528e20235e16cc1c18090b8f04179de76288ea4e410b0fcb8d1487e416a2d.jpg",
|
||||||
"blurhash UHI=0o~q4T-o~q%MozM{x]t7RjRPt7oKkCWB"
|
"blurhash UHI=0o~q4T-o~q%MozM{x]t7RjRPt7oKkCWB"
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -98,6 +102,10 @@ Clients SHOULD include `alt` (accessibility descriptions), `dim` (dimensions), `
|
|||||||
|
|
||||||
## Rationale
|
## Rationale
|
||||||
|
|
||||||
|
### Kind 360
|
||||||
|
|
||||||
|
Easy to remember as a 360-degree view of places.
|
||||||
|
|
||||||
### Why not use NIP-68 (Picture-first feeds)?
|
### Why not use NIP-68 (Picture-first feeds)?
|
||||||
|
|
||||||
NIP-68 is designed for general-purpose social feeds (like Instagram). Place photos require strict guarantees about what entity is being depicted to be useful for map clients, directories, and review aggregators. By mandating the `i` tag for POI linking and the `g` tag for spatial querying, this kind ensures interoperability for geo-spatial applications without cluttering general picture feeds with mundane POI images (like photos of storefronts or menus).
|
NIP-68 is designed for general-purpose social feeds (like Instagram). Place photos require strict guarantees about what entity is being depicted to be useful for map clients, directories, and review aggregators. By mandating the `i` tag for POI linking and the `g` tag for spatial querying, this kind ensures interoperability for geo-spatial applications without cluttering general picture feeds with mundane POI images (like photos of storefronts or menus).
|
||||||
|
|||||||
@@ -276,6 +276,10 @@ Content payloads SHOULD NOT include place identifiers.
|
|||||||
|
|
||||||
## Rationale
|
## Rationale
|
||||||
|
|
||||||
|
### Kind 30360
|
||||||
|
|
||||||
|
Pairs with kind 360 (Place Photos). Easy to remember as a 360-degree review of all aspects of a place.
|
||||||
|
|
||||||
### No Place Field in Content
|
### No Place Field in Content
|
||||||
|
|
||||||
Avoids duplication and inconsistency with tags.
|
Avoids duplication and inconsistency with tags.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marco",
|
"name": "marco",
|
||||||
"version": "1.20.0",
|
"version": "1.20.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Unhosted maps app",
|
"description": "Unhosted maps app",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
release/assets/main-CHuW_yI-.css
Normal file
1
release/assets/main-CHuW_yI-.css
Normal file
File diff suppressed because one or more lines are too long
16
release/assets/main-Dhq0XoTm.js
Normal file
16
release/assets/main-Dhq0XoTm.js
Normal file
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
@@ -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-AsE4IKjj.js"></script>
|
<script type="module" crossorigin src="/assets/main-Dhq0XoTm.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-BA3LWr76.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-CHuW_yI-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user