Merge pull request #14 from lnbits/search-nostr

initial drafting of nostr global search for products
This commit is contained in:
Vlad Stan 2023-03-13 22:26:19 +02:00 committed by GitHub
commit de1a13ca27
5 changed files with 173 additions and 31 deletions

View file

@ -1,4 +1,53 @@
<div> <div>
<div v-if="searchNostr" class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<!-- <q-card-section class="row items-center q-pb-none">
<div class="text-h6">Search Globally</div>
<q-space />
<q-btn
icon="close"
flat
round
dense
@click="searchNostr = !searchNostr"
/>
</q-card-section> -->
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Search Globally</h5>
<p>Search for products on Nostr</p>
</div>
</div>
</q-card-section>
<q-card-section>
<q-input
class="q-ml-md"
standout
square
dense
outlined
clearable
v-model.trim="search"
label="Search products"
@keydown.enter="searchProducts"
hint="Enter search terms separated by spaces"
>
<template v-slot:after>
<q-btn
dense
flat
icon="search"
label="Search"
@click="searchProducts"
/>
</template>
</q-input>
</q-card-section>
</q-card>
</div>
</div>
<q-toolbar> <q-toolbar>
<q-breadcrumbs> <q-breadcrumbs>
<q-breadcrumbs-el label="Market" icon="home"></q-breadcrumbs-el> <q-breadcrumbs-el label="Market" icon="home"></q-breadcrumbs-el>

View file

@ -4,13 +4,49 @@ async function customerMarket(path) {
name: 'customer-market', name: 'customer-market',
template, template,
props: ['products', 'change-page'], props: [
'products',
'change-page',
'search-nostr',
'relays',
'update-products',
'update-stalls'
],
data: function () { data: function () {
return {} return {
search: null
}
}, },
methods: { methods: {
changePageM(page, opts) { changePageM(page, opts) {
this.$emit('change-page', page, opts) this.$emit('change-page', page, opts)
},
async searchProducts() {
this.$q.loading.show()
let searchTags = this.search.split(' ')
const pool = new NostrTools.SimplePool()
let relays = Array.from(this.relays)
let merchants = new Set()
let productEvents = await pool.list(relays, [
{
kinds: [30018],
'#t': searchTags,
search: this.search, // NIP50, not very well supported
limit: 100
}
])
productEvents.map(e => merchants.add(e.pubkey))
let stallEvents = await pool.list(relays, [
{
kinds: [30017],
authors: Array.from(merchants)
}
])
pool.close(relays)
await this.$emit('update-data', [...stallEvents, ...productEvents])
this.$q.loading.hide()
} }
}, },
created() {} created() {}

View file

@ -60,12 +60,12 @@
label="Add to cart" label="Add to cart"
@click="$emit('add-to-cart', product)" @click="$emit('add-to-cart', product)"
/> />
<q-btn <!-- <q-btn
class="q-mt-md q-ml-md" class="q-mt-md q-ml-md"
color="primary" color="primary"
icon="share" icon="share"
label="Share" label="Share"
/> /> -->
</div> </div>
</div> </div>
<!-- RATING TO BE DONE --> <!-- RATING TO BE DONE -->

View file

@ -10,7 +10,7 @@ const market = async () => {
'wss://nostr.zebedee.cloud' 'wss://nostr.zebedee.cloud'
] ]
const eventToObj = event => { const eventToObj = event => {
event.content = JSON.parse(event.content) event.content = JSON.parse(event.content) || null
return { return {
...event, ...event,
...Object.values(event.tags).reduce((acc, tag) => { ...Object.values(event.tags).reduce((acc, tag) => {
@ -34,6 +34,7 @@ const market = async () => {
data: function () { data: function () {
return { return {
account: null, account: null,
accountMetadata: null,
accountDialog: { accountDialog: {
show: false, show: false,
data: { data: {
@ -41,6 +42,7 @@ const market = async () => {
key: null key: null
} }
}, },
searchNostr: false,
drawer: false, drawer: false,
pubkeys: new Set(), pubkeys: new Set(),
relays: new Set(), relays: new Set(),
@ -62,13 +64,15 @@ const market = async () => {
if (this.activeStall) { if (this.activeStall) {
products = products.filter(p => p.stall_id == 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() const searchText = this.searchText.toLowerCase()
if (!searchText || searchText.length < 2) return products
return products.filter(p => { return products.filter(p => {
return ( return (
p.name.toLowerCase().includes(searchText) || p.name.toLowerCase().includes(searchText) ||
p.description.toLowerCase().includes(searchText) || (p.description &&
p.categories.toLowerCase().includes(searchText) p.description.toLowerCase().includes(searchText)) ||
(p.categories &&
p.categories.toString().toLowerCase().includes(searchText))
) )
}) })
}, },
@ -87,9 +91,12 @@ const market = async () => {
return window.nostr return window.nostr
}, },
isValidKey() { isValidKey() {
return this.accountDialog.data.key let key = this.accountDialog.data.key
?.toLowerCase() if (key && key.startsWith('n')) {
?.match(/^[0-9a-f]{64}$/) let {type, data} = NostrTools.nip19.decode(key)
key = data
}
return key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)
} }
}, },
async created() { async created() {
@ -103,6 +110,9 @@ const market = async () => {
if (merchants && merchants.length) { if (merchants && merchants.length) {
this.pubkeys = new Set(merchants) this.pubkeys = new Set(merchants)
} }
if (this.account) {
this.pubkeys.add(this.account.pubkey)
}
if (relays && relays.length) { if (relays && relays.length) {
this.relays = new Set([...defaultRelays, ...relays]) this.relays = new Set([...defaultRelays, ...relays])
} else { } else {
@ -141,16 +151,6 @@ const market = async () => {
this.$q.loading.hide() this.$q.loading.hide()
}, },
methods: { methods: {
naddr() {
let naddr = NostrTools.nip19.naddrEncode({
identifier: '1234',
pubkey:
'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796',
kind: 30018,
relays: defaultRelays
})
console.log(naddr)
},
async deleteAccount() { async deleteAccount() {
await LNbits.utils await LNbits.utils
.confirmDialog( .confirmDialog(
@ -169,6 +169,10 @@ const market = async () => {
} }
if (this.isValidKey) { if (this.isValidKey) {
let {key, watchOnly} = this.accountDialog.data let {key, watchOnly} = this.accountDialog.data
if (key.startsWith('n')) {
let {type, data} = NostrTools.nip19.decode(key)
key = data
}
this.$q.localStorage.set('diagonAlley.account', { this.$q.localStorage.set('diagonAlley.account', {
privkey: watchOnly ? null : key, privkey: watchOnly ? null : key,
pubkey: watchOnly ? key : NostrTools.getPublicKey(key), pubkey: watchOnly ? key : NostrTools.getPublicKey(key),
@ -191,6 +195,36 @@ const market = async () => {
this.accountDialog.data.watchOnly = true this.accountDialog.data.watchOnly = true
return return
}, },
async updateData(events) {
let products = new Map()
let stalls = new Map()
this.stalls.forEach(s => stalls.set(s.id, s))
this.products.forEach(p => products.set(p.id, p))
events.map(eventToObj).map(e => {
if (e.kind == 30018) {
//it's a product `d` is the prod. id
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[0], pubkey: e.pubkey})
return
}
})
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
obj.images = [obj.image]
if (obj.currency != 'sat') {
obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency)
}
return obj
})
},
async initNostr() { async initNostr() {
this.$q.loading.show() this.$q.loading.show()
const pool = new NostrTools.SimplePool() const pool = new NostrTools.SimplePool()
@ -205,12 +239,14 @@ const market = async () => {
authors: Array.from(this.pubkeys) authors: Array.from(this.pubkeys)
} }
]) ])
.then(events => { .then(async events => {
console.log(events)
this.events = events || [] this.events = events || []
this.events.map(eventToObj).map(e => { this.events.map(eventToObj).map(e => {
if (e.kind == 0) { if (e.kind == 0) {
this.profiles.set(e.pubkey, e.content) this.profiles.set(e.pubkey, e.content)
if (e.pubkey == this.account?.pubkey) {
this.accountMetadata = this.profiles.get(this.account.pubkey)
}
return return
} else if (e.kind == 30018) { } else if (e.kind == 30018) {
//it's a product `d` is the prod. id //it's a product `d` is the prod. id

View file

@ -7,11 +7,19 @@
</q-toolbar> </q-toolbar>
<div> <div>
<div v-if="account" class="bg-transparent q-ma-md"> <div v-if="account" class="bg-transparent q-ma-md">
<!-- <q-avatar size="56px" class="q-mb-sm"> <q-avatar size="56px" class="q-mb-sm">
<img src="https://cdn.quasar.dev/img/boy-avatar.png" /> <img
:src="accountMetadata?.picture || '/nostrmarket/static/images/blank-avatar.webp'"
/>
</q-avatar> </q-avatar>
<div class="text-weight-bold">Razvan Stoenescu</div> {%raw%}
<div>@rstoenescu</div> --> <div class="text-weight-bold">
{{ `${account.pubkey.slice(0, 5)}...${account.pubkey.slice(-5)}` }}
</div>
<div v-if="accountMetadata && accountMetadata.name">
{{ accountMetadata.name }}
</div>
{%endraw%}
<q-btn <q-btn
label="Delete data" label="Delete data"
class="q-mt-md" class="q-mt-md"
@ -148,15 +156,24 @@
{{ activePage }} {{ activePage }}
</q-toolbar-title> </q-toolbar-title>
{%endraw%} {%endraw%}
<q-space></q-space>
<q-btn
v-if="!activeStall"
color="primary"
label="Search"
icon="travel_explore"
@click="searchNostr = !searchNostr"
><q-tooltip>Search for products on Nostr</q-tooltip></q-btn
>
<q-input <q-input
class="float-left q-ml-md" class="q-ml-md"
standout standout
square square
dense dense
outlined outlined
clearable clearable
v-model.trim="searchText" v-model.trim="searchText"
label="Search for products" label="Filter products"
> >
<template v-slot:append> <template v-slot:append>
<q-icon v-if="!searchText" name="search" /> <q-icon v-if="!searchText" name="search" />
@ -176,8 +193,11 @@
></customer-stall> ></customer-stall>
<customer-market <customer-market
v-else v-else
:search-nostr="searchNostr"
:relays="relays"
:products="filterProducts" :products="filterProducts"
@change-page="navigateTo" @change-page="navigateTo"
@update-data="updateData"
></customer-market> ></customer-market>
</q-page-container> </q-page-container>
<!-- ACCOUNT DIALOG --> <!-- ACCOUNT DIALOG -->
@ -196,11 +216,12 @@
<q-card-section class="q-pt-none"> <q-card-section class="q-pt-none">
<q-input <q-input
dense dense
label="Private key (hex)" label="nsec/npub/hex"
v-model="accountDialog.data.key" v-model="accountDialog.data.key"
autofocus autofocus
@keyup.enter="createAccount" @keyup.enter="createAccount"
:error="!isValidKey" :error="accountDialog.data.key && !isValidKey"
hint="Enter you private key (recommended) or public key"
></q-input> ></q-input>
<q-item tag="label"> <q-item tag="label">