Nostr NFC tap to sign!! is this a thing?
talvasconcelos
talvasconcelos@nostr.com
npub13a56...4t9m
Setting metadata from external tool... is way cool!!
Notes (13)
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!
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Nostr standalone local client to read notes (WIP)"
/>
<title>Nostr Client</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<header>
<nav class="container-fluid">
<ul>
<li><strong>NOSTR Client</strong></li>
</ul>
<ul>
<li>
<button
class="contrast"
data-target="pubkey-modal"
onclick="toggleModal(event)"
id="add-pubkey"
>
Add Pubkey
</button>
</li>
</ul>
</nav>
</header>
<main class="container">
<section id="notes"></section>
</main>
<!-- <footer>...</footer> -->
<dialog id="pubkey-modal">
<article>
<header>
<button
aria-label="Close"
rel="prev"
data-target="pubkey-modal"
onclick="toggleModal(event)"
></button>
<h3>Add you Public Key</h3>
</header>
<input
type="text"
name="pubkey"
placeholder="npub..."
aria-label="Pubkey"
/>
<footer>
<button
role="button"
class="secondary"
data-target="pubkey-modal"
onclick="toggleModal(event)"
>
Cancel</button
><button
autofocus
data-target="pubkey-modal"
onclick="toggleModal(event)"
>
Confirm
</button>
</footer>
</article>
</dialog>
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
<!-- <script src="js/index.js"></script> -->
<script>
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 = `
<header>
${getProfile(event.pubkey)}
</header>
${event.content}
<footer>
<time datetime="${event.created_at}">${new Date(
event.created_at * 1000
).toLocaleString()}</time>
</footer>
`
return note
}
</script>
</body>
</html>
```
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

If shit was valuable, poor people would be born without a asshole!!
#Bitcoin fixes that!
ELI5 #MLS ...
#asknostr
Twitter is so fucking tirying these days...
Elon, Trump, Kamala, socialism, internal shit in PT...
Think I'll have a Nostr September!!
How do I disable translation in Amethist?
Please don't drink and do politics!!