From c7254b16ee435e19d5102d366b935a3923f34bbe Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 14:45:45 +0000 Subject: [PATCH 01/58] initial files --- templates/nostrmarket/market.html | 202 ++++++++++++++++++++++++++++++ templates/nostrmarket/stall.html | 61 +++++++++ views.py | 18 +++ 3 files changed, 281 insertions(+) create mode 100644 templates/nostrmarket/market.html create mode 100644 templates/nostrmarket/stall.html diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html new file mode 100644 index 0000000..ef29755 --- /dev/null +++ b/templates/nostrmarket/market.html @@ -0,0 +1,202 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% raw %} + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ getAmountFormated(item.price, item.currency) }} + ({{ getValueInSats(item.price, item.currency) }} sats) + + {{item.quantity}} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} + + Visit Stall + + + {% endraw %} +
+
+
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/templates/nostrmarket/stall.html b/templates/nostrmarket/stall.html new file mode 100644 index 0000000..208938c --- /dev/null +++ b/templates/nostrmarket/stall.html @@ -0,0 +1,61 @@ +{% extends "public.html" %} {% block page %} +
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py index ca8e1f7..47b07ff 100644 --- a/views.py +++ b/views.py @@ -20,3 +20,21 @@ async def index(request: Request, user: User = Depends(check_user_exists)): "nostrmarket/index.html", {"request": request, "user": user.dict()}, ) + + +@nostrmarket_ext.get("/market", response_class=HTMLResponse) +async def market(request: Request): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/market.html", + { + "request": request, + }, + ) + + +@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) +async def stall(request: Request, stall_id: str): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/stall.html", + {"request": request, "stall_id": stall_id}, + ) From 7d273248a55b93e9feaee2059c0cb9de27b68117 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 16:00:57 +0000 Subject: [PATCH 02/58] get nostr events and display --- templates/nostrmarket/market.html | 299 ++++++++++++++++++------------ 1 file changed, 182 insertions(+), 117 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index ef29755..eb18f39 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,113 +1,162 @@ {% extends "public.html" %} {% block page %} -
-
- -
- Market: -
-
- - - -
+ + + + Settings -
-
-
-
- - {% raw %} - - - -
-
- {{ item.product }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ getAmountFormated(item.price, item.currency) }} - ({{ getValueInSats(item.price, item.currency) }} sats) - - {{item.quantity}} left -
-
- {{cat}} -
-
+ + -

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} - + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + +
+ + - Visit Stall - - - {% endraw %} - -
-
+ + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + + + + + + + +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% raw %} + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ getAmountFormated(item.price, item.currency) }} + ({{ getValueInSats(item.price, item.currency) }} sats) + + {{item.quantity}} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} + + Visit Stall + + + {% endraw %} +
+
+
+
+ + + + {% endblock %} {% block scripts %} From eac1a77781a40afd42f4c8d96657073f5b71c246 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 17:06:24 +0000 Subject: [PATCH 03/58] menu add pubkey/npub --- templates/nostrmarket/market.html | 120 +++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index eb18f39..33af581 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -14,10 +14,44 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + + + + + + {%raw%} + + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }} + + + + + {%endraw%} + + @@ -29,10 +63,35 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + {%raw%} + + {{ url }} + + + + + {%endraw%} + + @@ -190,11 +249,14 @@ return { drawer: true, pubkeys: new Set(), + relays: new Set(defaultRelays), stalls: [], products: [], events: [], searchText: null, - exchangeRates: null + exchangeRates: null, + inputPubkey: null, + inputRelay: null } }, computed: { @@ -220,7 +282,7 @@ async initNostr() { this.pool = new nostr.SimplePool() this.relays = new Set(defaultRelays) - await this.pool + let sub = await this.pool .list(Array.from(this.relays), [ { kinds: [30005], @@ -236,11 +298,12 @@ } else { // it's a stall this.stalls.push(e.content) + return } }) - console.log(this.stalls) - console.log(this.products) }) + await Promise.resolve(sub) + this.pool.close() }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -260,8 +323,41 @@ }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) + }, + addPubkey() { + let pubkey = String(this.inputPubkey).trim() + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (regExp.test(pubkey)) { + return this.pubkeys.add(pubkey) + } + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + }, + addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.inputRelay = null } } }) + {% endblock %} From 19001745036dbf3cd92fb4b6fe05e12a3e3d01d7 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 21:04:29 +0000 Subject: [PATCH 04/58] remove pubkeys and relays --- templates/nostrmarket/market.html | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 33af581..103d7db 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,6 +1,6 @@ {% extends "public.html" %} {% block page %} - - + + Settings @@ -47,6 +47,7 @@ dense round icon="delete" + @click="removePubkey(pub)" /> {%endraw%} @@ -87,6 +88,7 @@ dense round icon="delete" + @click="removeRelay(url)" /> {%endraw%} @@ -212,7 +214,7 @@ - + @@ -247,9 +249,9 @@ mixins: [windowMixin], data: function () { return { - drawer: true, + drawer: false, pubkeys: new Set(), - relays: new Set(defaultRelays), + relays: new Set(), stalls: [], products: [], events: [], @@ -269,6 +271,9 @@ p.categories.includes(this.searchText) ) }) + }, + relayList() { + return Array.from(this.relays) } }, async created() { @@ -276,12 +281,12 @@ this.pubkeys.add( '855ea22a88d7df7ccd8497777db81f115575d5362f51df3af02ead383f5eaba2' ) - //await this.initNostr() + this.relays = new Set(defaultRelays) + await this.initNostr() }, methods: { async initNostr() { this.pool = new nostr.SimplePool() - this.relays = new Set(defaultRelays) let sub = await this.pool .list(Array.from(this.relays), [ { @@ -343,6 +348,12 @@ console.error(err) } }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + }, addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { @@ -351,13 +362,14 @@ } this.relays.add(relay) this.inputRelay = null + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) } } }) - {% endblock %} From 63c7476e6f7723c2c31e28ee9661c5174273b30b Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:38:50 +0000 Subject: [PATCH 05/58] fetch profiles --- templates/nostrmarket/market.html | 104 +++++++++++++++++++----------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 103d7db..a85f2fc 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -24,20 +24,31 @@ > - + + {%raw%} - + + - {%raw%} {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }}{{ profiles.get(pub).name }} + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` + }} + {{ pub }} {% raw %} { this.events = events || [] this.events.map(eventToObj).map(e => { - if (e.content.stall) { - //it's a product - this.products.push(e.content) + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.content.stall) { + //it's a product `d` is the prod. id + products.set(e.d, e.content) } else { - // it's a stall - this.stalls.push(e.content) + // it's a stall `d` is the stall id + stalls.set(e.d, e.content) return } }) }) await Promise.resolve(sub) - this.pool.close() + this.products = Array.from(products.values()) + this.stalls = Array.from(stalls.values()) + pool.close(relays) }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -329,32 +352,36 @@ getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - addPubkey() { + async addPubkey() { let pubkey = String(this.inputPubkey).trim() let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (regExp.test(pubkey)) { - return this.pubkeys.add(pubkey) - } - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) + } else if (regExp.test(pubkey)) { + pubkey = pubkey } + this.pubkeys.add(pubkey) + await this.initNostr() }, removePubkey(pubkey) { // Needs a hack for Vue reactivity let pubkeys = this.pubkeys pubkeys.delete(pubkey) + this.profiles.delete(pubkey) this.pubkeys = new Set(Array.from(pubkeys)) }, - addRelay() { + async addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { console.debug('invalid url') @@ -362,6 +389,7 @@ } this.relays.add(relay) this.inputRelay = null + await this.initNostr() }, removeRelay(relay) { // Needs a hack for Vue reactivity From b0eca2b1db5be38523c9ed02550e10d0464ab915 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:39:05 +0000 Subject: [PATCH 06/58] avatar picture --- static/images/blank-avatar.webp | Bin 0 -> 11090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/blank-avatar.webp diff --git a/static/images/blank-avatar.webp b/static/images/blank-avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..513b0f3279d36569ead8f6553506c30fbb610703 GIT binary patch literal 11090 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 ASO5S3 literal 0 HcmV?d00001 From fd4a07d29c4f3384883a0ae7493e409d6d8e8daf Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 1 Mar 2023 16:59:59 +0000 Subject: [PATCH 07/58] market component (ready until final product object) --- .../customer-market/customer-market.html | 83 ++++++++ .../customer-market/customer-market.js | 17 ++ templates/nostrmarket/market.html | 180 +++++++----------- views.py | 10 +- 4 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 static/components/customer-market/customer-market.html create mode 100644 static/components/customer-market/customer-market.js diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html new file mode 100644 index 0000000..99af972 --- /dev/null +++ b/static/components/customer-market/customer-market.html @@ -0,0 +1,83 @@ +
+
+ + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ item.formatedPrice }} + ({{ item.priceInSats }} sats) + + {{ item.amount }} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} +
+ See product + + Visit Stall + +
+
+
+
+
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js new file mode 100644 index 0000000..5844105 --- /dev/null +++ b/static/components/customer-market/customer-market.js @@ -0,0 +1,17 @@ +async function customerMarket(path) { + const template = await loadTemplateAsync(path) + Vue.component('customer-market', { + name: 'customer-market', + template, + + props: ['products', 'exchange-rates'], + data: function () { + return {} + }, + methods: { + changePage() { + return + } + } + }) +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a85f2fc..dba048d 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -3,6 +3,7 @@ Settings +
@@ -83,7 +84,7 @@ label="Relay URL" hint="Add relays" > - + @@ -114,123 +115,42 @@
- -
- Market: -
-
- - - -
+ + + {%raw%} + + {{ activePage }} + + {%endraw%} + + +
-
-
- - {% raw %} - - - -
-
- {{ item.product }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ getAmountFormated(item.price, item.currency) }} - ({{ getValueInSats(item.price, item.currency) }} sats) - - {{item.quantity}} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} - - Visit Stall - - - {% endraw %} -
-
-
+
- - - {% endblock %} {% block scripts %} + + + + + + - - Vue.component(VueQrcode.name, VueQrcode) - - Promise.all([ - customerMarket('static/components/customer-market/customer-market.html'), - customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') - ]) - - new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - drawer: false, - pubkeys: new Set(), - relays: new Set(), - events: [], - stalls: [], - products: [], - profiles: new Map(), - searchText: null, - exchangeRates: null, - inputPubkey: null, - inputRelay: null, - activePage: 'market', - activeStall: null, - activeProduct: null - } - }, - computed: { - filterProducts() { - let products = this.products - if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) - } - if (!this.searchText || this.searchText.length < 2) return products - return products.filter(p => { - return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) - ) - }) - }, - stallName() { - return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' - }, - productName() { - return ( - this.products.find(p => p.id == this.activeProduct)?.name || 'Product' - ) - } - }, - async created() { - // Check for stored merchants and relays on localStorage - try { - let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) - let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) - if (merchants && merchants.length) { - this.pubkeys = new Set(merchants) - } - if (relays && relays.length) { - this.relays = new Set([...defaultRelays, ...relays]) - } - } catch (e) { - console.error(e) - } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - let merchant_pubkey = JSON.parse('{{ merchant_pubkey | tojson }}') - let stall_id = JSON.parse('{{ stall_id | tojson }}') - let product_id = JSON.parse('{{ product_id | tojson }}') - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() - - // What component to render on start - if (stall_id) { - if (product_id) { - this.activeProduct = product_id - } - this.activePage = 'stall' - this.activeStall = stall_id - } - - this.$q.loading.hide() - }, - methods: { - async initNostr() { - const pool = new nostr.SimplePool() - let relays = Array.from(this.relays) - let products = new Map() - let stalls = new Map() - // Get metadata and market data from the pubkeys - let sub = await pool - .list(relays, [ - { - kinds: [0, 30017, 30018], // for production kind is 30017 - authors: Array.from(this.pubkeys) - } - ]) - .then(events => { - console.log(events) - this.events = events || [] - this.events.map(eventToObj).map(e => { - if (e.kind == 0) { - this.profiles.set(e.pubkey, e.content) - return - } else if (e.kind == 30018) { - //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) - } else if (e.kind == 30017) { - // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) - return - } - }) - }) - await Promise.resolve(sub) - this.stalls = await Array.from(stalls.values()) - - this.products = Array.from(products.values()).map(obj => { - let stall = this.stalls.find(s => s.id == obj.stall_id) - obj.stallName = stall.name - if (obj.currency != 'sat') { - obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) - } - return obj - }) - - pool.close(relays) - }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { - let {stall, product, pubkey} = opts - let url = new URL(window.location) - - if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) - if (stall && !pubkey) { - pubkey = this.stalls.find(s => s.id == stall).pubkey - url.searchParams.set('merchant_pubkey', pubkey) - } - - switch (page) { - case 'stall': - if (stall) { - this.activeStall = stall - url.searchParams.set('stall_id', stall) - if (product) { - this.activeProduct = product - url.searchParams.set('product_id', product) - } - } - break - default: - this.activeStall = null - this.activeProduct = null - url.searchParams.delete('merchant_pubkey') - url.searchParams.delete('stall_id') - url.searchParams.delete('product_id') - break - } - - window.history.pushState({}, '', url) - this.activePage = page - }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, - getAmountFormated(amount, unit = 'USD') { - return LNbits.utils.formatCurrency(amount, unit) - }, - async addPubkey(pubkey = null) { - if (!pubkey) { - pubkey = String(this.inputPubkey).trim() - } - let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (pubkey.startsWith('n')) { - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays - } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) - } - } else if (regExp.test(pubkey)) { - pubkey = pubkey - } - this.pubkeys.add(pubkey) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - await this.initNostr() - }, - removePubkey(pubkey) { - // Needs a hack for Vue reactivity - let pubkeys = this.pubkeys - pubkeys.delete(pubkey) - this.profiles.delete(pubkey) - this.pubkeys = new Set(Array.from(pubkeys)) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - }, - async addRelay() { - let relay = String(this.inputRelay).trim() - if (!relay.startsWith('ws')) { - console.debug('invalid url') - return - } - this.relays.add(relay) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - this.inputRelay = null - await this.initNostr() - }, - removeRelay(relay) { - // Needs a hack for Vue reactivity - let relays = this.relays - relays.delete(relay) - this.relays = new Set(Array.from(relays)) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - } - } - }) - {% endblock %} From ac6df4041891671be0d54135587f3cfff8474f19 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:35 +0000 Subject: [PATCH 14/58] routing done --- .../customer-market/customer-market.html | 2 +- .../customer-market/customer-market.js | 6 +- .../customer-stall/customer-stall.html | 93 ++------------ .../customer-stall/customer-stall.js | 44 ++++--- .../components/product-card/product-card.html | 1 + .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 118 +----------------- static/js/market.js | 76 +++++------ templates/nostrmarket/market.html | 3 +- 9 files changed, 78 insertions(+), 267 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 5de0b0d..ce52d87 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,7 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - +
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 992222a..c14ffa9 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -8,7 +8,11 @@ async function customerMarket(path) { data: function () { return {} }, - methods: {}, + methods: { + changePageM(page, opts) { + this.$emit('change-page', page, opts) + } + }, created() {} }) } diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 306a5f7..9bc302c 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,94 +10,25 @@ - + - +
+ +
+ +
+
- - - - - Add to cart -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- See product -
-
-
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 7606d68..1d9b395 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -1,34 +1,32 @@ async function customerStall(path) { const template = await loadTemplateAsync(path) - const mock = { - stall: '4M8j9KKGzUckHgb4C3pKCv', - name: 'product 1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Leo integer malesuada nunc vel risus commodo. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Cras ornare arcu dui vivamus. Risus pretium quam vulputate dignissim suspendisse in est ante in. Faucibus in ornare quam viverra orci sagittis eu volutpat odio.', - amount: 100, - price: '10', - images: ['https://i.imgur.com/cEfpEjq.jpeg'], - id: ['RyMbE9Hdwk9X333JKtkkNS'], - categories: ['crafts', 'robots'], - currency: 'EUR', - stallName: 'stall 1', - formatedPrice: '€10.00', - priceInSats: 0 - } + Vue.component('customer-stall', { name: 'customer-stall', template, - props: ['stall', 'products', 'exchange-rates', 'product-detail'], + props: [ + 'stall', + 'products', + 'exchange-rates', + 'product-detail', + 'change-page' + ], data: function () { - return { - mock: mock + return {} + }, + computed: { + product() { + if (this.productDetail) { + return this.products.find(p => p.id == this.productDetail) + } } }, - methods: {}, - created() { - console.log(this.stall) - console.log(this.products) - } + methods: { + changePageS(page, opts) { + this.$emit('change-page', page, opts) + } + }, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5ff2782..93d4d96 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -52,6 +52,7 @@ Stall: {{ product.stallName }} + {{ $parent.activeStall }}
{{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }} -
-
-
Customer rating
-
4.2
-
- -
-
(357 reviews)
-
- 93% would recommend to a friend -
- - - - 5 - - - 273 - - - 4 - - -   69 - - - 3 - - -      6 - - - 2 - - -      3 - - - 1 - - -      6 - - -
-
- -
diff --git a/static/js/market.js b/static/js/market.js index 3c823fc..6a0d723 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -30,7 +30,8 @@ const market = async () => { productCard('static/components/product-card/product-card.html'), customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') + productDetail('static/components/product-detail/product-detail.html'), + shoppingCart('static/components/shopping-cart/shopping-cart.html') ]) new Vue({ @@ -58,7 +59,7 @@ const market = async () => { filterProducts() { let products = this.products if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) + products = products.filter(p => p.stall_id == this.activeStall) } if (!this.searchText || this.searchText.length < 2) return products return products.filter(p => { @@ -76,6 +77,9 @@ const market = async () => { return ( this.products.find(p => p.id == this.activeProduct)?.name || 'Product' ) + }, + isLoading() { + return this.$q.loading.isActive } }, async created() { @@ -88,51 +92,17 @@ const market = async () => { } if (relays && relays.length) { this.relays = new Set([...defaultRelays, ...relays]) + } else { + this.relays = new Set(defaultRelays) } } catch (e) { console.error(e) } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - /*let naddr = nostr.nip19.naddrEncode({ - identifier: '1234', - pubkey: - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', - kind: 30018, - relays: defaultRelays - }) - console.log(naddr) - console.log(nostr.nip19.decode(naddr)) - */ + let params = new URLSearchParams(window.location.search) let merchant_pubkey = params.get('merchant_pubkey') let stall_id = params.get('stall_id') let product_id = params.get('product_id') - console.log(merchant_pubkey, stall_id, product_id) - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() // What component to render on start if (stall_id) { @@ -142,11 +112,33 @@ const market = async () => { this.activePage = 'stall' this.activeStall = stall_id } + if (merchant_pubkey && !this.pubkeys.has(merchant_pubkey)) { + await LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onOk(async () => { + await this.addPubkey(merchant_pubkey) + }) + } + // Get notes from Nostr + await this.initNostr() this.$q.loading.hide() }, methods: { + naddr() { + let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + }, async initNostr() { + this.$q.loading.show() const pool = new nostr.SimplePool() let relays = Array.from(this.relays) let products = new Map() @@ -168,10 +160,10 @@ const market = async () => { return } else if (e.kind == 30018) { //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) + products.set(e.d, {...e.content, id: e.d[0], categories: e.t}) } else if (e.kind == 30017) { // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey}) return } }) @@ -182,13 +174,13 @@ const market = async () => { this.products = Array.from(products.values()).map(obj => { let stall = this.stalls.find(s => s.id == obj.stall_id) obj.stallName = stall.name + obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) - pool.close(relays) }, async getRates() { diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 0d3c36e..c885e10 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -140,7 +140,7 @@ + {% endblock %} From cbe4d32d8a05dd34920dac28a45c03fa5160375a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:46 +0000 Subject: [PATCH 15/58] init shopping cart --- .../components/shopping-cart/shopping-cart.html | 1 + static/components/shopping-cart/shopping-cart.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 static/components/shopping-cart/shopping-cart.html create mode 100644 static/components/shopping-cart/shopping-cart.js diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html new file mode 100644 index 0000000..8650cc6 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.html @@ -0,0 +1 @@ + diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js new file mode 100644 index 0000000..e0ad053 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.js @@ -0,0 +1,16 @@ +async function shoppingCart(path) { + const template = await loadTemplateAsync(path) + + Vue.component('shopping-cart', { + name: 'shopping-cart', + template, + + props: [], + data: function () { + return {} + }, + computed: {}, + methods: {}, + created() {} + }) +} From 8d5d546884fa47496c5ae08fdb128556f09bfafd Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sat, 4 Mar 2023 19:49:58 +0000 Subject: [PATCH 16/58] general fixes, prop passing and shopping cart (doesn't send dm) --- .../customer-stall/customer-stall.html | 106 ++++++++++- .../customer-stall/customer-stall.js | 167 +++++++++++++++++- .../components/product-card/product-card.html | 16 ++ .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 12 +- .../product-detail/product-detail.js | 15 +- .../shopping-cart/shopping-cart.html | 52 +++++- .../components/shopping-cart/shopping-cart.js | 2 +- static/js/market.js | 17 +- templates/nostrmarket/market.html | 1 + templates/nostrmarket/stall.html | 61 ------- 11 files changed, 355 insertions(+), 96 deletions(-) delete mode 100644 templates/nostrmarket/stall.html diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 9bc302c..aedec55 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,13 +10,20 @@ - +
@@ -28,7 +35,102 @@ v-for="(item, idx) in products" :key="idx" > - +
+ + + + + + + + + +
+
+ Generate key pair +
+
+ Get from Extension +
+
+ + +

Select the shipping zone:

+
+ +
+
+ Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : + finalCost + 'sats' }} + ({{ getValueInSats(finalCost) }} sats) +
+
+ Checkout + Cancel +
+
+
+
+ diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 1d9b395..858ed9d 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -10,23 +10,184 @@ async function customerStall(path) { 'products', 'exchange-rates', 'product-detail', - 'change-page' + 'change-page', + 'relays' ], data: function () { - return {} + return { + cart: { + total: 0, + size: 0, + products: new Map() + }, + cartMenu: [], + hasNip07: false, + checkoutDialog: { + show: false, + data: { + pubkey: null + } + }, + qrCodeDialog: { + data: { + payment_request: null + }, + show: false + } + } }, computed: { product() { if (this.productDetail) { return this.products.find(p => p.id == this.productDetail) } + }, + finalCost() { + if (!this.checkoutDialog.data.shippingzone) return this.cart.total + + let zoneCost = this.stall.shipping.find( + z => z.id == this.checkoutDialog.data.shippingzone + ) + return +this.cart.total + zoneCost.cost } }, methods: { changePageS(page, opts) { this.$emit('change-page', page, opts) + }, + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + addToCart(item) { + console.log('add to cart', item) + let prod = this.cart.products + if (prod.has(item.id)) { + let qty = prod.get(item.id).quantity + prod.set(item.id, { + ...prod.get(item.id), + quantity: qty + 1 + }) + } else { + prod.set(item.id, { + name: item.name, + quantity: 1, + price: item.price, + image: item?.images[0] || null + }) + } + this.$q.notify({ + type: 'positive', + message: `${item.name} added to cart`, + icon: 'thumb_up' + }) + this.cart.products = prod + this.updateCart(+item.price) + }, + removeFromCart(item) { + this.cart.products.delete(item.id) + this.updateCart(+item.price, true) + }, + updateCart(price, del = false) { + console.log(this.cart, this.cartMenu) + if (del) { + this.cart.total -= price + this.cart.size-- + } else { + this.cart.total += price + this.cart.size++ + } + this.cartMenu = Array.from(this.cart.products, item => { + return {id: item[0], ...item[1]} + }) + console.log(this.cart, this.cartMenu) + }, + resetCart() { + this.cart = { + total: 0, + size: 0, + products: new Map() + } + }, + async getPubkey() { + try { + this.checkoutDialog.data.pubkey = await window.nostr.getPublicKey() + this.checkoutDialog.data.privkey = null + } catch (err) { + console.error( + `Failed to get a public key from a Nostr extension: ${err}` + ) + } + }, + generateKeyPair() { + let sk = NostrTools.generatePrivateKey() + let pk = NostrTools.getPublicKey(sk) + this.checkoutDialog.data.pubkey = pk + this.checkoutDialog.data.privkey = sk + }, + placeOrder() { + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let content = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let event = { + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: await window.nostr.nip04.encrypt( + orderData.pubkey, + content + ), + pubkey: orderData.pubkey + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + await window.nostr.signEvent(event) + } + await this.sendOrder(event) + }) + }, + async sendOrder(order) { + const pool = new NostrTools.SimplePool() + let relays = Array.from(this.relays) + let pubs = await pool.publish(relays, order) + pubs.on('ok', relay => { + console.log(`${relay.url} has accepted our event`) + }) + pubs.on('failed', reason => { + console.log(`failed to publish to ${reason}`) + }) } }, - created() {} + created() { + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + } }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 93d4d96..5a92ba3 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -9,6 +9,22 @@ >
+ Add to cart
{{ product.name }}
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js index 9e8490b..5e049df 100644 --- a/static/components/product-card/product-card.js +++ b/static/components/product-card/product-card.js @@ -4,7 +4,7 @@ async function productCard(path) { name: 'product-card', template, - props: ['product', 'change-page'], + props: ['product', 'change-page', 'add-to-cart', 'is-stall'], data: function () { return {} }, diff --git a/static/components/product-detail/product-detail.html b/static/components/product-detail/product-detail.html index 92fa1c0..be31eda 100644 --- a/static/components/product-detail/product-detail.html +++ b/static/components/product-detail/product-detail.html @@ -17,6 +17,11 @@ style="/*background-size: contain; background-repeat: no-repeat*/" > +
@@ -47,7 +52,7 @@ {{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }}{{ product.quantity > 0 ? 'In stock.' : 'Out of stock.' }}
@@ -56,12 +61,13 @@ color="primary" icon="shopping_cart" label="Add to cart" + @click="$emit('add-to-cart', product)" />
diff --git a/static/components/product-detail/product-detail.js b/static/components/product-detail/product-detail.js index 7b60f6b..d55b653 100644 --- a/static/components/product-detail/product-detail.js +++ b/static/components/product-detail/product-detail.js @@ -4,23 +4,14 @@ async function productDetail(path) { name: 'product-detail', template, - props: ['product'], + props: ['product', 'add-to-cart'], data: function () { return { slide: 1 } }, - computed: { - win_width() { - return this.$q.screen.width - 59 - }, - win_height() { - return this.$q.screen.height - 0 - } - }, + computed: {}, methods: {}, - created() { - console.log('ping') - } + created() {} }) } diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html index 8650cc6..2864cf3 100644 --- a/static/components/shopping-cart/shopping-cart.html +++ b/static/components/shopping-cart/shopping-cart.html @@ -1 +1,51 @@ - + + + {{ cart.size }} + + + + + + {{p.quantity}} x + + + + + + + + + {{ p.name }} + + + + + {{p.currency != 'sat' ? p.formatedPrice : p.price + 'sats'}} + + + + + + +
+ +
diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js index e0ad053..8f6902d 100644 --- a/static/components/shopping-cart/shopping-cart.js +++ b/static/components/shopping-cart/shopping-cart.js @@ -5,7 +5,7 @@ async function shoppingCart(path) { name: 'shopping-cart', template, - props: [], + props: ['cart', 'cart-menu', 'remove-from-cart', 'reset-cart'], data: function () { return {} }, diff --git a/static/js/market.js b/static/js/market.js index 6a0d723..95bf5a6 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -1,20 +1,13 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) - const nostr = window.NostrTools + const NostrTools = window.NostrTools const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nos.lol', 'wss://nostr.wine', - 'wss://relay.nostr.bg', 'wss://nostr-pub.wellorder.net', - 'wss://nostr-pub.semisol.dev', - 'wss://eden.nostr.land', - 'wss://nostr.mom', - 'wss://nostr.fmt.wiz.biz', - 'wss://nostr.zebedee.cloud', - 'wss://nostr.rocks' + 'wss://nostr.zebedee.cloud' ] const eventToObj = event => { event.content = JSON.parse(event.content) @@ -128,7 +121,7 @@ const market = async () => { }, methods: { naddr() { - let naddr = nostr.nip19.naddrEncode({ + let naddr = NostrTools.nip19.naddrEncode({ identifier: '1234', pubkey: 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', @@ -139,7 +132,7 @@ const market = async () => { }, async initNostr() { this.$q.loading.show() - const pool = new nostr.SimplePool() + const pool = new NostrTools.SimplePool() let relays = Array.from(this.relays) let products = new Map() let stalls = new Map() @@ -243,7 +236,7 @@ const market = async () => { let regExp = /^#([0-9a-f]{3}){1,2}$/i if (pubkey.startsWith('n')) { try { - let {type, data} = nostr.nip19.decode(pubkey) + let {type, data} = NostrTools.nip19.decode(pubkey) if (type === 'npub') pubkey = data else if (type === 'nprofile') { pubkey = data.pubkey diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index c885e10..5f01c41 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -145,6 +145,7 @@ :products="filterProducts" :exchange-rates="exchangeRates" :product-detail="activeProduct" + :relays="relays" @change-page="navigateTo" > -{% endblock %} {% block scripts %} - - -{% endblock %} From 7da07491595f702e8960c72a7b85bf74d8cdf30b Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sun, 5 Mar 2023 20:32:35 +0000 Subject: [PATCH 17/58] ordering working --- .../customer-stall/customer-stall.js | 138 +++++++++++------- static/js/market.js | 2 +- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 858ed9d..ce2be6e 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -24,9 +24,7 @@ async function customerStall(path) { hasNip07: false, checkoutDialog: { show: false, - data: { - pubkey: null - } + data: {} }, qrCodeDialog: { data: { @@ -130,56 +128,96 @@ async function customerStall(path) { this.checkoutDialog.data.pubkey = pk this.checkoutDialog.data.privkey = sk }, - placeOrder() { - LNbits.utils - .confirmDialog( - `Send the order to the merchant? You should receive a message with the payment details.` - ) - .onOk(async () => { - let orderData = this.checkoutDialog.data - let content = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} - }) - } - let event = { - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: await window.nostr.nip04.encrypt( - orderData.pubkey, - content - ), - pubkey: orderData.pubkey - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - await window.nostr.signEvent(event) - } - await this.sendOrder(event) + async placeOrder() { + // LNbits.utils + // .confirmDialog( + // `Send the order to the merchant? You should receive a message with the payment details.` + // ) + // .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} }) + } + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.stall.pubkey]], + pubkey: orderData.pubkey + } + if (orderData.privkey) { + event.content = await NostrTools.nip04.encrypt( + orderData.privkey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + console.log('use extension') + event.content = await window.nostr.nip04.encrypt( + orderData.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = await NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderData) + await this.sendOrder(event) + // }) }, async sendOrder(order) { - const pool = new NostrTools.SimplePool() - let relays = Array.from(this.relays) - let pubs = await pool.publish(relays, order) - pubs.on('ok', relay => { - console.log(`${relay.url} has accepted our event`) - }) - pubs.on('failed', reason => { - console.log(`failed to publish to ${reason}`) - }) + for (const url of Array.from(this.relays)) { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } + this.checkoutDialog = {show: false, data: {}} + // const pool = new NostrTools.SimplePool() + // let relays = Array.from(this.relays) + // try { + // let pubs = await pool.publish(relays, order) + // pubs.on('ok', relay => { + // console.log(`${relay.url} has accepted our event`) + // }) + // pubs.on('failed', (reason, err) => { + // console.log(`failed to publish to ${reason}: ${err}`) + // }) + // } catch (err) { + // console.error(err) + // } } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 95bf5a6..a7c724e 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -5,7 +5,6 @@ const market = async () => { const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nostr.wine', 'wss://nostr-pub.wellorder.net', 'wss://nostr.zebedee.cloud' ] @@ -175,6 +174,7 @@ const market = async () => { return obj }) pool.close(relays) + return }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') From abd169e7930cb1e5cdf230d0bb3e7c95594b6672 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 09:59:17 +0000 Subject: [PATCH 18/58] hash string --- static/js/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/js/utils.js b/static/js/utils.js index 11ebc81..e684ab8 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -16,3 +16,13 @@ function loadTemplateAsync(path) { return result } + +async function hash(string) { + const utf8 = new TextEncoder().encode(string) + const hashBuffer = await crypto.subtle.digest('SHA-256', utf8) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map(bytes => bytes.toString(16).padStart(2, '0')) + .join('') + return hashHex +} From 3f4860e1c05b7b4475dbf37c6cd372a39de96384 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 12:03:32 +0000 Subject: [PATCH 19/58] send/receive messages --- .../customer-stall/customer-stall.html | 7 + .../customer-stall/customer-stall.js | 239 +++++++++++------- 2 files changed, 159 insertions(+), 87 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index aedec55..c6ac229 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -95,6 +95,13 @@ label="Email *optional" hint="Merchant may not use email" > +

Select the shipping zone:

{ - let orderData = this.checkoutDialog.data - let orderObj = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + address: orderData.address, + message: orderData?.message, + contact: { + nostr: this.customerPubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let created_at = Math.floor(Date.now() / 1000) + orderObj.id = await hash( + [this.customerPubkey, created_at, JSON.stringify(orderObj)].join( + ':' + ) + ) + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at, + tags: [['p', this.stall.pubkey]], + pubkey: this.customerPubkey + } + if (this.customerPrivKey) { + event.content = await NostrTools.nip04.encrypt( + this.customerPrivKey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + event.content = await window.nostr.nip04.encrypt( + this.stall.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (this.customerPrivKey) { + event.sig = await NostrTools.signEvent( + event, + this.customerPrivKey + ) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderObj) + await this.sendOrder(event) }) - } - let event = { - ...(await NostrTools.getBlankEvent()), - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', this.stall.pubkey]], - pubkey: orderData.pubkey - } - if (orderData.privkey) { - event.content = await NostrTools.nip04.encrypt( - orderData.privkey, - this.stall.pubkey, - JSON.stringify(orderObj) - ) - } else { - console.log('use extension') - event.content = await window.nostr.nip04.encrypt( - orderData.pubkey, - JSON.stringify(orderObj) - ) - let userRelays = Object.keys( - (await window.nostr?.getRelays?.()) || [] - ) - if (userRelays.length != 0) { - userRelays.map(r => this.relays.add(r)) - } - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = await NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - event = await window.nostr.signEvent(event) - } - console.log(event, orderData) - await this.sendOrder(event) - // }) }, async sendOrder(order) { for (const url of Array.from(this.relays)) { - let relay = NostrTools.relayInit(url) - relay.on('connect', () => { - console.log(`connected to ${relay.url}`) - }) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`) - }) + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) - await relay.connect() - let pub = relay.publish(order) - pub.on('ok', () => { - console.log(`${relay.url} has accepted our event`) - }) - pub.on('failed', reason => { - console.log(`failed to publish to ${relay.url}: ${reason}`) - }) + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } catch (err) { + console.error(`Error: ${err}`) + } + } + this.resetCheckout() + this.listenMessages() + }, + async listenMessages() { + try { + const pool = new NostrTools.SimplePool() + const filters = [ + { + kinds: [4], + authors: [this.customerPubkey] + }, + { + kinds: [4], + '#p': [this.customerPubkey] + } + ] + let relays = Array.from(this.relays) + let subs = pool.sub(relays, filters) + subs.on('event', async event => { + let mine = event.pubkey == this.customerPubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + if ( + (mine && sender != this.stall.pubkey) || + (!mine && sender != this.customerPubkey) + ) { + console.log(`Not relevant message!`) + return + } + try { + let plaintext = this.customerPrivKey + ? await NostrTools.nip04.decrypt( + this.customerPrivKey, + sender, + event.content + ) + : await window.nostr.nip04.decrypt(sender, event.content) + // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) + this.nostrMessages.set(event.id, { + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + return + } + }) + } catch (err) { + console.error(`Error: ${err}`) } - this.checkoutDialog = {show: false, data: {}} - // const pool = new NostrTools.SimplePool() - // let relays = Array.from(this.relays) - // try { - // let pubs = await pool.publish(relays, order) - // pubs.on('ok', relay => { - // console.log(`${relay.url} has accepted our event`) - // }) - // pubs.on('failed', (reason, err) => { - // console.log(`failed to publish to ${reason}: ${err}`) - // }) - // } catch (err) { - // console.error(err) - // } } }, created() { From 07dddc628b3179933357ad6a7237fc86394734d2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 7 Mar 2023 13:25:16 +0000 Subject: [PATCH 20/58] login/account functionality --- static/js/market.js | 70 ++++++++++++++++- templates/nostrmarket/market.html | 120 ++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index a7c724e..88042ce 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -2,6 +2,7 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) const NostrTools = window.NostrTools + const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', @@ -23,7 +24,8 @@ const market = async () => { customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), productDetail('static/components/product-detail/product-detail.html'), - shoppingCart('static/components/shopping-cart/shopping-cart.html') + shoppingCart('static/components/shopping-cart/shopping-cart.html'), + chatDialog('static/components/chat-dialog/chat-dialog.html') ]) new Vue({ @@ -31,7 +33,15 @@ const market = async () => { mixins: [windowMixin], data: function () { return { - drawer: false, + account: null, + accountDialog: { + show: false, + data: { + watchOnly: false, + key: null + } + }, + drawer: true, pubkeys: new Set(), relays: new Set(), events: [], @@ -72,9 +82,20 @@ const market = async () => { }, isLoading() { return this.$q.loading.isActive + }, + hasExtension() { + return window.nostr + }, + isValidKey() { + return this.accountDialog.data.key + ?.toLowerCase() + ?.match(/^[0-9a-f]{64}$/) } }, async created() { + // Check for user stored + this.account = this.$q.localStorage.getItem('diagonAlley.account') || null + // Check for stored merchants and relays on localStorage try { let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) @@ -115,7 +136,10 @@ const market = async () => { } // Get notes from Nostr - await this.initNostr() + //await this.initNostr() + + // Get fiat rates (i think there's an LNbits endpoint for this) + //await this.getRates() this.$q.loading.hide() }, methods: { @@ -129,6 +153,46 @@ const market = async () => { }) console.log(naddr) }, + async deleteAccount() { + await LNbits.utils + .confirmDialog( + `This will delete all stored data. If you didn't backup the Key Pair (Private and Public Keys), you will lose it. Continue?` + ) + .onOk(() => { + window.localStorage.removeItem('diagonAlley.account') + this.account = null + }) + }, + async createAccount(useExtension = false) { + let nip07 + if (useExtension) { + await this.getFromExtension() + nip07 = true + } + if (this.isValidKey) { + let {key, watchOnly} = this.accountDialog.data + this.$q.localStorage.set('diagonAlley.account', { + privkey: watchOnly ? null : key, + pubkey: watchOnly ? key : NostrTools.getPublicKey(key), + useExtension: nip07 ?? false + }) + this.accountDialog.data = { + watchOnly: false, + key: null + } + this.accountDialog.show = false + this.account = this.$q.localStorage.getItem('diagonAlley.account') + } + }, + generateKeyPair() { + this.accountDialog.data.key = NostrTools.generatePrivateKey() + this.accountDialog.data.watchOnly = false + }, + async getFromExtension() { + this.accountDialog.data.key = await window.nostr.getPublicKey() + this.accountDialog.data.watchOnly = true + return + }, async initNostr() { this.$q.loading.show() const pool = new NostrTools.SimplePool() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 5f01c41..77c0cac 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -5,6 +5,32 @@ Settings +
+
+ + Delete account data +
+
+ Login or Create account +
+ +
+ + + + +
Account Setup
+ + +
+ +

Type your Nostr private key or generate a new one.

+ You can also use a Nostr-capable extension. +
+ + + + + + + + + + Is this a Public Key? + + If not using an Nostr capable extension, you'll have to sign + events manually! Better to use a Private Key that you can delete + later, or just generate an ephemeral key pair to use in the + Marketplace! + + + + + + + + + + +
+
{% endblock %} {% block scripts %} @@ -165,6 +247,44 @@ + + {% endblock %} From 91f6a9d36cd5d555aa5d166500b1b35f842e9908 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:03 +0000 Subject: [PATCH 21/58] remove exchange rates --- .../customer-market/customer-market.js | 2 +- .../customer-stall/customer-stall.html | 72 +++++++-- .../customer-stall/customer-stall.js | 144 +++++++++++------- .../components/product-card/product-card.html | 3 - .../product-detail/product-detail.html | 3 - static/js/market.js | 25 +-- static/js/utils.js | 12 ++ templates/nostrmarket/market.html | 3 +- 8 files changed, 163 insertions(+), 101 deletions(-) diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index c14ffa9..45d5bea 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,7 +4,7 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates', 'change-page'], + props: ['products', 'change-page'], data: function () { return {} }, diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index c6ac229..ea63043 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,6 +10,12 @@ + @@ -64,12 +71,12 @@ filled dense readonly - hint="This your key pair! Don't lose it!" - v-if="checkoutDialog.data.privkey" - v-model="checkoutDialog.data.privkey" + type="password" + v-if="customerPrivkey" + v-model="customerPrivkey" > -
+
- Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : - finalCost + 'sats' }} - ({{ getValueInSats(finalCost) }} sats) + > -->
+ +
+ +
+
+ + +
+ Copy invoice + Close +
+
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index b72ba7d..4bd74c3 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -6,15 +6,16 @@ async function customerStall(path) { template, props: [ + 'account', 'stall', 'products', - 'exchange-rates', 'product-detail', 'change-page', 'relays' ], data: function () { return { + loading: false, cart: { total: 0, size: 0, @@ -23,8 +24,9 @@ async function customerStall(path) { cartMenu: [], hasNip07: false, customerPubkey: null, - customerPrivKey: null, - nostrMessages: new Map(), + customerPrivkey: null, + customerUseExtension: null, + activeOrder: null, checkoutDialog: { show: false, data: { @@ -58,12 +60,6 @@ async function customerStall(path) { changePageS(page, opts) { this.$emit('change-page', page, opts) }, - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, @@ -125,26 +121,12 @@ async function customerStall(path) { } } }, - async getPubkey() { - try { - this.customerPubkey = await window.nostr.getPublicKey() - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = null - } catch (err) { - console.error( - `Failed to get a public key from a Nostr extension: ${err}` - ) - } - }, - generateKeyPair() { - let sk = NostrTools.generatePrivateKey() - let pk = NostrTools.getPublicKey(sk) - this.customerPubkey = pk - this.customerPrivKey = sk - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = this.customerPrivKey + closeQrCodeDialog() { + this.qrCodeDialog.dismissMsg() + this.qrCodeDialog.show = false }, async placeOrder() { + this.loading = true LNbits.utils .confirmDialog( `Send the order to the merchant? You should receive a message with the payment details.` @@ -170,6 +152,7 @@ async function customerStall(path) { ':' ) ) + this.activeOrder = orderObj.id let event = { ...(await NostrTools.getBlankEvent()), kind: 4, @@ -177,13 +160,13 @@ async function customerStall(path) { tags: [['p', this.stall.pubkey]], pubkey: this.customerPubkey } - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.content = await NostrTools.nip04.encrypt( - this.customerPrivKey, + this.customerPrivkey, this.stall.pubkey, JSON.stringify(orderObj) ) - } else { + } else if (this.customerUseExtension && this.hasNip07) { event.content = await window.nostr.nip04.encrypt( this.stall.pubkey, JSON.stringify(orderObj) @@ -196,15 +179,15 @@ async function customerStall(path) { } } event.id = NostrTools.getEventHash(event) - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.sig = await NostrTools.signEvent( event, - this.customerPrivKey + this.customerPrivkey ) - } else if (this.hasNip07) { + } else if (this.customerUseExtension && this.hasNip07) { event = await window.nostr.signEvent(event) } - console.log(event, orderObj) + await this.sendOrder(event) }) }, @@ -223,25 +206,32 @@ async function customerStall(path) { let pub = relay.publish(order) pub.on('ok', () => { console.log(`${relay.url} has accepted our event`) + relay.close() }) pub.on('failed', reason => { console.log(`failed to publish to ${relay.url}: ${reason}`) + relay.close() }) } catch (err) { console.error(`Error: ${err}`) } } + this.loading = false this.resetCheckout() + this.resetCart() + this.qrCodeDialog.show = true + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for invoice from merchant...' + }) this.listenMessages() }, async listenMessages() { + console.log('LISTEN') try { const pool = new NostrTools.SimplePool() const filters = [ - { - kinds: [4], - authors: [this.customerPubkey] - }, + // / { kinds: [4], '#p': [this.customerPubkey] @@ -254,38 +244,74 @@ async function customerStall(path) { let sender = mine ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] : event.pubkey - if ( - (mine && sender != this.stall.pubkey) || - (!mine && sender != this.customerPubkey) - ) { - console.log(`Not relevant message!`) - return - } + try { - let plaintext = this.customerPrivKey - ? await NostrTools.nip04.decrypt( - this.customerPrivKey, - sender, - event.content - ) - : await window.nostr.nip04.decrypt(sender, event.content) - // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) - this.nostrMessages.set(event.id, { - msg: plaintext, - timestamp: event.created_at, - sender: `${mine ? 'Me' : 'Merchant'}` - }) + let plaintext + if (this.customerPrivkey) { + plaintext = await NostrTools.nip04.decrypt( + this.customerPrivkey, + sender, + event.content + ) + } else if (this.customerUseExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) + + // this.nostrMessages.set(event.id, { + // msg: plaintext, + // timestamp: event.created_at, + // sender: `${mine ? 'Me' : 'Merchant'}` + // }) + this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') - return } }) } catch (err) { console.error(`Error: ${err}`) } + }, + messageFilter(text, cb = () => {}) { + if (!isJson(text)) return + let json = JSON.parse(text) + if (json.id != this.activeOrder) return + if (json?.payment_options) { + // this.qrCodeDialog.show = true + this.qrCodeDialog.data.payment_request = json.payment_options.find( + o => o.type == 'ln' + ).link + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for payment...' + }) + } else if (json?.paid) { + this.qrCodeDialog.dismissMsg = this.$q.notify({ + type: 'positive', + message: 'Sats received, thanks!', + icon: 'thumb_up' + }) + this.closeQrCodeDialog() + this.activeOrder = null + Promise.resolve(cb()) + } else { + return + } } + // async mockInit() { + // this.customerPubkey = await window.nostr.getPublicKey() + // this.activeOrder = + // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' + // await this.listenMessages() + // } }, created() { + this.customerPubkey = this.account.pubkey + this.customerPrivkey = this.account.privkey + this.customerUseExtension = this.account.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5a92ba3..95f86e2 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -43,9 +43,6 @@ {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) {{ product.quantity }} left {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) { key: null } }, - drawer: true, + drawer: false, pubkeys: new Set(), relays: new Set(), events: [], @@ -49,7 +49,6 @@ const market = async () => { products: [], profiles: new Map(), searchText: null, - exchangeRates: null, inputPubkey: null, inputRelay: null, activePage: 'market', @@ -136,10 +135,8 @@ const market = async () => { } // Get notes from Nostr - //await this.initNostr() + await this.initNostr() - // Get fiat rates (i think there's an LNbits endpoint for this) - //await this.getRates() this.$q.loading.hide() }, methods: { @@ -233,23 +230,12 @@ const market = async () => { obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) pool.close(relays) return }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { let {stall, product, pubkey} = opts let url = new URL(window.location) @@ -283,13 +269,6 @@ const market = async () => { window.history.pushState({}, '', url) this.activePage = page }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, diff --git a/static/js/utils.js b/static/js/utils.js index e684ab8..c11cdef 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -26,3 +26,15 @@ async function hash(string) { .join('') return hashHex } + +function isJson(str) { + if (typeof str !== 'string') { + return false + } + try { + JSON.parse(str) + return true + } catch (error) { + return false + } +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 77c0cac..20a4332 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -169,15 +169,14 @@ v-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)" :products="filterProducts" - :exchange-rates="exchangeRates" :product-detail="activeProduct" :relays="relays" + :account="account" @change-page="navigateTo" > From 9d5ed55c371d24278c314c4a9f8a8f1a50b9ff80 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:15 +0000 Subject: [PATCH 22/58] chat dialog component --- .../components/chat-dialog/chat-dialog.html | 89 +++++++++++ static/components/chat-dialog/chat-dialog.js | 143 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 static/components/chat-dialog/chat-dialog.html create mode 100644 static/components/chat-dialog/chat-dialog.js diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html new file mode 100644 index 0000000..9ab5296 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.html @@ -0,0 +1,89 @@ +
+ + + + + +
Chat Box
+ + + + Minimize + + + Maximize + + + Close + +
+ + +
+
+ +
+ +
+
+ + + + + + + +
+
+
+
+
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js new file mode 100644 index 0000000..96f82f6 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.js @@ -0,0 +1,143 @@ +async function chatDialog(path) { + const template = await loadTemplateAsync(path) + + Vue.component('chat-dialog', { + name: 'chat-dialog', + template, + + props: ['account', 'merchant', 'relays'], + data: function () { + return { + dialog: false, + maximizedToggle: true, + pool: null, + nostrMessages: [], + newMessage: '' + } + }, + computed: { + sortedMessages() { + return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + } + }, + methods: { + async startPool() { + let sub = this.pool.sub(Array.from(this.relays), [ + { + kinds: [4], + authors: [this.account.pubkey] + }, + { + kinds: [4], + '#p': [this.account.pubkey] + } + ]) + sub.on('event', async event => { + let mine = event.pubkey == this.account.pubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + + try { + let plaintext + if (this.account.privkey) { + plaintext = await NostrTools.nip04.decrypt( + this.account.privkey, + sender, + event.content + ) + } else if (this.account.useExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + this.nostrMessages.push({ + id: event.id, + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + } + }) + }, + async sendMessage() { + if (this.newMessage && this.newMessage.length < 1) return + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.merchant]], + pubkey: this.account.pubkey, + content: await this.encryptMsg() + } + event.id = NostrTools.getEventHash(event) + event.sig = this.signEvent(event) + for (const url of Array.from(this.relays)) { + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.debug(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.debug(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(event) + pub.on('ok', () => { + console.debug(`${relay.url} has accepted our event`) + relay.close() + }) + pub.on('failed', reason => { + console.debug(`failed to publish to ${relay.url}: ${reason}`) + relay.close() + }) + this.newMessage = '' + } catch (e) { + console.error(e) + } + } + }, + async encryptMsg() { + try { + let cypher + if (this.account.privkey) { + cypher = await NostrTools.nip04.encrypt( + this.account.privkey, + this.merchant, + this.newMessage + ) + } else if (this.account.useExtension && this.hasNip07) { + cypher = await window.nostr.nip04.encrypt( + this.merchant, + this.newMessage + ) + } + return cypher + } catch (e) { + console.error(e) + } + }, + async signEvent(event) { + if (this.account.privkey) { + event.sig = await NostrTools.signEvent(event, this.account.privkey) + } else if (this.account.useExtension && this.hasNip07) { + event = await window.nostr.signEvent(event) + } + return event + } + }, + created() { + this.pool = new NostrTools.SimplePool() + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + this.startPool() + } + }) +} From a93d4166087b5c95f9dc31eb4762c5654bc55a18 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 11:23:51 +0000 Subject: [PATCH 23/58] clean up --- static/components/customer-stall/customer-stall.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index ea63043..5e2d085 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -121,9 +121,6 @@
Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost, stall.currency) : finalCost + 'sats' }} -
Date: Wed, 8 Mar 2023 11:24:11 +0000 Subject: [PATCH 24/58] fix add merchant pubkey --- static/js/market.js | 6 +++++- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0077344..0d823e9 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,6 +222,7 @@ const market = async () => { }) }) await Promise.resolve(sub) + this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -272,7 +273,8 @@ const market = async () => { getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - async addPubkey(pubkey = null) { + async addPubkey(pubkey) { + console.log(pubkey, this.inputPubkey) if (!pubkey) { pubkey = String(this.inputPubkey).trim() } @@ -285,6 +287,7 @@ const market = async () => { pubkey = data.pubkey givenRelays = data.relays } + console.log(pubkey) this.pubkeys.add(pubkey) this.inputPubkey = null } catch (err) { @@ -310,6 +313,7 @@ const market = async () => { `diagonAlley.merchants`, Array.from(this.pubkeys) ) + Promise.resolve(this.initNostr()) }, async addRelay() { let relay = String(this.inputRelay).trim() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 20a4332..a5ebeb3 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -44,12 +44,12 @@ - + From 6a94f6c3663fb5c99675900529b4734647989806 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:56:39 +0000 Subject: [PATCH 25/58] better-ish chat box --- .../components/chat-dialog/chat-dialog.html | 118 ++++++++---------- static/components/chat-dialog/chat-dialog.js | 81 +++++++++++- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 9ab5296..fd87273 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -1,9 +1,9 @@ -
- +
+ @@ -13,77 +13,57 @@
Chat Box
- - Minimize - - - Maximize - - + + Close - -
-
- -
- -
-
- - - - - - - -
+ + + + + + + + + + + +
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index 96f82f6..e89e26d 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -9,7 +9,7 @@ async function chatDialog(path) { data: function () { return { dialog: false, - maximizedToggle: true, + loading: false, pool: null, nostrMessages: [], newMessage: '' @@ -17,11 +17,22 @@ async function chatDialog(path) { }, computed: { sortedMessages() { - return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + return this.nostrMessages.sort((a, b) => b.timestamp - a.timestamp) } }, methods: { + async startDialog() { + this.dialog = true + await this.startPool() + }, + async closeDialog() { + this.dialog = false + await this.pool.close(Array.from(this.relays)) + }, async startPool() { + this.loading = true + this.pool = new NostrTools.SimplePool() + let messagesMap = new Map() let sub = this.pool.sub(Array.from(this.relays), [ { kinds: [4], @@ -32,6 +43,10 @@ async function chatDialog(path) { '#p': [this.account.pubkey] } ]) + sub.on('eose', () => { + this.loading = false + this.nostrMessages = Array.from(messagesMap.values()) + }) sub.on('event', async event => { let mine = event.pubkey == this.account.pubkey let sender = mine @@ -52,8 +67,7 @@ async function chatDialog(path) { event.content ) } - this.nostrMessages.push({ - id: event.id, + messagesMap.set(event.id, { msg: plaintext, timestamp: event.created_at, sender: `${mine ? 'Me' : 'Merchant'}` @@ -62,6 +76,10 @@ async function chatDialog(path) { console.error('Unable to decrypt message!') } }) + setTimeout(() => { + this.nostrMessages = Array.from(messagesMap.values()) + this.loading = false + }, 5000) }, async sendMessage() { if (this.newMessage && this.newMessage.length < 1) return @@ -128,16 +146,67 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event + }, + timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` } }, created() { - this.pool = new NostrTools.SimplePool() setTimeout(() => { if (window.nostr) { this.hasNip07 = true } }, 1000) - this.startPool() } }) } From 288111a14426c1011df33e75afb88220f002eedf Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:16 +0000 Subject: [PATCH 26/58] payment dialog flow --- .../customer-stall/customer-stall.html | 16 +++++++--------- .../components/customer-stall/customer-stall.js | 17 ++--------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 5e2d085..1e06b29 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -7,7 +7,10 @@ @click="$emit('change-page', 'market')" style="cursor: pointer" > - + - -
- -
-
+ + +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 4bd74c3..58ab910 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -231,7 +231,6 @@ async function customerStall(path) { try { const pool = new NostrTools.SimplePool() const filters = [ - // / { kinds: [4], '#p': [this.customerPubkey] @@ -261,11 +260,6 @@ async function customerStall(path) { } console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) - // this.nostrMessages.set(event.id, { - // msg: plaintext, - // timestamp: event.created_at, - // sender: `${mine ? 'Me' : 'Merchant'}` - // }) this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') @@ -280,7 +274,6 @@ async function customerStall(path) { let json = JSON.parse(text) if (json.id != this.activeOrder) return if (json?.payment_options) { - // this.qrCodeDialog.show = true this.qrCodeDialog.data.payment_request = json.payment_options.find( o => o.type == 'ln' ).link @@ -289,24 +282,18 @@ async function customerStall(path) { message: 'Waiting for payment...' }) } else if (json?.paid) { - this.qrCodeDialog.dismissMsg = this.$q.notify({ + this.closeQrCodeDialog() + this.$q.notify({ type: 'positive', message: 'Sats received, thanks!', icon: 'thumb_up' }) - this.closeQrCodeDialog() this.activeOrder = null Promise.resolve(cb()) } else { return } } - // async mockInit() { - // this.customerPubkey = await window.nostr.getPublicKey() - // this.activeOrder = - // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' - // await this.listenMessages() - // } }, created() { this.customerPubkey = this.account.pubkey From 4c3f969606c671b83ab1fa1b59a0f5bce8d8a21c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:34 +0000 Subject: [PATCH 27/58] tweaks --- static/js/market.js | 2 +- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0d823e9..3cf9b90 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,7 +222,6 @@ const market = async () => { }) }) await Promise.resolve(sub) - this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -234,6 +233,7 @@ const market = async () => { } return obj }) + this.$q.loading.hide() pool.close(relays) return }, diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a5ebeb3..baa9715 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -57,7 +57,7 @@ {{ profiles.get(pub).name }} Date: Wed, 8 Mar 2023 21:05:53 +0000 Subject: [PATCH 28/58] brush up checkout dialog --- .../components/chat-dialog/chat-dialog.html | 1 - .../customer-stall/customer-stall.html | 94 +++++++++++++++---- .../customer-stall/customer-stall.js | 25 ++++- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index fd87273..1e46a4d 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -2,7 +2,6 @@
@@ -62,35 +62,77 @@ v-model.trim="checkoutDialog.data.username" label="Name *optional" > + + + + It seems you haven't logged in. You can: +
    +
  1. + enter your public and private keys bellow (to sign the order + message) +
  2. +
  3. use a Nostr Signer Extension (NIP07)
  4. +
  5. + fill out the required fields, without keys, and download the + order and send as a direct message to the merchant on any + Nostr client +
  6. +
+
+ + Use a Nostr browser extension + Download the order and send manually + +
+
+ -
Download Order + Checkout @@ -154,7 +208,7 @@ position="top" @hide="closeQrCodeDialog" > - +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 58ab910..b417c91 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -16,6 +16,7 @@ async function customerStall(path) { data: function () { return { loading: false, + isPwd: true, cart: { total: 0, size: 0, @@ -113,6 +114,24 @@ async function customerStall(path) { products: new Map() } }, + async downloadOrder() { + return + }, + async getFromExtension() { + this.customerPubkey = await window.nostr.getPublicKey() + this.customerUseExtension = true + this.checkoutDialog.data.pubkey = this.customerPubkey + }, + openCheckout() { + // Check if user is logged in + if (this.customerPubkey) { + this.checkoutDialog.data.pubkey = this.customerPubkey + if (this.customerPrivkey && !useExtension) { + this.checkoutDialog.data.privkey = this.customerPrivkey + } + } + this.checkoutDialog.show = true + }, resetCheckout() { this.checkoutDialog = { show: false, @@ -296,9 +315,9 @@ async function customerStall(path) { } }, created() { - this.customerPubkey = this.account.pubkey - this.customerPrivkey = this.account.privkey - this.customerUseExtension = this.account.useExtension + this.customerPubkey = this.account?.pubkey + this.customerPrivkey = this.account?.privkey + this.customerUseExtension = this.account?.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true From 039b4e23cd123dee534b05c61a55b6036417488a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 9 Mar 2023 10:00:55 +0000 Subject: [PATCH 29/58] fixing vlad's review comments --- .../components/chat-dialog/chat-dialog.html | 2 +- static/components/chat-dialog/chat-dialog.js | 57 +------------------ static/js/market.js | 9 +-- static/js/utils.js | 54 ++++++++++++++++++ views.py | 14 +---- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 1e46a4d..a428044 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -33,7 +33,7 @@ :text="[message.msg]" :sent="message.sender == 'Me'" :bg-color="message.sender == 'Me' ? 'white' : 'light-green-2'" - :stamp="`${timeFromNow(message.timestamp * 1000)}`" + :stamp="message.timestamp" size="6" /> diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index e89e26d..c111e27 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -69,7 +69,7 @@ async function chatDialog(path) { } messagesMap.set(event.id, { msg: plaintext, - timestamp: event.created_at, + timestamp: timeFromNow(event.created_at * 1000), sender: `${mine ? 'Me' : 'Merchant'}` }) } catch { @@ -107,11 +107,9 @@ async function chatDialog(path) { let pub = relay.publish(event) pub.on('ok', () => { console.debug(`${relay.url} has accepted our event`) - relay.close() }) pub.on('failed', reason => { console.debug(`failed to publish to ${relay.url}: ${reason}`) - relay.close() }) this.newMessage = '' } catch (e) { @@ -146,59 +144,6 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event - }, - timeFromNow(time) { - // Get timestamps - let unixTime = new Date(time).getTime() - if (!unixTime) return - let now = new Date().getTime() - - // Calculate difference - let difference = unixTime / 1000 - now / 1000 - - // Setup return object - let tfn = {} - - // Check if time is in the past, present, or future - tfn.when = 'now' - if (difference > 0) { - tfn.when = 'future' - } else if (difference < -1) { - tfn.when = 'past' - } - - // Convert difference to absolute - difference = Math.abs(difference) - - // Calculate time unit - if (difference / (60 * 60 * 24 * 365) > 1) { - // Years - tfn.unitOfTime = 'years' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) - } else if (difference / (60 * 60 * 24 * 45) > 1) { - // Months - tfn.unitOfTime = 'months' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) - } else if (difference / (60 * 60 * 24) > 1) { - // Days - tfn.unitOfTime = 'days' - tfn.time = Math.floor(difference / (60 * 60 * 24)) - } else if (difference / (60 * 60) > 1) { - // Hours - tfn.unitOfTime = 'hours' - tfn.time = Math.floor(difference / (60 * 60)) - } else if (difference / 60 > 1) { - // Minutes - tfn.unitOfTime = 'minutes' - tfn.time = Math.floor(difference / 60) - } else { - // Seconds - tfn.unitOfTime = 'seconds' - tfn.time = Math.floor(difference) - } - - // Return time from now data - return `${tfn.time} ${tfn.unitOfTime}` } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 3cf9b90..52fed25 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -62,12 +62,13 @@ const market = async () => { if (this.activeStall) { products = products.filter(p => p.stall_id == this.activeStall) } - if (!this.searchText || this.searchText.length < 2) return products + const searchText = this.searchText.toLowerCase() + if (!searchText || searchText.length < 2) return products return products.filter(p => { return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) + p.name.toLowerCase().includes(searchText) || + p.description.toLowerCase().includes(searchText) || + p.categories.toLowerCase().includes(searchText) ) }) }, diff --git a/static/js/utils.js b/static/js/utils.js index c11cdef..7843723 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -38,3 +38,57 @@ function isJson(str) { return false } } + +function timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` +} diff --git a/views.py b/views.py index d83bc85..3b757fb 100644 --- a/views.py +++ b/views.py @@ -23,18 +23,8 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) -async def market( - request: Request, - stall_id: str = Query(None), - product_id: str = Query(None), - merchant_pubkey: str = Query(None), -): +async def market(request: Request): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - { - "request": request, - "stall_id": stall_id, - "product_id": product_id, - "merchant_pubkey": merchant_pubkey, - }, + {"request": request}, ) From 296c3915ead4fa6031b4b3358fc139d4f403ff9e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 14:45:45 +0000 Subject: [PATCH 30/58] initial files --- templates/nostrmarket/market.html | 202 ++++++++++++++++++++++++++++++ templates/nostrmarket/stall.html | 61 +++++++++ views.py | 18 +++ 3 files changed, 281 insertions(+) create mode 100644 templates/nostrmarket/market.html create mode 100644 templates/nostrmarket/stall.html diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html new file mode 100644 index 0000000..ef29755 --- /dev/null +++ b/templates/nostrmarket/market.html @@ -0,0 +1,202 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% raw %} + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ getAmountFormated(item.price, item.currency) }} + ({{ getValueInSats(item.price, item.currency) }} sats) + + {{item.quantity}} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} + + Visit Stall + + + {% endraw %} +
+
+
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/templates/nostrmarket/stall.html b/templates/nostrmarket/stall.html new file mode 100644 index 0000000..208938c --- /dev/null +++ b/templates/nostrmarket/stall.html @@ -0,0 +1,61 @@ +{% extends "public.html" %} {% block page %} +
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py index ca8e1f7..47b07ff 100644 --- a/views.py +++ b/views.py @@ -20,3 +20,21 @@ async def index(request: Request, user: User = Depends(check_user_exists)): "nostrmarket/index.html", {"request": request, "user": user.dict()}, ) + + +@nostrmarket_ext.get("/market", response_class=HTMLResponse) +async def market(request: Request): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/market.html", + { + "request": request, + }, + ) + + +@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) +async def stall(request: Request, stall_id: str): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/stall.html", + {"request": request, "stall_id": stall_id}, + ) From 69d699672c2f536850b8cb94654f02b305321f6e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 16:00:57 +0000 Subject: [PATCH 31/58] get nostr events and display --- templates/nostrmarket/market.html | 299 ++++++++++++++++++------------ 1 file changed, 182 insertions(+), 117 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index ef29755..eb18f39 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,113 +1,162 @@ {% extends "public.html" %} {% block page %} -
-
- -
- Market: -
-
- - - -
+ + + + Settings -
-
-
-
- - {% raw %} - - - -
-
- {{ item.product }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ getAmountFormated(item.price, item.currency) }} - ({{ getValueInSats(item.price, item.currency) }} sats) - - {{item.quantity}} left -
-
- {{cat}} -
-
+ + -

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} - + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + +
+ + - Visit Stall - - - {% endraw %} - -
-
+ + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + + + + +
+ + +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% raw %} + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ getAmountFormated(item.price, item.currency) }} + ({{ getValueInSats(item.price, item.currency) }} sats) + + {{item.quantity}} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} + + Visit Stall + + + {% endraw %} +
+
+
+
+ + + + {% endblock %} {% block scripts %} From de506b1111dad2c4fcf20c114f7d2f2ffa36c3b0 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 17:06:24 +0000 Subject: [PATCH 32/58] menu add pubkey/npub --- templates/nostrmarket/market.html | 120 +++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index eb18f39..33af581 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -14,10 +14,44 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + + + + + + {%raw%} + + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }} + + + + + {%endraw%} + + @@ -29,10 +63,35 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + {%raw%} + + {{ url }} + + + + + {%endraw%} + + @@ -190,11 +249,14 @@ return { drawer: true, pubkeys: new Set(), + relays: new Set(defaultRelays), stalls: [], products: [], events: [], searchText: null, - exchangeRates: null + exchangeRates: null, + inputPubkey: null, + inputRelay: null } }, computed: { @@ -220,7 +282,7 @@ async initNostr() { this.pool = new nostr.SimplePool() this.relays = new Set(defaultRelays) - await this.pool + let sub = await this.pool .list(Array.from(this.relays), [ { kinds: [30005], @@ -236,11 +298,12 @@ } else { // it's a stall this.stalls.push(e.content) + return } }) - console.log(this.stalls) - console.log(this.products) }) + await Promise.resolve(sub) + this.pool.close() }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -260,8 +323,41 @@ }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) + }, + addPubkey() { + let pubkey = String(this.inputPubkey).trim() + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (regExp.test(pubkey)) { + return this.pubkeys.add(pubkey) + } + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + }, + addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.inputRelay = null } } }) + {% endblock %} From af0d2774b995950ffe4e75e1ac6be122da36c33c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 21:04:29 +0000 Subject: [PATCH 33/58] remove pubkeys and relays --- templates/nostrmarket/market.html | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 33af581..103d7db 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,6 +1,6 @@ {% extends "public.html" %} {% block page %} - - + + Settings @@ -47,6 +47,7 @@ dense round icon="delete" + @click="removePubkey(pub)" /> {%endraw%} @@ -87,6 +88,7 @@ dense round icon="delete" + @click="removeRelay(url)" /> {%endraw%} @@ -212,7 +214,7 @@
- + @@ -247,9 +249,9 @@ mixins: [windowMixin], data: function () { return { - drawer: true, + drawer: false, pubkeys: new Set(), - relays: new Set(defaultRelays), + relays: new Set(), stalls: [], products: [], events: [], @@ -269,6 +271,9 @@ p.categories.includes(this.searchText) ) }) + }, + relayList() { + return Array.from(this.relays) } }, async created() { @@ -276,12 +281,12 @@ this.pubkeys.add( '855ea22a88d7df7ccd8497777db81f115575d5362f51df3af02ead383f5eaba2' ) - //await this.initNostr() + this.relays = new Set(defaultRelays) + await this.initNostr() }, methods: { async initNostr() { this.pool = new nostr.SimplePool() - this.relays = new Set(defaultRelays) let sub = await this.pool .list(Array.from(this.relays), [ { @@ -343,6 +348,12 @@ console.error(err) } }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + }, addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { @@ -351,13 +362,14 @@ } this.relays.add(relay) this.inputRelay = null + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) } } }) - {% endblock %} From c58af7e233485c8b77310549212c4c94e396c589 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:38:50 +0000 Subject: [PATCH 34/58] fetch profiles --- templates/nostrmarket/market.html | 104 +++++++++++++++++++----------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 103d7db..a85f2fc 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -24,20 +24,31 @@ > - + + {%raw%} - + + - {%raw%} {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }}{{ profiles.get(pub).name }} + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` + }} + {{ pub }} {% raw %} { this.events = events || [] this.events.map(eventToObj).map(e => { - if (e.content.stall) { - //it's a product - this.products.push(e.content) + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.content.stall) { + //it's a product `d` is the prod. id + products.set(e.d, e.content) } else { - // it's a stall - this.stalls.push(e.content) + // it's a stall `d` is the stall id + stalls.set(e.d, e.content) return } }) }) await Promise.resolve(sub) - this.pool.close() + this.products = Array.from(products.values()) + this.stalls = Array.from(stalls.values()) + pool.close(relays) }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -329,32 +352,36 @@ getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - addPubkey() { + async addPubkey() { let pubkey = String(this.inputPubkey).trim() let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (regExp.test(pubkey)) { - return this.pubkeys.add(pubkey) - } - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) + } else if (regExp.test(pubkey)) { + pubkey = pubkey } + this.pubkeys.add(pubkey) + await this.initNostr() }, removePubkey(pubkey) { // Needs a hack for Vue reactivity let pubkeys = this.pubkeys pubkeys.delete(pubkey) + this.profiles.delete(pubkey) this.pubkeys = new Set(Array.from(pubkeys)) }, - addRelay() { + async addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { console.debug('invalid url') @@ -362,6 +389,7 @@ } this.relays.add(relay) this.inputRelay = null + await this.initNostr() }, removeRelay(relay) { // Needs a hack for Vue reactivity From ec98664610defa03ca88286a1b000f4bc66fc7e0 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:39:05 +0000 Subject: [PATCH 35/58] avatar picture --- static/images/blank-avatar.webp | Bin 0 -> 11090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/blank-avatar.webp diff --git a/static/images/blank-avatar.webp b/static/images/blank-avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..513b0f3279d36569ead8f6553506c30fbb610703 GIT binary patch literal 11090 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 ASO5S3 literal 0 HcmV?d00001 From ff1b6ec4fb1b4d43a9d8a7c3531df02b1e16d572 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 1 Mar 2023 16:59:59 +0000 Subject: [PATCH 36/58] market component (ready until final product object) --- .../customer-market/customer-market.html | 83 ++++++++ .../customer-market/customer-market.js | 17 ++ templates/nostrmarket/market.html | 180 +++++++----------- views.py | 10 +- 4 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 static/components/customer-market/customer-market.html create mode 100644 static/components/customer-market/customer-market.js diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html new file mode 100644 index 0000000..99af972 --- /dev/null +++ b/static/components/customer-market/customer-market.html @@ -0,0 +1,83 @@ +
+
+ + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ item.formatedPrice }} + ({{ item.priceInSats }} sats) + + {{ item.amount }} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} +
+ See product + + Visit Stall + +
+
+
+
+
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js new file mode 100644 index 0000000..5844105 --- /dev/null +++ b/static/components/customer-market/customer-market.js @@ -0,0 +1,17 @@ +async function customerMarket(path) { + const template = await loadTemplateAsync(path) + Vue.component('customer-market', { + name: 'customer-market', + template, + + props: ['products', 'exchange-rates'], + data: function () { + return {} + }, + methods: { + changePage() { + return + } + } + }) +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a85f2fc..dba048d 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -3,6 +3,7 @@ Settings +
@@ -83,7 +84,7 @@ label="Relay URL" hint="Add relays" > - + @@ -114,123 +115,42 @@
- -
- Market: -
-
- - - -
+ + + {%raw%} + + {{ activePage }} + + {%endraw%} + + +
-
-
- - {% raw %} - - - -
-
- {{ item.product }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ getAmountFormated(item.price, item.currency) }} - ({{ getValueInSats(item.price, item.currency) }} sats) - - {{item.quantity}} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} - - Visit Stall - - - {% endraw %} -
-
-
+
- - - {% endblock %} {% block scripts %} + + + + + + - - Vue.component(VueQrcode.name, VueQrcode) - - Promise.all([ - customerMarket('static/components/customer-market/customer-market.html'), - customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') - ]) - - new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - drawer: false, - pubkeys: new Set(), - relays: new Set(), - events: [], - stalls: [], - products: [], - profiles: new Map(), - searchText: null, - exchangeRates: null, - inputPubkey: null, - inputRelay: null, - activePage: 'market', - activeStall: null, - activeProduct: null - } - }, - computed: { - filterProducts() { - let products = this.products - if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) - } - if (!this.searchText || this.searchText.length < 2) return products - return products.filter(p => { - return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) - ) - }) - }, - stallName() { - return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' - }, - productName() { - return ( - this.products.find(p => p.id == this.activeProduct)?.name || 'Product' - ) - } - }, - async created() { - // Check for stored merchants and relays on localStorage - try { - let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) - let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) - if (merchants && merchants.length) { - this.pubkeys = new Set(merchants) - } - if (relays && relays.length) { - this.relays = new Set([...defaultRelays, ...relays]) - } - } catch (e) { - console.error(e) - } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - let merchant_pubkey = JSON.parse('{{ merchant_pubkey | tojson }}') - let stall_id = JSON.parse('{{ stall_id | tojson }}') - let product_id = JSON.parse('{{ product_id | tojson }}') - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() - - // What component to render on start - if (stall_id) { - if (product_id) { - this.activeProduct = product_id - } - this.activePage = 'stall' - this.activeStall = stall_id - } - - this.$q.loading.hide() - }, - methods: { - async initNostr() { - const pool = new nostr.SimplePool() - let relays = Array.from(this.relays) - let products = new Map() - let stalls = new Map() - // Get metadata and market data from the pubkeys - let sub = await pool - .list(relays, [ - { - kinds: [0, 30017, 30018], // for production kind is 30017 - authors: Array.from(this.pubkeys) - } - ]) - .then(events => { - console.log(events) - this.events = events || [] - this.events.map(eventToObj).map(e => { - if (e.kind == 0) { - this.profiles.set(e.pubkey, e.content) - return - } else if (e.kind == 30018) { - //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) - } else if (e.kind == 30017) { - // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) - return - } - }) - }) - await Promise.resolve(sub) - this.stalls = await Array.from(stalls.values()) - - this.products = Array.from(products.values()).map(obj => { - let stall = this.stalls.find(s => s.id == obj.stall_id) - obj.stallName = stall.name - if (obj.currency != 'sat') { - obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) - } - return obj - }) - - pool.close(relays) - }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { - let {stall, product, pubkey} = opts - let url = new URL(window.location) - - if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) - if (stall && !pubkey) { - pubkey = this.stalls.find(s => s.id == stall).pubkey - url.searchParams.set('merchant_pubkey', pubkey) - } - - switch (page) { - case 'stall': - if (stall) { - this.activeStall = stall - url.searchParams.set('stall_id', stall) - if (product) { - this.activeProduct = product - url.searchParams.set('product_id', product) - } - } - break - default: - this.activeStall = null - this.activeProduct = null - url.searchParams.delete('merchant_pubkey') - url.searchParams.delete('stall_id') - url.searchParams.delete('product_id') - break - } - - window.history.pushState({}, '', url) - this.activePage = page - }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, - getAmountFormated(amount, unit = 'USD') { - return LNbits.utils.formatCurrency(amount, unit) - }, - async addPubkey(pubkey = null) { - if (!pubkey) { - pubkey = String(this.inputPubkey).trim() - } - let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (pubkey.startsWith('n')) { - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays - } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) - } - } else if (regExp.test(pubkey)) { - pubkey = pubkey - } - this.pubkeys.add(pubkey) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - await this.initNostr() - }, - removePubkey(pubkey) { - // Needs a hack for Vue reactivity - let pubkeys = this.pubkeys - pubkeys.delete(pubkey) - this.profiles.delete(pubkey) - this.pubkeys = new Set(Array.from(pubkeys)) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - }, - async addRelay() { - let relay = String(this.inputRelay).trim() - if (!relay.startsWith('ws')) { - console.debug('invalid url') - return - } - this.relays.add(relay) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - this.inputRelay = null - await this.initNostr() - }, - removeRelay(relay) { - // Needs a hack for Vue reactivity - let relays = this.relays - relays.delete(relay) - this.relays = new Set(Array.from(relays)) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - } - } - }) - {% endblock %} From 85832b28c887e50ea20a840437e79c64ae14ef61 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:35 +0000 Subject: [PATCH 43/58] routing done --- .../customer-market/customer-market.html | 2 +- .../customer-market/customer-market.js | 6 +- .../customer-stall/customer-stall.html | 93 ++------------ .../customer-stall/customer-stall.js | 44 ++++--- .../components/product-card/product-card.html | 1 + .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 118 +----------------- static/js/market.js | 76 +++++------ templates/nostrmarket/market.html | 3 +- 9 files changed, 78 insertions(+), 267 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 5de0b0d..ce52d87 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,7 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - +
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 992222a..c14ffa9 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -8,7 +8,11 @@ async function customerMarket(path) { data: function () { return {} }, - methods: {}, + methods: { + changePageM(page, opts) { + this.$emit('change-page', page, opts) + } + }, created() {} }) } diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 306a5f7..9bc302c 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,94 +10,25 @@ - + - +
+ +
+ +
+
- - - - - Add to cart -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- See product -
-
-
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 7606d68..1d9b395 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -1,34 +1,32 @@ async function customerStall(path) { const template = await loadTemplateAsync(path) - const mock = { - stall: '4M8j9KKGzUckHgb4C3pKCv', - name: 'product 1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Leo integer malesuada nunc vel risus commodo. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Cras ornare arcu dui vivamus. Risus pretium quam vulputate dignissim suspendisse in est ante in. Faucibus in ornare quam viverra orci sagittis eu volutpat odio.', - amount: 100, - price: '10', - images: ['https://i.imgur.com/cEfpEjq.jpeg'], - id: ['RyMbE9Hdwk9X333JKtkkNS'], - categories: ['crafts', 'robots'], - currency: 'EUR', - stallName: 'stall 1', - formatedPrice: '€10.00', - priceInSats: 0 - } + Vue.component('customer-stall', { name: 'customer-stall', template, - props: ['stall', 'products', 'exchange-rates', 'product-detail'], + props: [ + 'stall', + 'products', + 'exchange-rates', + 'product-detail', + 'change-page' + ], data: function () { - return { - mock: mock + return {} + }, + computed: { + product() { + if (this.productDetail) { + return this.products.find(p => p.id == this.productDetail) + } } }, - methods: {}, - created() { - console.log(this.stall) - console.log(this.products) - } + methods: { + changePageS(page, opts) { + this.$emit('change-page', page, opts) + } + }, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5ff2782..93d4d96 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -52,6 +52,7 @@ Stall: {{ product.stallName }} + {{ $parent.activeStall }}
{{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }} -
-
-
Customer rating
-
4.2
-
- -
-
(357 reviews)
-
- 93% would recommend to a friend -
- - - - 5 - - - 273 - - - 4 - - -   69 - - - 3 - - -      6 - - - 2 - - -      3 - - - 1 - - -      6 - - -
-
- -
diff --git a/static/js/market.js b/static/js/market.js index 3c823fc..6a0d723 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -30,7 +30,8 @@ const market = async () => { productCard('static/components/product-card/product-card.html'), customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') + productDetail('static/components/product-detail/product-detail.html'), + shoppingCart('static/components/shopping-cart/shopping-cart.html') ]) new Vue({ @@ -58,7 +59,7 @@ const market = async () => { filterProducts() { let products = this.products if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) + products = products.filter(p => p.stall_id == this.activeStall) } if (!this.searchText || this.searchText.length < 2) return products return products.filter(p => { @@ -76,6 +77,9 @@ const market = async () => { return ( this.products.find(p => p.id == this.activeProduct)?.name || 'Product' ) + }, + isLoading() { + return this.$q.loading.isActive } }, async created() { @@ -88,51 +92,17 @@ const market = async () => { } if (relays && relays.length) { this.relays = new Set([...defaultRelays, ...relays]) + } else { + this.relays = new Set(defaultRelays) } } catch (e) { console.error(e) } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - /*let naddr = nostr.nip19.naddrEncode({ - identifier: '1234', - pubkey: - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', - kind: 30018, - relays: defaultRelays - }) - console.log(naddr) - console.log(nostr.nip19.decode(naddr)) - */ + let params = new URLSearchParams(window.location.search) let merchant_pubkey = params.get('merchant_pubkey') let stall_id = params.get('stall_id') let product_id = params.get('product_id') - console.log(merchant_pubkey, stall_id, product_id) - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() // What component to render on start if (stall_id) { @@ -142,11 +112,33 @@ const market = async () => { this.activePage = 'stall' this.activeStall = stall_id } + if (merchant_pubkey && !this.pubkeys.has(merchant_pubkey)) { + await LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onOk(async () => { + await this.addPubkey(merchant_pubkey) + }) + } + // Get notes from Nostr + await this.initNostr() this.$q.loading.hide() }, methods: { + naddr() { + let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + }, async initNostr() { + this.$q.loading.show() const pool = new nostr.SimplePool() let relays = Array.from(this.relays) let products = new Map() @@ -168,10 +160,10 @@ const market = async () => { return } else if (e.kind == 30018) { //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) + products.set(e.d, {...e.content, id: e.d[0], categories: e.t}) } else if (e.kind == 30017) { // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey}) return } }) @@ -182,13 +174,13 @@ const market = async () => { this.products = Array.from(products.values()).map(obj => { let stall = this.stalls.find(s => s.id == obj.stall_id) obj.stallName = stall.name + obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) - pool.close(relays) }, async getRates() { diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 0d3c36e..c885e10 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -140,7 +140,7 @@ + {% endblock %} From 562ac856534b9c4a8c3b2e463d867187afe1e00b Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:46 +0000 Subject: [PATCH 44/58] init shopping cart --- .../components/shopping-cart/shopping-cart.html | 1 + static/components/shopping-cart/shopping-cart.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 static/components/shopping-cart/shopping-cart.html create mode 100644 static/components/shopping-cart/shopping-cart.js diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html new file mode 100644 index 0000000..8650cc6 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.html @@ -0,0 +1 @@ + diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js new file mode 100644 index 0000000..e0ad053 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.js @@ -0,0 +1,16 @@ +async function shoppingCart(path) { + const template = await loadTemplateAsync(path) + + Vue.component('shopping-cart', { + name: 'shopping-cart', + template, + + props: [], + data: function () { + return {} + }, + computed: {}, + methods: {}, + created() {} + }) +} From c041fdac5f47085bb30c7e29c02efd615bdbba7a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sat, 4 Mar 2023 19:49:58 +0000 Subject: [PATCH 45/58] general fixes, prop passing and shopping cart (doesn't send dm) --- .../customer-stall/customer-stall.html | 106 ++++++++++- .../customer-stall/customer-stall.js | 167 +++++++++++++++++- .../components/product-card/product-card.html | 16 ++ .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 12 +- .../product-detail/product-detail.js | 15 +- .../shopping-cart/shopping-cart.html | 52 +++++- .../components/shopping-cart/shopping-cart.js | 2 +- static/js/market.js | 17 +- templates/nostrmarket/market.html | 1 + templates/nostrmarket/stall.html | 61 ------- 11 files changed, 355 insertions(+), 96 deletions(-) delete mode 100644 templates/nostrmarket/stall.html diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 9bc302c..aedec55 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,13 +10,20 @@ - +
@@ -28,7 +35,102 @@ v-for="(item, idx) in products" :key="idx" > - +
+ + + + + + + + + +
+
+ Generate key pair +
+
+ Get from Extension +
+
+ + +

Select the shipping zone:

+
+ +
+
+ Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : + finalCost + 'sats' }} + ({{ getValueInSats(finalCost) }} sats) +
+
+ Checkout + Cancel +
+
+
+
+ diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 1d9b395..858ed9d 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -10,23 +10,184 @@ async function customerStall(path) { 'products', 'exchange-rates', 'product-detail', - 'change-page' + 'change-page', + 'relays' ], data: function () { - return {} + return { + cart: { + total: 0, + size: 0, + products: new Map() + }, + cartMenu: [], + hasNip07: false, + checkoutDialog: { + show: false, + data: { + pubkey: null + } + }, + qrCodeDialog: { + data: { + payment_request: null + }, + show: false + } + } }, computed: { product() { if (this.productDetail) { return this.products.find(p => p.id == this.productDetail) } + }, + finalCost() { + if (!this.checkoutDialog.data.shippingzone) return this.cart.total + + let zoneCost = this.stall.shipping.find( + z => z.id == this.checkoutDialog.data.shippingzone + ) + return +this.cart.total + zoneCost.cost } }, methods: { changePageS(page, opts) { this.$emit('change-page', page, opts) + }, + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + addToCart(item) { + console.log('add to cart', item) + let prod = this.cart.products + if (prod.has(item.id)) { + let qty = prod.get(item.id).quantity + prod.set(item.id, { + ...prod.get(item.id), + quantity: qty + 1 + }) + } else { + prod.set(item.id, { + name: item.name, + quantity: 1, + price: item.price, + image: item?.images[0] || null + }) + } + this.$q.notify({ + type: 'positive', + message: `${item.name} added to cart`, + icon: 'thumb_up' + }) + this.cart.products = prod + this.updateCart(+item.price) + }, + removeFromCart(item) { + this.cart.products.delete(item.id) + this.updateCart(+item.price, true) + }, + updateCart(price, del = false) { + console.log(this.cart, this.cartMenu) + if (del) { + this.cart.total -= price + this.cart.size-- + } else { + this.cart.total += price + this.cart.size++ + } + this.cartMenu = Array.from(this.cart.products, item => { + return {id: item[0], ...item[1]} + }) + console.log(this.cart, this.cartMenu) + }, + resetCart() { + this.cart = { + total: 0, + size: 0, + products: new Map() + } + }, + async getPubkey() { + try { + this.checkoutDialog.data.pubkey = await window.nostr.getPublicKey() + this.checkoutDialog.data.privkey = null + } catch (err) { + console.error( + `Failed to get a public key from a Nostr extension: ${err}` + ) + } + }, + generateKeyPair() { + let sk = NostrTools.generatePrivateKey() + let pk = NostrTools.getPublicKey(sk) + this.checkoutDialog.data.pubkey = pk + this.checkoutDialog.data.privkey = sk + }, + placeOrder() { + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let content = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let event = { + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: await window.nostr.nip04.encrypt( + orderData.pubkey, + content + ), + pubkey: orderData.pubkey + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + await window.nostr.signEvent(event) + } + await this.sendOrder(event) + }) + }, + async sendOrder(order) { + const pool = new NostrTools.SimplePool() + let relays = Array.from(this.relays) + let pubs = await pool.publish(relays, order) + pubs.on('ok', relay => { + console.log(`${relay.url} has accepted our event`) + }) + pubs.on('failed', reason => { + console.log(`failed to publish to ${reason}`) + }) } }, - created() {} + created() { + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + } }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 93d4d96..5a92ba3 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -9,6 +9,22 @@ > + Add to cart
{{ product.name }}
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js index 9e8490b..5e049df 100644 --- a/static/components/product-card/product-card.js +++ b/static/components/product-card/product-card.js @@ -4,7 +4,7 @@ async function productCard(path) { name: 'product-card', template, - props: ['product', 'change-page'], + props: ['product', 'change-page', 'add-to-cart', 'is-stall'], data: function () { return {} }, diff --git a/static/components/product-detail/product-detail.html b/static/components/product-detail/product-detail.html index 92fa1c0..be31eda 100644 --- a/static/components/product-detail/product-detail.html +++ b/static/components/product-detail/product-detail.html @@ -17,6 +17,11 @@ style="/*background-size: contain; background-repeat: no-repeat*/" > +
@@ -47,7 +52,7 @@ {{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }}{{ product.quantity > 0 ? 'In stock.' : 'Out of stock.' }}
@@ -56,12 +61,13 @@ color="primary" icon="shopping_cart" label="Add to cart" + @click="$emit('add-to-cart', product)" />
diff --git a/static/components/product-detail/product-detail.js b/static/components/product-detail/product-detail.js index 7b60f6b..d55b653 100644 --- a/static/components/product-detail/product-detail.js +++ b/static/components/product-detail/product-detail.js @@ -4,23 +4,14 @@ async function productDetail(path) { name: 'product-detail', template, - props: ['product'], + props: ['product', 'add-to-cart'], data: function () { return { slide: 1 } }, - computed: { - win_width() { - return this.$q.screen.width - 59 - }, - win_height() { - return this.$q.screen.height - 0 - } - }, + computed: {}, methods: {}, - created() { - console.log('ping') - } + created() {} }) } diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html index 8650cc6..2864cf3 100644 --- a/static/components/shopping-cart/shopping-cart.html +++ b/static/components/shopping-cart/shopping-cart.html @@ -1 +1,51 @@ - + + + {{ cart.size }} + + + + + + {{p.quantity}} x + + + + + + + + + {{ p.name }} + + + + + {{p.currency != 'sat' ? p.formatedPrice : p.price + 'sats'}} + + + + + + +
+ +
diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js index e0ad053..8f6902d 100644 --- a/static/components/shopping-cart/shopping-cart.js +++ b/static/components/shopping-cart/shopping-cart.js @@ -5,7 +5,7 @@ async function shoppingCart(path) { name: 'shopping-cart', template, - props: [], + props: ['cart', 'cart-menu', 'remove-from-cart', 'reset-cart'], data: function () { return {} }, diff --git a/static/js/market.js b/static/js/market.js index 6a0d723..95bf5a6 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -1,20 +1,13 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) - const nostr = window.NostrTools + const NostrTools = window.NostrTools const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nos.lol', 'wss://nostr.wine', - 'wss://relay.nostr.bg', 'wss://nostr-pub.wellorder.net', - 'wss://nostr-pub.semisol.dev', - 'wss://eden.nostr.land', - 'wss://nostr.mom', - 'wss://nostr.fmt.wiz.biz', - 'wss://nostr.zebedee.cloud', - 'wss://nostr.rocks' + 'wss://nostr.zebedee.cloud' ] const eventToObj = event => { event.content = JSON.parse(event.content) @@ -128,7 +121,7 @@ const market = async () => { }, methods: { naddr() { - let naddr = nostr.nip19.naddrEncode({ + let naddr = NostrTools.nip19.naddrEncode({ identifier: '1234', pubkey: 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', @@ -139,7 +132,7 @@ const market = async () => { }, async initNostr() { this.$q.loading.show() - const pool = new nostr.SimplePool() + const pool = new NostrTools.SimplePool() let relays = Array.from(this.relays) let products = new Map() let stalls = new Map() @@ -243,7 +236,7 @@ const market = async () => { let regExp = /^#([0-9a-f]{3}){1,2}$/i if (pubkey.startsWith('n')) { try { - let {type, data} = nostr.nip19.decode(pubkey) + let {type, data} = NostrTools.nip19.decode(pubkey) if (type === 'npub') pubkey = data else if (type === 'nprofile') { pubkey = data.pubkey diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index c885e10..5f01c41 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -145,6 +145,7 @@ :products="filterProducts" :exchange-rates="exchangeRates" :product-detail="activeProduct" + :relays="relays" @change-page="navigateTo" >
-{% endblock %} {% block scripts %} - - -{% endblock %} From 7a62631e5751fcd88e1f6eec6d4f7b0864680662 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sun, 5 Mar 2023 20:32:35 +0000 Subject: [PATCH 46/58] ordering working --- .../customer-stall/customer-stall.js | 138 +++++++++++------- static/js/market.js | 2 +- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 858ed9d..ce2be6e 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -24,9 +24,7 @@ async function customerStall(path) { hasNip07: false, checkoutDialog: { show: false, - data: { - pubkey: null - } + data: {} }, qrCodeDialog: { data: { @@ -130,56 +128,96 @@ async function customerStall(path) { this.checkoutDialog.data.pubkey = pk this.checkoutDialog.data.privkey = sk }, - placeOrder() { - LNbits.utils - .confirmDialog( - `Send the order to the merchant? You should receive a message with the payment details.` - ) - .onOk(async () => { - let orderData = this.checkoutDialog.data - let content = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} - }) - } - let event = { - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: await window.nostr.nip04.encrypt( - orderData.pubkey, - content - ), - pubkey: orderData.pubkey - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - await window.nostr.signEvent(event) - } - await this.sendOrder(event) + async placeOrder() { + // LNbits.utils + // .confirmDialog( + // `Send the order to the merchant? You should receive a message with the payment details.` + // ) + // .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} }) + } + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.stall.pubkey]], + pubkey: orderData.pubkey + } + if (orderData.privkey) { + event.content = await NostrTools.nip04.encrypt( + orderData.privkey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + console.log('use extension') + event.content = await window.nostr.nip04.encrypt( + orderData.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = await NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderData) + await this.sendOrder(event) + // }) }, async sendOrder(order) { - const pool = new NostrTools.SimplePool() - let relays = Array.from(this.relays) - let pubs = await pool.publish(relays, order) - pubs.on('ok', relay => { - console.log(`${relay.url} has accepted our event`) - }) - pubs.on('failed', reason => { - console.log(`failed to publish to ${reason}`) - }) + for (const url of Array.from(this.relays)) { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } + this.checkoutDialog = {show: false, data: {}} + // const pool = new NostrTools.SimplePool() + // let relays = Array.from(this.relays) + // try { + // let pubs = await pool.publish(relays, order) + // pubs.on('ok', relay => { + // console.log(`${relay.url} has accepted our event`) + // }) + // pubs.on('failed', (reason, err) => { + // console.log(`failed to publish to ${reason}: ${err}`) + // }) + // } catch (err) { + // console.error(err) + // } } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 95bf5a6..a7c724e 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -5,7 +5,6 @@ const market = async () => { const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nostr.wine', 'wss://nostr-pub.wellorder.net', 'wss://nostr.zebedee.cloud' ] @@ -175,6 +174,7 @@ const market = async () => { return obj }) pool.close(relays) + return }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') From 7cdbc5b752a605e7734acbd732b456fe8205aa00 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 09:59:17 +0000 Subject: [PATCH 47/58] hash string --- static/js/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/js/utils.js b/static/js/utils.js index 83e886b..3e3a14e 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -25,3 +25,13 @@ function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) { ) return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio} } + +async function hash(string) { + const utf8 = new TextEncoder().encode(string) + const hashBuffer = await crypto.subtle.digest('SHA-256', utf8) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map(bytes => bytes.toString(16).padStart(2, '0')) + .join('') + return hashHex +} From 040158866897903760aed6d0a7f3ca635f637c74 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 12:03:32 +0000 Subject: [PATCH 48/58] send/receive messages --- .../customer-stall/customer-stall.html | 7 + .../customer-stall/customer-stall.js | 239 +++++++++++------- 2 files changed, 159 insertions(+), 87 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index aedec55..c6ac229 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -95,6 +95,13 @@ label="Email *optional" hint="Merchant may not use email" > +

Select the shipping zone:

{ - let orderData = this.checkoutDialog.data - let orderObj = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + address: orderData.address, + message: orderData?.message, + contact: { + nostr: this.customerPubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let created_at = Math.floor(Date.now() / 1000) + orderObj.id = await hash( + [this.customerPubkey, created_at, JSON.stringify(orderObj)].join( + ':' + ) + ) + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at, + tags: [['p', this.stall.pubkey]], + pubkey: this.customerPubkey + } + if (this.customerPrivKey) { + event.content = await NostrTools.nip04.encrypt( + this.customerPrivKey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + event.content = await window.nostr.nip04.encrypt( + this.stall.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (this.customerPrivKey) { + event.sig = await NostrTools.signEvent( + event, + this.customerPrivKey + ) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderObj) + await this.sendOrder(event) }) - } - let event = { - ...(await NostrTools.getBlankEvent()), - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', this.stall.pubkey]], - pubkey: orderData.pubkey - } - if (orderData.privkey) { - event.content = await NostrTools.nip04.encrypt( - orderData.privkey, - this.stall.pubkey, - JSON.stringify(orderObj) - ) - } else { - console.log('use extension') - event.content = await window.nostr.nip04.encrypt( - orderData.pubkey, - JSON.stringify(orderObj) - ) - let userRelays = Object.keys( - (await window.nostr?.getRelays?.()) || [] - ) - if (userRelays.length != 0) { - userRelays.map(r => this.relays.add(r)) - } - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = await NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - event = await window.nostr.signEvent(event) - } - console.log(event, orderData) - await this.sendOrder(event) - // }) }, async sendOrder(order) { for (const url of Array.from(this.relays)) { - let relay = NostrTools.relayInit(url) - relay.on('connect', () => { - console.log(`connected to ${relay.url}`) - }) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`) - }) + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) - await relay.connect() - let pub = relay.publish(order) - pub.on('ok', () => { - console.log(`${relay.url} has accepted our event`) - }) - pub.on('failed', reason => { - console.log(`failed to publish to ${relay.url}: ${reason}`) - }) + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } catch (err) { + console.error(`Error: ${err}`) + } + } + this.resetCheckout() + this.listenMessages() + }, + async listenMessages() { + try { + const pool = new NostrTools.SimplePool() + const filters = [ + { + kinds: [4], + authors: [this.customerPubkey] + }, + { + kinds: [4], + '#p': [this.customerPubkey] + } + ] + let relays = Array.from(this.relays) + let subs = pool.sub(relays, filters) + subs.on('event', async event => { + let mine = event.pubkey == this.customerPubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + if ( + (mine && sender != this.stall.pubkey) || + (!mine && sender != this.customerPubkey) + ) { + console.log(`Not relevant message!`) + return + } + try { + let plaintext = this.customerPrivKey + ? await NostrTools.nip04.decrypt( + this.customerPrivKey, + sender, + event.content + ) + : await window.nostr.nip04.decrypt(sender, event.content) + // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) + this.nostrMessages.set(event.id, { + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + return + } + }) + } catch (err) { + console.error(`Error: ${err}`) } - this.checkoutDialog = {show: false, data: {}} - // const pool = new NostrTools.SimplePool() - // let relays = Array.from(this.relays) - // try { - // let pubs = await pool.publish(relays, order) - // pubs.on('ok', relay => { - // console.log(`${relay.url} has accepted our event`) - // }) - // pubs.on('failed', (reason, err) => { - // console.log(`failed to publish to ${reason}: ${err}`) - // }) - // } catch (err) { - // console.error(err) - // } } }, created() { From 60f3edc25af20bc08a280007da6c0ea548c3e3b4 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 7 Mar 2023 13:25:16 +0000 Subject: [PATCH 49/58] login/account functionality --- static/js/market.js | 70 ++++++++++++++++- templates/nostrmarket/market.html | 120 ++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index a7c724e..88042ce 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -2,6 +2,7 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) const NostrTools = window.NostrTools + const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', @@ -23,7 +24,8 @@ const market = async () => { customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), productDetail('static/components/product-detail/product-detail.html'), - shoppingCart('static/components/shopping-cart/shopping-cart.html') + shoppingCart('static/components/shopping-cart/shopping-cart.html'), + chatDialog('static/components/chat-dialog/chat-dialog.html') ]) new Vue({ @@ -31,7 +33,15 @@ const market = async () => { mixins: [windowMixin], data: function () { return { - drawer: false, + account: null, + accountDialog: { + show: false, + data: { + watchOnly: false, + key: null + } + }, + drawer: true, pubkeys: new Set(), relays: new Set(), events: [], @@ -72,9 +82,20 @@ const market = async () => { }, isLoading() { return this.$q.loading.isActive + }, + hasExtension() { + return window.nostr + }, + isValidKey() { + return this.accountDialog.data.key + ?.toLowerCase() + ?.match(/^[0-9a-f]{64}$/) } }, async created() { + // Check for user stored + this.account = this.$q.localStorage.getItem('diagonAlley.account') || null + // Check for stored merchants and relays on localStorage try { let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) @@ -115,7 +136,10 @@ const market = async () => { } // Get notes from Nostr - await this.initNostr() + //await this.initNostr() + + // Get fiat rates (i think there's an LNbits endpoint for this) + //await this.getRates() this.$q.loading.hide() }, methods: { @@ -129,6 +153,46 @@ const market = async () => { }) console.log(naddr) }, + async deleteAccount() { + await LNbits.utils + .confirmDialog( + `This will delete all stored data. If you didn't backup the Key Pair (Private and Public Keys), you will lose it. Continue?` + ) + .onOk(() => { + window.localStorage.removeItem('diagonAlley.account') + this.account = null + }) + }, + async createAccount(useExtension = false) { + let nip07 + if (useExtension) { + await this.getFromExtension() + nip07 = true + } + if (this.isValidKey) { + let {key, watchOnly} = this.accountDialog.data + this.$q.localStorage.set('diagonAlley.account', { + privkey: watchOnly ? null : key, + pubkey: watchOnly ? key : NostrTools.getPublicKey(key), + useExtension: nip07 ?? false + }) + this.accountDialog.data = { + watchOnly: false, + key: null + } + this.accountDialog.show = false + this.account = this.$q.localStorage.getItem('diagonAlley.account') + } + }, + generateKeyPair() { + this.accountDialog.data.key = NostrTools.generatePrivateKey() + this.accountDialog.data.watchOnly = false + }, + async getFromExtension() { + this.accountDialog.data.key = await window.nostr.getPublicKey() + this.accountDialog.data.watchOnly = true + return + }, async initNostr() { this.$q.loading.show() const pool = new NostrTools.SimplePool() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 5f01c41..77c0cac 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -5,6 +5,32 @@ Settings +
+
+ + Delete account data +
+
+ Login or Create account +
+ +
+ + + + +
Account Setup
+ + +
+ +

Type your Nostr private key or generate a new one.

+ You can also use a Nostr-capable extension. +
+ + + + + + + + + + Is this a Public Key? + + If not using an Nostr capable extension, you'll have to sign + events manually! Better to use a Private Key that you can delete + later, or just generate an ephemeral key pair to use in the + Marketplace! + + + + + + + + + + +
+
{% endblock %} {% block scripts %} @@ -165,6 +247,44 @@ + + {% endblock %} From 68d36ae3a071fcb548a08a3ad7146729a940852a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:03 +0000 Subject: [PATCH 50/58] remove exchange rates --- .../customer-market/customer-market.js | 2 +- .../customer-stall/customer-stall.html | 72 +++++++-- .../customer-stall/customer-stall.js | 144 +++++++++++------- .../components/product-card/product-card.html | 3 - .../product-detail/product-detail.html | 3 - static/js/market.js | 25 +-- static/js/utils.js | 12 ++ templates/nostrmarket/market.html | 3 +- 8 files changed, 163 insertions(+), 101 deletions(-) diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index c14ffa9..45d5bea 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,7 +4,7 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates', 'change-page'], + props: ['products', 'change-page'], data: function () { return {} }, diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index c6ac229..ea63043 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,6 +10,12 @@ + @@ -64,12 +71,12 @@ filled dense readonly - hint="This your key pair! Don't lose it!" - v-if="checkoutDialog.data.privkey" - v-model="checkoutDialog.data.privkey" + type="password" + v-if="customerPrivkey" + v-model="customerPrivkey" > -
+
- Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : - finalCost + 'sats' }} - ({{ getValueInSats(finalCost) }} sats) + > -->
+ +
+ +
+
+ +
+
+ Copy invoice + Close +
+ +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index b72ba7d..4bd74c3 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -6,15 +6,16 @@ async function customerStall(path) { template, props: [ + 'account', 'stall', 'products', - 'exchange-rates', 'product-detail', 'change-page', 'relays' ], data: function () { return { + loading: false, cart: { total: 0, size: 0, @@ -23,8 +24,9 @@ async function customerStall(path) { cartMenu: [], hasNip07: false, customerPubkey: null, - customerPrivKey: null, - nostrMessages: new Map(), + customerPrivkey: null, + customerUseExtension: null, + activeOrder: null, checkoutDialog: { show: false, data: { @@ -58,12 +60,6 @@ async function customerStall(path) { changePageS(page, opts) { this.$emit('change-page', page, opts) }, - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, @@ -125,26 +121,12 @@ async function customerStall(path) { } } }, - async getPubkey() { - try { - this.customerPubkey = await window.nostr.getPublicKey() - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = null - } catch (err) { - console.error( - `Failed to get a public key from a Nostr extension: ${err}` - ) - } - }, - generateKeyPair() { - let sk = NostrTools.generatePrivateKey() - let pk = NostrTools.getPublicKey(sk) - this.customerPubkey = pk - this.customerPrivKey = sk - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = this.customerPrivKey + closeQrCodeDialog() { + this.qrCodeDialog.dismissMsg() + this.qrCodeDialog.show = false }, async placeOrder() { + this.loading = true LNbits.utils .confirmDialog( `Send the order to the merchant? You should receive a message with the payment details.` @@ -170,6 +152,7 @@ async function customerStall(path) { ':' ) ) + this.activeOrder = orderObj.id let event = { ...(await NostrTools.getBlankEvent()), kind: 4, @@ -177,13 +160,13 @@ async function customerStall(path) { tags: [['p', this.stall.pubkey]], pubkey: this.customerPubkey } - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.content = await NostrTools.nip04.encrypt( - this.customerPrivKey, + this.customerPrivkey, this.stall.pubkey, JSON.stringify(orderObj) ) - } else { + } else if (this.customerUseExtension && this.hasNip07) { event.content = await window.nostr.nip04.encrypt( this.stall.pubkey, JSON.stringify(orderObj) @@ -196,15 +179,15 @@ async function customerStall(path) { } } event.id = NostrTools.getEventHash(event) - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.sig = await NostrTools.signEvent( event, - this.customerPrivKey + this.customerPrivkey ) - } else if (this.hasNip07) { + } else if (this.customerUseExtension && this.hasNip07) { event = await window.nostr.signEvent(event) } - console.log(event, orderObj) + await this.sendOrder(event) }) }, @@ -223,25 +206,32 @@ async function customerStall(path) { let pub = relay.publish(order) pub.on('ok', () => { console.log(`${relay.url} has accepted our event`) + relay.close() }) pub.on('failed', reason => { console.log(`failed to publish to ${relay.url}: ${reason}`) + relay.close() }) } catch (err) { console.error(`Error: ${err}`) } } + this.loading = false this.resetCheckout() + this.resetCart() + this.qrCodeDialog.show = true + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for invoice from merchant...' + }) this.listenMessages() }, async listenMessages() { + console.log('LISTEN') try { const pool = new NostrTools.SimplePool() const filters = [ - { - kinds: [4], - authors: [this.customerPubkey] - }, + // / { kinds: [4], '#p': [this.customerPubkey] @@ -254,38 +244,74 @@ async function customerStall(path) { let sender = mine ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] : event.pubkey - if ( - (mine && sender != this.stall.pubkey) || - (!mine && sender != this.customerPubkey) - ) { - console.log(`Not relevant message!`) - return - } + try { - let plaintext = this.customerPrivKey - ? await NostrTools.nip04.decrypt( - this.customerPrivKey, - sender, - event.content - ) - : await window.nostr.nip04.decrypt(sender, event.content) - // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) - this.nostrMessages.set(event.id, { - msg: plaintext, - timestamp: event.created_at, - sender: `${mine ? 'Me' : 'Merchant'}` - }) + let plaintext + if (this.customerPrivkey) { + plaintext = await NostrTools.nip04.decrypt( + this.customerPrivkey, + sender, + event.content + ) + } else if (this.customerUseExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) + + // this.nostrMessages.set(event.id, { + // msg: plaintext, + // timestamp: event.created_at, + // sender: `${mine ? 'Me' : 'Merchant'}` + // }) + this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') - return } }) } catch (err) { console.error(`Error: ${err}`) } + }, + messageFilter(text, cb = () => {}) { + if (!isJson(text)) return + let json = JSON.parse(text) + if (json.id != this.activeOrder) return + if (json?.payment_options) { + // this.qrCodeDialog.show = true + this.qrCodeDialog.data.payment_request = json.payment_options.find( + o => o.type == 'ln' + ).link + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for payment...' + }) + } else if (json?.paid) { + this.qrCodeDialog.dismissMsg = this.$q.notify({ + type: 'positive', + message: 'Sats received, thanks!', + icon: 'thumb_up' + }) + this.closeQrCodeDialog() + this.activeOrder = null + Promise.resolve(cb()) + } else { + return + } } + // async mockInit() { + // this.customerPubkey = await window.nostr.getPublicKey() + // this.activeOrder = + // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' + // await this.listenMessages() + // } }, created() { + this.customerPubkey = this.account.pubkey + this.customerPrivkey = this.account.privkey + this.customerUseExtension = this.account.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5a92ba3..95f86e2 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -43,9 +43,6 @@ {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) {{ product.quantity }} left {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) { key: null } }, - drawer: true, + drawer: false, pubkeys: new Set(), relays: new Set(), events: [], @@ -49,7 +49,6 @@ const market = async () => { products: [], profiles: new Map(), searchText: null, - exchangeRates: null, inputPubkey: null, inputRelay: null, activePage: 'market', @@ -136,10 +135,8 @@ const market = async () => { } // Get notes from Nostr - //await this.initNostr() + await this.initNostr() - // Get fiat rates (i think there's an LNbits endpoint for this) - //await this.getRates() this.$q.loading.hide() }, methods: { @@ -233,23 +230,12 @@ const market = async () => { obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) pool.close(relays) return }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { let {stall, product, pubkey} = opts let url = new URL(window.location) @@ -283,13 +269,6 @@ const market = async () => { window.history.pushState({}, '', url) this.activePage = page }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, diff --git a/static/js/utils.js b/static/js/utils.js index 3e3a14e..736fc8d 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -35,3 +35,15 @@ async function hash(string) { .join('') return hashHex } + +function isJson(str) { + if (typeof str !== 'string') { + return false + } + try { + JSON.parse(str) + return true + } catch (error) { + return false + } +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 77c0cac..20a4332 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -169,15 +169,14 @@ v-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)" :products="filterProducts" - :exchange-rates="exchangeRates" :product-detail="activeProduct" :relays="relays" + :account="account" @change-page="navigateTo" > From 1e9b138297f41792fa65d76cef674b8ec9adb544 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:15 +0000 Subject: [PATCH 51/58] chat dialog component --- .../components/chat-dialog/chat-dialog.html | 89 +++++++++++ static/components/chat-dialog/chat-dialog.js | 143 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 static/components/chat-dialog/chat-dialog.html create mode 100644 static/components/chat-dialog/chat-dialog.js diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html new file mode 100644 index 0000000..9ab5296 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.html @@ -0,0 +1,89 @@ +
+ + + + + +
Chat Box
+ + + + Minimize + + + Maximize + + + Close + +
+ + +
+
+ +
+ +
+
+ + + + + + + +
+
+
+
+
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js new file mode 100644 index 0000000..96f82f6 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.js @@ -0,0 +1,143 @@ +async function chatDialog(path) { + const template = await loadTemplateAsync(path) + + Vue.component('chat-dialog', { + name: 'chat-dialog', + template, + + props: ['account', 'merchant', 'relays'], + data: function () { + return { + dialog: false, + maximizedToggle: true, + pool: null, + nostrMessages: [], + newMessage: '' + } + }, + computed: { + sortedMessages() { + return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + } + }, + methods: { + async startPool() { + let sub = this.pool.sub(Array.from(this.relays), [ + { + kinds: [4], + authors: [this.account.pubkey] + }, + { + kinds: [4], + '#p': [this.account.pubkey] + } + ]) + sub.on('event', async event => { + let mine = event.pubkey == this.account.pubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + + try { + let plaintext + if (this.account.privkey) { + plaintext = await NostrTools.nip04.decrypt( + this.account.privkey, + sender, + event.content + ) + } else if (this.account.useExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + this.nostrMessages.push({ + id: event.id, + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + } + }) + }, + async sendMessage() { + if (this.newMessage && this.newMessage.length < 1) return + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.merchant]], + pubkey: this.account.pubkey, + content: await this.encryptMsg() + } + event.id = NostrTools.getEventHash(event) + event.sig = this.signEvent(event) + for (const url of Array.from(this.relays)) { + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.debug(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.debug(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(event) + pub.on('ok', () => { + console.debug(`${relay.url} has accepted our event`) + relay.close() + }) + pub.on('failed', reason => { + console.debug(`failed to publish to ${relay.url}: ${reason}`) + relay.close() + }) + this.newMessage = '' + } catch (e) { + console.error(e) + } + } + }, + async encryptMsg() { + try { + let cypher + if (this.account.privkey) { + cypher = await NostrTools.nip04.encrypt( + this.account.privkey, + this.merchant, + this.newMessage + ) + } else if (this.account.useExtension && this.hasNip07) { + cypher = await window.nostr.nip04.encrypt( + this.merchant, + this.newMessage + ) + } + return cypher + } catch (e) { + console.error(e) + } + }, + async signEvent(event) { + if (this.account.privkey) { + event.sig = await NostrTools.signEvent(event, this.account.privkey) + } else if (this.account.useExtension && this.hasNip07) { + event = await window.nostr.signEvent(event) + } + return event + } + }, + created() { + this.pool = new NostrTools.SimplePool() + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + this.startPool() + } + }) +} From 9acc67b2ce780defe0bd3c19c6b0b7d768297a64 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 11:23:51 +0000 Subject: [PATCH 52/58] clean up --- static/components/customer-stall/customer-stall.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index ea63043..5e2d085 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -121,9 +121,6 @@
Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost, stall.currency) : finalCost + 'sats' }} -
Date: Wed, 8 Mar 2023 11:24:11 +0000 Subject: [PATCH 53/58] fix add merchant pubkey --- static/js/market.js | 6 +++++- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0077344..0d823e9 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,6 +222,7 @@ const market = async () => { }) }) await Promise.resolve(sub) + this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -272,7 +273,8 @@ const market = async () => { getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - async addPubkey(pubkey = null) { + async addPubkey(pubkey) { + console.log(pubkey, this.inputPubkey) if (!pubkey) { pubkey = String(this.inputPubkey).trim() } @@ -285,6 +287,7 @@ const market = async () => { pubkey = data.pubkey givenRelays = data.relays } + console.log(pubkey) this.pubkeys.add(pubkey) this.inputPubkey = null } catch (err) { @@ -310,6 +313,7 @@ const market = async () => { `diagonAlley.merchants`, Array.from(this.pubkeys) ) + Promise.resolve(this.initNostr()) }, async addRelay() { let relay = String(this.inputRelay).trim() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 20a4332..a5ebeb3 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -44,12 +44,12 @@ - + From 2a652643c47d1e6608b76752c1ff607b078e35c2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:56:39 +0000 Subject: [PATCH 54/58] better-ish chat box --- .../components/chat-dialog/chat-dialog.html | 118 ++++++++---------- static/components/chat-dialog/chat-dialog.js | 81 +++++++++++- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 9ab5296..fd87273 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -1,9 +1,9 @@ -
- +
+ @@ -13,77 +13,57 @@
Chat Box
- - Minimize - - - Maximize - - + + Close - -
-
- -
- -
-
- - - - - - - -
+ + + + + + + + + + + +
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index 96f82f6..e89e26d 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -9,7 +9,7 @@ async function chatDialog(path) { data: function () { return { dialog: false, - maximizedToggle: true, + loading: false, pool: null, nostrMessages: [], newMessage: '' @@ -17,11 +17,22 @@ async function chatDialog(path) { }, computed: { sortedMessages() { - return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + return this.nostrMessages.sort((a, b) => b.timestamp - a.timestamp) } }, methods: { + async startDialog() { + this.dialog = true + await this.startPool() + }, + async closeDialog() { + this.dialog = false + await this.pool.close(Array.from(this.relays)) + }, async startPool() { + this.loading = true + this.pool = new NostrTools.SimplePool() + let messagesMap = new Map() let sub = this.pool.sub(Array.from(this.relays), [ { kinds: [4], @@ -32,6 +43,10 @@ async function chatDialog(path) { '#p': [this.account.pubkey] } ]) + sub.on('eose', () => { + this.loading = false + this.nostrMessages = Array.from(messagesMap.values()) + }) sub.on('event', async event => { let mine = event.pubkey == this.account.pubkey let sender = mine @@ -52,8 +67,7 @@ async function chatDialog(path) { event.content ) } - this.nostrMessages.push({ - id: event.id, + messagesMap.set(event.id, { msg: plaintext, timestamp: event.created_at, sender: `${mine ? 'Me' : 'Merchant'}` @@ -62,6 +76,10 @@ async function chatDialog(path) { console.error('Unable to decrypt message!') } }) + setTimeout(() => { + this.nostrMessages = Array.from(messagesMap.values()) + this.loading = false + }, 5000) }, async sendMessage() { if (this.newMessage && this.newMessage.length < 1) return @@ -128,16 +146,67 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event + }, + timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` } }, created() { - this.pool = new NostrTools.SimplePool() setTimeout(() => { if (window.nostr) { this.hasNip07 = true } }, 1000) - this.startPool() } }) } From 9f1d11d4518a933bca8736950253cc7176b2a497 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:16 +0000 Subject: [PATCH 55/58] payment dialog flow --- .../customer-stall/customer-stall.html | 16 +++++++--------- .../components/customer-stall/customer-stall.js | 17 ++--------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 5e2d085..1e06b29 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -7,7 +7,10 @@ @click="$emit('change-page', 'market')" style="cursor: pointer" > - + - -
- -
-
+ + +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 4bd74c3..58ab910 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -231,7 +231,6 @@ async function customerStall(path) { try { const pool = new NostrTools.SimplePool() const filters = [ - // / { kinds: [4], '#p': [this.customerPubkey] @@ -261,11 +260,6 @@ async function customerStall(path) { } console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) - // this.nostrMessages.set(event.id, { - // msg: plaintext, - // timestamp: event.created_at, - // sender: `${mine ? 'Me' : 'Merchant'}` - // }) this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') @@ -280,7 +274,6 @@ async function customerStall(path) { let json = JSON.parse(text) if (json.id != this.activeOrder) return if (json?.payment_options) { - // this.qrCodeDialog.show = true this.qrCodeDialog.data.payment_request = json.payment_options.find( o => o.type == 'ln' ).link @@ -289,24 +282,18 @@ async function customerStall(path) { message: 'Waiting for payment...' }) } else if (json?.paid) { - this.qrCodeDialog.dismissMsg = this.$q.notify({ + this.closeQrCodeDialog() + this.$q.notify({ type: 'positive', message: 'Sats received, thanks!', icon: 'thumb_up' }) - this.closeQrCodeDialog() this.activeOrder = null Promise.resolve(cb()) } else { return } } - // async mockInit() { - // this.customerPubkey = await window.nostr.getPublicKey() - // this.activeOrder = - // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' - // await this.listenMessages() - // } }, created() { this.customerPubkey = this.account.pubkey From fe0fcfdc9f39283453e4965accd556896245c763 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:34 +0000 Subject: [PATCH 56/58] tweaks --- static/js/market.js | 2 +- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0d823e9..3cf9b90 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,7 +222,6 @@ const market = async () => { }) }) await Promise.resolve(sub) - this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -234,6 +233,7 @@ const market = async () => { } return obj }) + this.$q.loading.hide() pool.close(relays) return }, diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a5ebeb3..baa9715 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -57,7 +57,7 @@ {{ profiles.get(pub).name }} Date: Wed, 8 Mar 2023 21:05:53 +0000 Subject: [PATCH 57/58] brush up checkout dialog --- .../components/chat-dialog/chat-dialog.html | 1 - .../customer-stall/customer-stall.html | 94 +++++++++++++++---- .../customer-stall/customer-stall.js | 25 ++++- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index fd87273..1e46a4d 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -2,7 +2,6 @@
@@ -62,35 +62,77 @@ v-model.trim="checkoutDialog.data.username" label="Name *optional" > + + + + It seems you haven't logged in. You can: +
    +
  1. + enter your public and private keys bellow (to sign the order + message) +
  2. +
  3. use a Nostr Signer Extension (NIP07)
  4. +
  5. + fill out the required fields, without keys, and download the + order and send as a direct message to the merchant on any + Nostr client +
  6. +
+
+ + Use a Nostr browser extension + Download the order and send manually + +
+
+ -
Download Order + Checkout @@ -154,7 +208,7 @@ position="top" @hide="closeQrCodeDialog" > - +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 58ab910..b417c91 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -16,6 +16,7 @@ async function customerStall(path) { data: function () { return { loading: false, + isPwd: true, cart: { total: 0, size: 0, @@ -113,6 +114,24 @@ async function customerStall(path) { products: new Map() } }, + async downloadOrder() { + return + }, + async getFromExtension() { + this.customerPubkey = await window.nostr.getPublicKey() + this.customerUseExtension = true + this.checkoutDialog.data.pubkey = this.customerPubkey + }, + openCheckout() { + // Check if user is logged in + if (this.customerPubkey) { + this.checkoutDialog.data.pubkey = this.customerPubkey + if (this.customerPrivkey && !useExtension) { + this.checkoutDialog.data.privkey = this.customerPrivkey + } + } + this.checkoutDialog.show = true + }, resetCheckout() { this.checkoutDialog = { show: false, @@ -296,9 +315,9 @@ async function customerStall(path) { } }, created() { - this.customerPubkey = this.account.pubkey - this.customerPrivkey = this.account.privkey - this.customerUseExtension = this.account.useExtension + this.customerPubkey = this.account?.pubkey + this.customerPrivkey = this.account?.privkey + this.customerUseExtension = this.account?.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true From bed734e9b424f80d452a53bca912913a31a52ece Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 9 Mar 2023 10:00:55 +0000 Subject: [PATCH 58/58] fixing vlad's review comments --- .../components/chat-dialog/chat-dialog.html | 2 +- static/components/chat-dialog/chat-dialog.js | 57 +------------------ static/js/market.js | 9 +-- static/js/utils.js | 54 ++++++++++++++++++ views.py | 14 +---- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 1e46a4d..a428044 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -33,7 +33,7 @@ :text="[message.msg]" :sent="message.sender == 'Me'" :bg-color="message.sender == 'Me' ? 'white' : 'light-green-2'" - :stamp="`${timeFromNow(message.timestamp * 1000)}`" + :stamp="message.timestamp" size="6" /> diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index e89e26d..c111e27 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -69,7 +69,7 @@ async function chatDialog(path) { } messagesMap.set(event.id, { msg: plaintext, - timestamp: event.created_at, + timestamp: timeFromNow(event.created_at * 1000), sender: `${mine ? 'Me' : 'Merchant'}` }) } catch { @@ -107,11 +107,9 @@ async function chatDialog(path) { let pub = relay.publish(event) pub.on('ok', () => { console.debug(`${relay.url} has accepted our event`) - relay.close() }) pub.on('failed', reason => { console.debug(`failed to publish to ${relay.url}: ${reason}`) - relay.close() }) this.newMessage = '' } catch (e) { @@ -146,59 +144,6 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event - }, - timeFromNow(time) { - // Get timestamps - let unixTime = new Date(time).getTime() - if (!unixTime) return - let now = new Date().getTime() - - // Calculate difference - let difference = unixTime / 1000 - now / 1000 - - // Setup return object - let tfn = {} - - // Check if time is in the past, present, or future - tfn.when = 'now' - if (difference > 0) { - tfn.when = 'future' - } else if (difference < -1) { - tfn.when = 'past' - } - - // Convert difference to absolute - difference = Math.abs(difference) - - // Calculate time unit - if (difference / (60 * 60 * 24 * 365) > 1) { - // Years - tfn.unitOfTime = 'years' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) - } else if (difference / (60 * 60 * 24 * 45) > 1) { - // Months - tfn.unitOfTime = 'months' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) - } else if (difference / (60 * 60 * 24) > 1) { - // Days - tfn.unitOfTime = 'days' - tfn.time = Math.floor(difference / (60 * 60 * 24)) - } else if (difference / (60 * 60) > 1) { - // Hours - tfn.unitOfTime = 'hours' - tfn.time = Math.floor(difference / (60 * 60)) - } else if (difference / 60 > 1) { - // Minutes - tfn.unitOfTime = 'minutes' - tfn.time = Math.floor(difference / 60) - } else { - // Seconds - tfn.unitOfTime = 'seconds' - tfn.time = Math.floor(difference) - } - - // Return time from now data - return `${tfn.time} ${tfn.unitOfTime}` } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 3cf9b90..52fed25 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -62,12 +62,13 @@ const market = async () => { if (this.activeStall) { products = products.filter(p => p.stall_id == this.activeStall) } - if (!this.searchText || this.searchText.length < 2) return products + const searchText = this.searchText.toLowerCase() + if (!searchText || searchText.length < 2) return products return products.filter(p => { return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) + p.name.toLowerCase().includes(searchText) || + p.description.toLowerCase().includes(searchText) || + p.categories.toLowerCase().includes(searchText) ) }) }, diff --git a/static/js/utils.js b/static/js/utils.js index 736fc8d..86c4d00 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -47,3 +47,57 @@ function isJson(str) { return false } } + +function timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` +} diff --git a/views.py b/views.py index d83bc85..3b757fb 100644 --- a/views.py +++ b/views.py @@ -23,18 +23,8 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) -async def market( - request: Request, - stall_id: str = Query(None), - product_id: str = Query(None), - merchant_pubkey: str = Query(None), -): +async def market(request: Request): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - { - "request": request, - "stall_id": stall_id, - "product_id": product_id, - "merchant_pubkey": merchant_pubkey, - }, + {"request": request}, )