Compare commits
3 Commits
03583e5a52
...
99d8ca9174
| Author | SHA1 | Date | |
|---|---|---|---|
|
99d8ca9174
|
|||
|
629a308b79
|
|||
|
798ed0c8dd
|
81
app/components/nostr-connect.gjs
Normal file
81
app/components/nostr-connect.gjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { on } from '@ember/modifier';
|
||||
import { eq } from 'ember-truth-helpers';
|
||||
|
||||
export default class NostrConnectComponent extends Component {
|
||||
@service nostrAuth;
|
||||
|
||||
get hasExtension() {
|
||||
return typeof window !== 'undefined' && typeof window.nostr !== 'undefined';
|
||||
}
|
||||
|
||||
@action
|
||||
async connectExtension() {
|
||||
try {
|
||||
await this.nostrAuth.login('extension');
|
||||
if (this.args.onConnect) {
|
||||
this.args.onConnect();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async connectApp() {
|
||||
try {
|
||||
await this.nostrAuth.login('connect');
|
||||
if (this.args.onConnect) {
|
||||
this.args.onConnect();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="nostr-connect-modal">
|
||||
<h2>Connect with Nostr</h2>
|
||||
|
||||
<div class="nostr-connect-options">
|
||||
{{#if this.hasExtension}}
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
{{on "click" this.connectExtension}}
|
||||
>
|
||||
Browser Extension (nos2x, Alby)
|
||||
</button>
|
||||
{{else}}
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
type="button"
|
||||
disabled
|
||||
title="No Nostr extension found in your browser."
|
||||
>
|
||||
Browser Extension (Not Found)
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
{{on "click" this.connectApp}}
|
||||
>
|
||||
Mobile Signer App (Amber, etc.)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{#if (eq this.nostrAuth.connectStatus "waiting")}}
|
||||
<div class="alert alert-info nostr-connect-status">
|
||||
<p>Waiting for you to approve the connection in your mobile signer
|
||||
app...</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import Icon from '../components/icon';
|
||||
import PlaceEditForm from './place-edit-form';
|
||||
import PlaceListsManager from './place-lists-manager';
|
||||
import PlacePhotoUpload from './place-photo-upload';
|
||||
import NostrConnect from './nostr-connect';
|
||||
import Modal from './modal';
|
||||
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
@@ -17,16 +18,22 @@ import { action } from '@ember/object';
|
||||
|
||||
export default class PlaceDetails extends Component {
|
||||
@service storage;
|
||||
@service nostrAuth;
|
||||
@tracked isEditing = false;
|
||||
@tracked showLists = false;
|
||||
@tracked isPhotoUploadModalOpen = false;
|
||||
@tracked isNostrConnectModalOpen = false;
|
||||
|
||||
@action
|
||||
openPhotoUploadModal(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
this.isPhotoUploadModalOpen = true;
|
||||
if (!this.nostrAuth.isConnected) {
|
||||
this.isNostrConnectModalOpen = true;
|
||||
} else {
|
||||
this.isPhotoUploadModalOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -34,6 +41,17 @@ export default class PlaceDetails extends Component {
|
||||
this.isPhotoUploadModalOpen = false;
|
||||
}
|
||||
|
||||
@action
|
||||
closeNostrConnectModal() {
|
||||
this.isNostrConnectModalOpen = false;
|
||||
}
|
||||
|
||||
@action
|
||||
onNostrConnected() {
|
||||
this.isNostrConnectModalOpen = false;
|
||||
this.isPhotoUploadModalOpen = true;
|
||||
}
|
||||
|
||||
get isSaved() {
|
||||
return this.storage.isPlaceSaved(this.place.id || this.place.osmId);
|
||||
}
|
||||
@@ -515,7 +533,10 @@ export default class PlaceDetails extends Component {
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.osmUrl}}
|
||||
</div>
|
||||
|
||||
{{#if this.osmUrl}}
|
||||
<div class="meta-info">
|
||||
<p class="content-with-icon">
|
||||
<Icon @name="camera" />
|
||||
<span>
|
||||
@@ -524,9 +545,8 @@ export default class PlaceDetails extends Component {
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.isPhotoUploadModalOpen}}
|
||||
@@ -534,5 +554,11 @@ export default class PlaceDetails extends Component {
|
||||
<PlacePhotoUpload @place={{this.saveablePlace}} />
|
||||
</Modal>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isNostrConnectModalOpen}}
|
||||
<Modal @onClose={{this.closeNostrConnectModal}}>
|
||||
<NostrConnect @onConnect={{this.onNostrConnected}} />
|
||||
</Modal>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -22,16 +22,6 @@ export default class PlacePhotoUpload extends Component {
|
||||
return this.place.title || 'this place';
|
||||
}
|
||||
|
||||
@action
|
||||
async login() {
|
||||
try {
|
||||
this.error = '';
|
||||
await this.nostrAuth.login();
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async uploadPhoto(event) {
|
||||
event.preventDefault();
|
||||
@@ -133,36 +123,25 @@ export default class PlacePhotoUpload extends Component {
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.nostrAuth.isConnected}}
|
||||
<div class="connected-status">
|
||||
<strong>Connected:</strong>
|
||||
{{this.nostrAuth.pubkey}}
|
||||
</div>
|
||||
|
||||
<form {{on "submit" this.uploadPhoto}}>
|
||||
{{#if this.photoUrl}}
|
||||
<div class="preview-group">
|
||||
<p>Photo Preview:</p>
|
||||
<img src={{this.photoUrl}} alt="Preview" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
{{on "click" this.publish}}
|
||||
>
|
||||
Publish Event (kind: 360)
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
Mock Upload Photo
|
||||
</button>
|
||||
{{/if}}
|
||||
</form>
|
||||
{{else}}
|
||||
<button type="button" class="btn btn-primary" {{on "click" this.login}}>
|
||||
Connect Nostr Extension
|
||||
</button>
|
||||
{{/if}}
|
||||
<form {{on "submit" this.uploadPhoto}}>
|
||||
{{#if this.photoUrl}}
|
||||
<div class="preview-group">
|
||||
<p>Photo Preview:</p>
|
||||
<img src={{this.photoUrl}} alt="Preview" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
{{on "click" this.publish}}
|
||||
>
|
||||
Publish Event (kind: 360)
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
Mock Upload Photo
|
||||
</button>
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Icon from '#components/icon';
|
||||
import { on } from '@ember/modifier';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import Modal from './modal';
|
||||
import NostrConnect from './nostr-connect';
|
||||
|
||||
export default class UserMenuComponent extends Component {
|
||||
@service storage;
|
||||
@service osmAuth;
|
||||
|
||||
@service nostrAuth;
|
||||
|
||||
@tracked isNostrConnectModalOpen = false;
|
||||
|
||||
@action
|
||||
connectRS() {
|
||||
this.args.onClose();
|
||||
@@ -33,13 +37,13 @@ export default class UserMenuComponent extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
async connectNostr() {
|
||||
try {
|
||||
await this.nostrAuth.login();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e.message);
|
||||
}
|
||||
openNostrConnectModal() {
|
||||
this.isNostrConnectModalOpen = true;
|
||||
}
|
||||
|
||||
@action
|
||||
closeNostrConnectModal() {
|
||||
this.isNostrConnectModalOpen = false;
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -124,7 +128,7 @@ export default class UserMenuComponent extends Component {
|
||||
<button
|
||||
class="btn-text text-primary"
|
||||
type="button"
|
||||
{{on "click" this.connectNostr}}
|
||||
{{on "click" this.openNostrConnectModal}}
|
||||
>Connect</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -140,5 +144,11 @@ export default class UserMenuComponent extends Component {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{#if this.isNostrConnectModalOpen}}
|
||||
<Modal @onClose={{this.closeNostrConnectModal}}>
|
||||
<NostrConnect @onConnect={{this.closeNostrConnectModal}} />
|
||||
</Modal>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -1,64 +1,224 @@
|
||||
import Service from '@ember/service';
|
||||
import Service, { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { ExtensionSigner } from 'applesauce-signers';
|
||||
import {
|
||||
ExtensionSigner,
|
||||
NostrConnectSigner,
|
||||
PrivateKeySigner,
|
||||
} from 'applesauce-signers';
|
||||
|
||||
const STORAGE_KEY = 'marco:nostr_pubkey';
|
||||
const STORAGE_KEY_TYPE = 'marco:nostr_signer_type'; // 'extension' | 'connect'
|
||||
const STORAGE_KEY_CONNECT_LOCAL_KEY = 'marco:nostr_connect_local_key';
|
||||
const STORAGE_KEY_CONNECT_REMOTE_PUBKEY = 'marco:nostr_connect_remote_pubkey';
|
||||
const STORAGE_KEY_CONNECT_RELAY = 'marco:nostr_connect_relay';
|
||||
|
||||
export default class NostrAuthService extends Service {
|
||||
@service nostrRelay;
|
||||
|
||||
@tracked pubkey = null;
|
||||
@tracked signerType = null; // 'extension' or 'connect'
|
||||
|
||||
// Track NostrConnect state for the UI
|
||||
@tracked connectStatus = null; // null | 'waiting' | 'connected'
|
||||
@tracked connectUri = null; // For displaying a QR code if needed
|
||||
|
||||
_signerInstance = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
const type = localStorage.getItem(STORAGE_KEY_TYPE);
|
||||
if (saved) {
|
||||
this.pubkey = saved;
|
||||
this.signerType = type || 'extension';
|
||||
this._verifyPubkey();
|
||||
}
|
||||
}
|
||||
|
||||
async _verifyPubkey() {
|
||||
if (typeof window.nostr === 'undefined') {
|
||||
this.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const signer = new ExtensionSigner();
|
||||
const extensionPubkey = await signer.getPublicKey();
|
||||
|
||||
if (extensionPubkey !== this.pubkey) {
|
||||
this.pubkey = extensionPubkey;
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
if (this.signerType === 'extension') {
|
||||
if (typeof window.nostr === 'undefined') {
|
||||
this.logout();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const signer = new ExtensionSigner();
|
||||
const extensionPubkey = await signer.getPublicKey();
|
||||
if (extensionPubkey !== this.pubkey) {
|
||||
this.pubkey = extensionPubkey;
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to verify extension nostr pubkey, logging out', e);
|
||||
this.logout();
|
||||
}
|
||||
} else if (this.signerType === 'connect') {
|
||||
try {
|
||||
await this._initConnectSigner();
|
||||
} catch (e) {
|
||||
console.warn('Failed to verify connect nostr pubkey, logging out', e);
|
||||
this.logout();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to verify nostr pubkey, logging out', e);
|
||||
this.logout();
|
||||
}
|
||||
}
|
||||
|
||||
get isConnected() {
|
||||
return !!this.pubkey;
|
||||
return (
|
||||
!!this.pubkey &&
|
||||
(this.signerType === 'extension'
|
||||
? typeof window.nostr !== 'undefined'
|
||||
: true)
|
||||
);
|
||||
}
|
||||
|
||||
get signer() {
|
||||
if (typeof window.nostr !== 'undefined') {
|
||||
if (this._signerInstance) return this._signerInstance;
|
||||
|
||||
if (
|
||||
this.signerType === 'extension' &&
|
||||
typeof window.nostr !== 'undefined'
|
||||
) {
|
||||
return new ExtensionSigner();
|
||||
}
|
||||
|
||||
if (this.signerType === 'connect') {
|
||||
// Must be initialized async due to the connect handshakes
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async login() {
|
||||
if (typeof window.nostr === 'undefined') {
|
||||
throw new Error('No NIP-07 Nostr extension found (e.g., Alby, nos2x).');
|
||||
async login(type = 'extension') {
|
||||
if (type === 'extension') {
|
||||
if (typeof window.nostr === 'undefined') {
|
||||
throw new Error('No NIP-07 Nostr extension found (e.g., Alby, nos2x).');
|
||||
}
|
||||
|
||||
try {
|
||||
this._signerInstance = new ExtensionSigner();
|
||||
this.pubkey = await this._signerInstance.getPublicKey();
|
||||
this.signerType = 'extension';
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
localStorage.setItem(STORAGE_KEY_TYPE, 'extension');
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
console.error('Failed to get public key from extension:', error);
|
||||
throw error;
|
||||
}
|
||||
} else if (type === 'connect') {
|
||||
this.connectStatus = 'waiting';
|
||||
|
||||
try {
|
||||
// Generate or retrieve a local ephemeral keypair
|
||||
let localKeyHex = localStorage.getItem(STORAGE_KEY_CONNECT_LOCAL_KEY);
|
||||
let localSigner;
|
||||
if (localKeyHex) {
|
||||
localSigner = PrivateKeySigner.fromKey(localKeyHex);
|
||||
} else {
|
||||
localSigner = new PrivateKeySigner();
|
||||
// Store the raw Uint8Array as hex string
|
||||
localKeyHex = Array.from(localSigner.key)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
localStorage.setItem(STORAGE_KEY_CONNECT_LOCAL_KEY, localKeyHex);
|
||||
}
|
||||
|
||||
// We use a specific relay for the connection handshake.
|
||||
const relay = 'wss://relay.nsec.app';
|
||||
localStorage.setItem(STORAGE_KEY_CONNECT_RELAY, relay);
|
||||
|
||||
this._signerInstance = new NostrConnectSigner({
|
||||
pool: this.nostrRelay.pool,
|
||||
relays: [relay],
|
||||
signer: localSigner,
|
||||
onAuth: async (url) => {
|
||||
// NIP-46 auth callback. Normally the signer app does this natively via notification.
|
||||
// But if it requires an explicit browser window:
|
||||
if (
|
||||
confirm(
|
||||
`Your signer app requests authentication via a web page. Open it now?\n\nURL: ${url}`
|
||||
)
|
||||
) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Set the uri for display (e.g., to redirect via intent)
|
||||
this.connectUri = this._signerInstance.getNostrConnectURI({
|
||||
name: 'Marco',
|
||||
url: window.location.origin,
|
||||
description: 'A privacy-respecting maps application.',
|
||||
icons: [],
|
||||
});
|
||||
|
||||
// Trigger the deep link intent immediately for the user
|
||||
window.location.href = this.connectUri;
|
||||
|
||||
// Start listening to the relay
|
||||
await this._signerInstance.open();
|
||||
|
||||
// Wait for the remote signer to reply with their pubkey
|
||||
await this._signerInstance.waitForSigner();
|
||||
|
||||
// Once connected, get the actual user pubkey
|
||||
this.pubkey = await this._signerInstance.getPublicKey();
|
||||
this.signerType = 'connect';
|
||||
this.connectStatus = 'connected';
|
||||
|
||||
// Save connection state
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
localStorage.setItem(STORAGE_KEY_TYPE, 'connect');
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY_CONNECT_REMOTE_PUBKEY,
|
||||
this._signerInstance.remote
|
||||
);
|
||||
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
this.connectStatus = null;
|
||||
console.error('Failed to connect via Nostr Connect:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _initConnectSigner() {
|
||||
const localKeyHex = localStorage.getItem(STORAGE_KEY_CONNECT_LOCAL_KEY);
|
||||
const remotePubkey = localStorage.getItem(
|
||||
STORAGE_KEY_CONNECT_REMOTE_PUBKEY
|
||||
);
|
||||
const relay =
|
||||
localStorage.getItem(STORAGE_KEY_CONNECT_RELAY) || 'wss://relay.nsec.app';
|
||||
|
||||
if (!localKeyHex || !remotePubkey) {
|
||||
throw new Error('Missing Nostr Connect local state.');
|
||||
}
|
||||
|
||||
try {
|
||||
this.pubkey = await this.signer.getPublicKey();
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
console.error('Failed to get public key from extension:', error);
|
||||
throw error;
|
||||
const localSigner = PrivateKeySigner.fromKey(localKeyHex);
|
||||
|
||||
this._signerInstance = new NostrConnectSigner({
|
||||
pool: this.nostrRelay.pool,
|
||||
relays: [relay],
|
||||
signer: localSigner,
|
||||
remote: remotePubkey,
|
||||
onAuth: async (url) => {
|
||||
if (
|
||||
confirm(
|
||||
`Your signer app requests authentication via a web page. Open it now?\n\nURL: ${url}`
|
||||
)
|
||||
) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await this._signerInstance.open();
|
||||
// Validate we can still get the pubkey from the remote signer
|
||||
const pubkey = await this._signerInstance.getPublicKey();
|
||||
if (pubkey !== this.pubkey) {
|
||||
throw new Error('Remote signer pubkey mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +231,23 @@ export default class NostrAuthService extends Service {
|
||||
return await this.signer.signEvent(event);
|
||||
}
|
||||
|
||||
logout() {
|
||||
async logout() {
|
||||
this.pubkey = null;
|
||||
this.signerType = null;
|
||||
this.connectStatus = null;
|
||||
this.connectUri = null;
|
||||
if (
|
||||
this._signerInstance &&
|
||||
typeof this._signerInstance.close === 'function'
|
||||
) {
|
||||
await this._signerInstance.close();
|
||||
}
|
||||
this._signerInstance = null;
|
||||
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(STORAGE_KEY_TYPE);
|
||||
localStorage.removeItem(STORAGE_KEY_CONNECT_LOCAL_KEY);
|
||||
localStorage.removeItem(STORAGE_KEY_CONNECT_REMOTE_PUBKEY);
|
||||
localStorage.removeItem(STORAGE_KEY_CONNECT_RELAY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,6 +1378,22 @@ button.create-place {
|
||||
}
|
||||
}
|
||||
|
||||
/* Nostr Connect */
|
||||
.nostr-connect-modal h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.nostr-connect-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.nostr-connect-status {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import activity from 'feather-icons/dist/icons/activity.svg?raw';
|
||||
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
|
||||
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
|
||||
import camera from 'feather-icons/dist/icons/camera.svg?raw';
|
||||
import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
|
||||
import clock from 'feather-icons/dist/icons/clock.svg?raw';
|
||||
import edit from 'feather-icons/dist/icons/edit.svg?raw';
|
||||
@@ -39,7 +40,6 @@ import beerMugWithFoam from '@waysidemapping/pinhead/dist/icons/beer_mug_with_fo
|
||||
import burgerAndDrinkCupWithStraw from '@waysidemapping/pinhead/dist/icons/burger_and_drink_cup_with_straw.svg?raw';
|
||||
import bus from '@waysidemapping/pinhead/dist/icons/bus.svg?raw';
|
||||
import boxingGloveUp from '@waysidemapping/pinhead/dist/icons/boxing_glove_up.svg?raw';
|
||||
import camera from '@waysidemapping/pinhead/dist/icons/camera.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 classicalBuilding from '@waysidemapping/pinhead/dist/icons/classical_building.svg?raw';
|
||||
@@ -235,7 +235,6 @@ const FILLED_ICONS = [
|
||||
'cup-and-saucer',
|
||||
'coffee-bean',
|
||||
'shopping-basket',
|
||||
'camera',
|
||||
'person-sleeping-in-bed',
|
||||
'loading-ring',
|
||||
'nostrich',
|
||||
|
||||
Reference in New Issue
Block a user