it was being sent to the relays, just not the correct kind. it's fixed now on v.0.5.6
it's already released, as of last night!!
update the LNURL pay extension on your nostr:npub10efcj7x65z2ak6vd69xr8f2hvqwuaqrhlygl3yqa4y63hfvc02mqwzaeh3 and all should be fixed + the receipt on webhook.
why are pulling follows follows?
pull your follows, and add others on a need basis, or even pull on the fly when needed then discard/keep in memory
First iteration of a standalone, very lite and naive #nostr client
https://github.com/talvasconcelos/litestr
To make it even more cypherpunk, just copy the code bellow to an HTML file and open it on your browser!
```
name="description"
content="Nostr standalone local client to read notes (WIP)"
/>
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
- NOSTR Client
Add you Public Key
type="text"
name="pubkey"
placeholder="npub..."
aria-label="Pubkey"
/>
>
const nostr = NostrTools
const notesSection = document.getElementById('notes')
const pool = new nostr.SimplePool()
let relays = new Set([
'wss://relay.nostr.band/',
'wss://nostr-pub.wellorder.net/',
'wss://relay.damus.io/'
])
// timestamp a year ago
let aYearAgo = 365 * 24 * 60 * 60
let since = parseInt((Date.now() - aYearAgo) / 1000) || 0
let pk = localStorage.getItem('pk') || null
let npub = null
let follows = new Set()
let ev = new Map()
let profiles = new Map()
if (pk) {
pk = sanitizePK(pk)
getInitial().then(async () => {
await getNotes()
subscribeToRelays()
})
}
async function getInitial() {
console.log('Getting initial')
notesSection.ariaBusy = true
let events = await pool.querySync([...relays], {
kinds: [0, 3, 10002],
authors: [pk]
})
events.forEach(event => {
if (event.kind == 0) {
profiles.set(event.id, event)
}
if (event.kind == 3) {
event.tags.forEach(tag => {
if (tag[0] == ['p']) {
follows.add(tag[1])
}
})
}
if (event.kind == 10002) {
event.tags.forEach(tag => {
if (tag[0] == ['r'] && tag[2] != 'write') {
relays.add(tag[1])
}
})
}
})
const _profiles = await pool.querySync([...relays], {
kinds: [0],
authors: [...follows]
})
_profiles.forEach(profile => {
profiles.set(profile.pubkey, profile)
})
notesSection.ariaBusy = false
}
function subscribeToRelays() {
const events = [...ev.values()] || []
const _since =
events.sort((a, b) => b.created_at - a.created_at)[0].created_at || 0
console.log('Since:', since)
pool.subscribeMany(
Array.from(relays),
[
{
authors: Array.from(follows),
kinds: [1],
since: _since
}
],
{
onevent(event) {
console.log('Got an event')
if (event.kind == 1) {
// check if the event is already in the map
if (ev.has(event.id)) return
ev.set(event.id, event)
addNote(event)
}
}
}
)
}
async function getNotes() {
console.log('Getting notes', since)
notesSection.ariaBusy = true
let events = await pool.querySync([...relays], {
kinds: [1],
authors: [...follows],
since
})
events
.sort((a, b) => b.created_at - a.created_at)
.forEach(event => {
ev.set(event.id, event)
})
notesSection.ariaBusy = false
renderNotes()
}
function sanitizePK(pk) {
if (pk.startsWith('npub')) {
npub = pk
let {type, data} = nostr.nip19.decode(npub)
pk = data
} else {
npub = nostr.nip19.npubEncode(pk)
}
follows.add(pk) // follow self
return pk
}
function renderNotes() {
notesSection.innerHTML = ''
ev.forEach(event => {
if (event.kind === 1) {
notesSection.appendChild(noteTemplate(event))
}
})
}
function addNote(event) {
notesSection.prepend(noteTemplate(event))
}
// Modal
const isOpenClass = 'modal-is-open'
const openingClass = 'modal-is-opening'
const closingClass = 'modal-is-closing'
const scrollbarWidthCssVar = '--pico-scrollbar-width'
const animationDuration = 400 // ms
const inputPK = document.getElementsByName('pubkey')[0]
let visibleModal = null
// Toggle modal
const toggleModal = event => {
event.preventDefault()
const modal = document.getElementById(
event.currentTarget.dataset.target
)
if (!modal) return
modal && (modal.open ? closeModal(modal) : openModal(modal))
}
// Open modal
const openModal = modal => {
const {documentElement: html} = document
if (pk) {
inputPK.value = pk
}
const scrollbarWidth = getScrollbarWidth()
if (scrollbarWidth) {
html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`)
}
html.classList.add(isOpenClass, openingClass)
setTimeout(() => {
visibleModal = modal
html.classList.remove(openingClass)
}, animationDuration)
modal.showModal()
}
// Close modal
const closeModal = modal => {
visibleModal = null
const {documentElement: html} = document
html.classList.add(closingClass)
if (inputPK.value !== '') {
pk = sanitizePK(inputPK.value)
localStorage.setItem('pk', pk)
inputPK.value = ''
}
setTimeout(() => {
html.classList.remove(closingClass, isOpenClass)
html.style.removeProperty(scrollbarWidthCssVar)
modal.close()
}, animationDuration)
pk &&
Promise.resolve(getInitial()).then(async () => {
await getNotes()
subscribeToRelays()
})
}
// Close with a click outside
document.addEventListener('click', event => {
if (visibleModal === null) return
const modalContent = visibleModal.querySelector('article')
const isClickInside = modalContent.contains(event.target)
!isClickInside && closeModal(visibleModal)
})
// Close with Esc key
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && visibleModal) {
closeModal(visibleModal)
}
})
// Get scrollbar width
const getScrollbarWidth = () => {
const scrollbarWidth =
window.innerWidth - document.documentElement.clientWidth
return scrollbarWidth
}
// Is scrollbar visible
const isScrollbarVisible = () => {
return document.body.scrollHeight > screen.height
}
// Templates
const getProfile = id => {
try {
const profile = profiles.get(id)
if (!profile) return id
console.log('Profile:', profile)
const content = JSON.parse(profile?.content)
console.log('Content:', content)
const name = content?.name || id
return name
} catch (error) {
return id
}
}
const noteTemplate = event => {
let note = document.createElement('article')
note.whiteSpace = 'preserve'
note.innerHTML = `
${getProfile(event.pubkey)}
${event.content}
event.created_at * 1000
).toLocaleString()}
`
return note
}
```
First iteration of a standalone, very lite and naive #nostr client
https://github.com/talvasconcelos/litestr
To make it even more cypherpunk, just copy the code bellow to an HTML file and open it on your browser!
```
name="description"
content="Nostr standalone local client to read notes (WIP)"
/>
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
- NOSTR Client
Add you Public Key
type="text"
name="pubkey"
placeholder="npub..."
aria-label="Pubkey"
/>
>
const nostr = NostrTools
const notesSection = document.getElementById('notes')
const pool = new nostr.SimplePool()
let relays = new Set([
'wss://relay.nostr.band/',
'wss://nostr-pub.wellorder.net/',
'wss://relay.damus.io/'
])
// timestamp a year ago
let aYearAgo = 365 * 24 * 60 * 60
let since = parseInt((Date.now() - aYearAgo) / 1000) || 0
let pk = localStorage.getItem('pk') || null
let npub = null
let follows = new Set()
let ev = new Map()
let profiles = new Map()
if (pk) {
pk = sanitizePK(pk)
getInitial().then(async () => {
await getNotes()
subscribeToRelays()
})
}
async function getInitial() {
console.log('Getting initial')
notesSection.ariaBusy = true
let events = await pool.querySync([...relays], {
kinds: [0, 3, 10002],
authors: [pk]
})
events.forEach(event => {
if (event.kind == 0) {
profiles.set(event.id, event)
}
if (event.kind == 3) {
event.tags.forEach(tag => {
if (tag[0] == ['p']) {
follows.add(tag[1])
}
})
}
if (event.kind == 10002) {
event.tags.forEach(tag => {
if (tag[0] == ['r'] && tag[2] != 'write') {
relays.add(tag[1])
}
})
}
})
const _profiles = await pool.querySync([...relays], {
kinds: [0],
authors: [...follows]
})
_profiles.forEach(profile => {
profiles.set(profile.pubkey, profile)
})
notesSection.ariaBusy = false
}
function subscribeToRelays() {
const events = [...ev.values()] || []
const _since =
events.sort((a, b) => b.created_at - a.created_at)[0].created_at || 0
console.log('Since:', since)
pool.subscribeMany(
Array.from(relays),
[
{
authors: Array.from(follows),
kinds: [1],
since: _since
}
],
{
onevent(event) {
console.log('Got an event')
if (event.kind == 1) {
// check if the event is already in the map
if (ev.has(event.id)) return
ev.set(event.id, event)
addNote(event)
}
}
}
)
}
async function getNotes() {
console.log('Getting notes', since)
notesSection.ariaBusy = true
let events = await pool.querySync([...relays], {
kinds: [1],
authors: [...follows],
since
})
events
.sort((a, b) => b.created_at - a.created_at)
.forEach(event => {
ev.set(event.id, event)
})
notesSection.ariaBusy = false
renderNotes()
}
function sanitizePK(pk) {
if (pk.startsWith('npub')) {
npub = pk
let {type, data} = nostr.nip19.decode(npub)
pk = data
} else {
npub = nostr.nip19.npubEncode(pk)
}
follows.add(pk) // follow self
return pk
}
function renderNotes() {
notesSection.innerHTML = ''
ev.forEach(event => {
if (event.kind === 1) {
notesSection.appendChild(noteTemplate(event))
}
})
}
function addNote(event) {
notesSection.prepend(noteTemplate(event))
}
// Modal
const isOpenClass = 'modal-is-open'
const openingClass = 'modal-is-opening'
const closingClass = 'modal-is-closing'
const scrollbarWidthCssVar = '--pico-scrollbar-width'
const animationDuration = 400 // ms
const inputPK = document.getElementsByName('pubkey')[0]
let visibleModal = null
// Toggle modal
const toggleModal = event => {
event.preventDefault()
const modal = document.getElementById(
event.currentTarget.dataset.target
)
if (!modal) return
modal && (modal.open ? closeModal(modal) : openModal(modal))
}
// Open modal
const openModal = modal => {
const {documentElement: html} = document
if (pk) {
inputPK.value = pk
}
const scrollbarWidth = getScrollbarWidth()
if (scrollbarWidth) {
html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`)
}
html.classList.add(isOpenClass, openingClass)
setTimeout(() => {
visibleModal = modal
html.classList.remove(openingClass)
}, animationDuration)
modal.showModal()
}
// Close modal
const closeModal = modal => {
visibleModal = null
const {documentElement: html} = document
html.classList.add(closingClass)
if (inputPK.value !== '') {
pk = sanitizePK(inputPK.value)
localStorage.setItem('pk', pk)
inputPK.value = ''
}
setTimeout(() => {
html.classList.remove(closingClass, isOpenClass)
html.style.removeProperty(scrollbarWidthCssVar)
modal.close()
}, animationDuration)
pk &&
Promise.resolve(getInitial()).then(async () => {
await getNotes()
subscribeToRelays()
})
}
// Close with a click outside
document.addEventListener('click', event => {
if (visibleModal === null) return
const modalContent = visibleModal.querySelector('article')
const isClickInside = modalContent.contains(event.target)
!isClickInside && closeModal(visibleModal)
})
// Close with Esc key
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && visibleModal) {
closeModal(visibleModal)
}
})
// Get scrollbar width
const getScrollbarWidth = () => {
const scrollbarWidth =
window.innerWidth - document.documentElement.clientWidth
return scrollbarWidth
}
// Is scrollbar visible
const isScrollbarVisible = () => {
return document.body.scrollHeight > screen.height
}
// Templates
const getProfile = id => {
try {
const profile = profiles.get(id)
if (!profile) return id
console.log('Profile:', profile)
const content = JSON.parse(profile?.content)
console.log('Content:', content)
const name = content?.name || id
return name
} catch (error) {
return id
}
}
const noteTemplate = event => {
let note = document.createElement('article')
note.whiteSpace = 'preserve'
note.innerHTML = `
${getProfile(event.pubkey)}
${event.content}
event.created_at * 1000
).toLocaleString()}
`
return note
}
```
Beef bacon!
Brave New World
If 1984 had a child with Brave New World, it would be exactly where we are now!
LNbits Inc. seed raise dataroom (with deck and videos): https://drive.proton.me/urls/GRKR4ZWMBM#o4qGDbKAJeqx
For anyone interested. I think the pitch/products is correct. Other successful companies related to a FOSS project, and with healthy relationship, just offer support services as a way of feeding back developer time into the FOSS project, WordPress, Canonical, etc. A good example is the nip05 example in the deck, we built a thing using lnbits which gave us a good reason to tlc the nip05 extension, which anyone can now make use of.
Any VC types types here interested, please reach out to ben@lnbits.com π
ποΈ nostr:npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m
LNbits Inc. seed raise dataroom (with deck and videos): https://drive.proton.me/urls/GRKR4ZWMBM#o4qGDbKAJeqx
For anyone interested. I think the pitch/products is correct. Other successful companies related to a FOSS project, and with healthy relationship, just offer support services as a way of feeding back developer time into the FOSS project, WordPress, Canonical, etc. A good example is the nip05 example in the deck, we built a thing using lnbits which gave us a good reason to tlc the nip05 extension, which anyone can now make use of.
Any VC types types here interested, please reach out to ben@lnbits.com π
Bring it
Awesome day, awesome organization, awesome people, awesome food... when next?!
nostr:npub13nfdp7p3pacqn6202q33sur4djeehf50xagxq3y3pchhzjptz7yqenvn7c nostr:npub1kazpwv2n474s0tkqrd2cp8r74r6ym5qt4dutpjk9kur0cs5z7eaq9wzd0r nostr:npub160u5kdf4g2nr993qvteuj9rr35x7awny4u0esrvnjplwrvlq6nus90as4t nostr:npub1f2gumc4t04q6h0rlv3avdpfru5dnvpqk9nvh4a6ad09l57mzggwqrd4ckf
Nostr is so fucking awesome π!!
Can't wait to show you... it's epic... well maybe it's just good... or maybe it just doesn't suck... much!!
Yay....
Just testing something...
When you go full degen, dips like this are painful!!
#bitcoin
One can literally make a #Nostr client with a static html + websocket and run it locally!! WTF
Good weekend project, BTW!
nostr:npub1vt803quxxq32fuwkp42g2lyaw2t9qupvnl3z0vyc3s9kudkyhn8qt28cxv , I'm taking on your weekend projects concept and make this...

