-
@ df3b8a38:40ca8150
2025-01-29 01:30:46
Wednesday #SelfieDay 🤳 Speeding through life with a smile 💨💖 #LoveSelfieCar #AdrenalineRush #BeautyInMotion https://images2.imgbox.com/34/d1/FfmGuTRA_o.jpg
-
@ df3b8a38:40ca8150
2025-01-29 00:30:06
Wednesday #SelfieDay 🤳 Riding in style, always 💅🏎️ #LoveSelfieCar #GlamRides #LuxuryVibes https://images2.imgbox.com/57/c5/jDevOCHy_o.jpg
-
@ df3b8a38:40ca8150
2025-01-28 23:04:48
-
@ ac5c59bc:9afde5b2
2025-01-28 20:55:10
Gun night frens...
Block height: 881244
#Bitcoin price 102054 USD
-
@ c7eda660:efd97c86
2025-01-28 20:40:43
This afternoon, I had an in-depth conversation with a local business owner who runs a long-standing, respected retail business. He barely made it through lockdowns, and now is getting crushed by inflation. His input costs have risen 3-5x. He's been saving in Bitcoin, personally, and now wants to do so for his business.
We're getting him started using nostr:nprofile1qy2hwumn8ghj7enjv4h8xtn4w3ux7tn0dejj7qg4waehxw309aex2mrp0yhxgctdw4eju6t09uqzpzkc78mcers3je3y9c520js4eymtyw5en40mjxl7fez89ckkat64azpmc6 to trial accepting Lightning payments. His 'Why' is crystal clear. Now it's just a matter of how.
I expect to see a lot more of this.
-
@ a39d19ec:3d88f61e
2025-01-28 18:54:21
"Sozialismus heißt heute jede Gesinnung und jede Tendenz und jeder Plan, die auf die Ordnung des Zusammenarbeitens und Zusammenlebens aller gehen unter dem Maßstab der Gerechtigkeit, unter Ablehnung von Privilegien... Insofern ist heute fast jeder ein Sozialist... Sozialismus ist der Grundzug unserer Zeit"
- Karl Jaspers
-
@ c7eda660:efd97c86
2025-01-28 17:52:20
Listening to someone offer up Mastodon as the way to break free of FaceTwitterGramTok now elicits the same response from me as Defi believers.
-
@ c7eda660:efd97c86
2025-01-28 14:59:35
The propaganda is unending. Save in Bitcoin.
“What is driving up prices?
The bird flu outbreak that started in 2022 is the main reason egg prices are up so much.“
https://apnews.com/article/egg-prices-bird-flu-poultry-inflation-9ea9934e20e3fe393abb1bb85aa31c30
-
@ d043c3a7:ba24b89e
2025-01-28 13:50:16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>zchan</title>
<style>
body { font-family: Arial, sans-serif; background: #ffe }
h1 {
margin-top: 0;
}
#list, .replies {
list-style: none;
padding: 0;
}
.replies {
margin-top: -0.5em;
margin-bottom: 1em;
}
li, #download-link-container {
background: #F0E0D6;
padding: .3em;
}
#download-link-container {
font-weight: bold;
}
li {
position: relative;
}
textarea {
display: block;
width: calc(100% - 3em);
height: 7em;
}
#content {
position: relative;
}
.date {
color: #555;
color: #555;
position: absolute;
right: 0;
bottom: 0;
margin: .2em;
}
.date span {
margin-left: .6em;
}
img {
max-width: 100%;
max-height: 20vh;
}
.loading,
.loading * {
cursor: wait;
}
.container { max-width: 1000px; margin: auto; border: 2px solid #ddd; border-radius: 1em; padding: 1em; }
.category, .thread, .message { margin-bottom: 1em; word-wrap: break-word; }
.thread.reply, .more {
margin-left: 3em;
margin-bottom: 10px;
}
.thread {
padding-bottom: 1.5em;
}
.pagination { display: flex; justify-content: center; margin-top: 1em; }
.pagination a { margin: 0 5px; }
.new-thread-form, .new-message-form { display: none; margin-top: 1em; }
#download-link-container {
display: none;
}
.threadTitle {
display: block;
overflow: hidden;
text-overflow: ellipsis;
background: #EEAA88;
padding: .2em;
white-space: nowrap;
}
.imgLink {
display: inline-block;
margin-right: .2em;
}
h2 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#actions {
position: absolute;
right: 0;
}
</style>
</head>
<body>
<div class="container">
<h1>zchan</h1>
<div id="content">Loading...</div>
<p id="download-link-container">
Download <a id="download-link" download="chan.html" href="#">chan.html</a> to run this app locally
</p>
</div>
<script src="https://unpkg.com/nostr-tools@2.7.2/lib/nostr.bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/image-blob-reduce@4.1.0/dist/image-blob-reduce.min.js"></script>
<script>
window.nostr = window.NostrTools
const reduce = new ImageBlobReduce()
const max_file_size = 12e4
const local = !location.href.startsWith("http")
const fileServers = [
"https://blossom.primal.net",
"https://nostr.download",
"https://blossom.f7z.io"
]
const fileRelays = [
"wss://nostr.swiss-enigma.ch",
"wss://nostr.cercatrova.me"
]
if(!local){
document.getElementById("download-link").href = location.pathname
document.getElementById("download-link-container").style.display = "block"
}
const maxWait = 1000
const catFreshDays = 10
const clientTags = []
let relays = []
let sockets = []
let queryIdCounter = 0
let activeThread
function init(){
relays.push("wss://nostr.swiss-enigma.ch")
relays.push("wss://nostr.cercatrova.me")
relays.push("wss://soloco.nl")
relays.push("wss://nostr.oxtr.dev")
relays.push("wss://relay.piazza.today")
relays.push("wss://junxingwang.org")
relays.push("wss://n.ok0.org")
relays.push("wss://nostr.data.haus")
relays.push("wss://bostr.bitcointxoko.com")
relays.push("wss://relaypag.es")
relays.push("wss://relay.nostr.watch")
relays.push("wss://relay.damus.io")
const contentDiv = document.getElementById('content')
contentDiv.innerText = "Connecting"
return new Promise(resolve => {
let openSockets = []
let resolved = false
for(const relay of relays){
const sock = new WebSocket(relay)
sock.onopen = function(){
if(resolved){
sock.close()
return
}
openSockets.push(this)
contentDiv.innerText = "Connected to " + openSockets.length + " relays"
if(openSockets.length >= 5){
sockets = openSockets
resolved = true
resolve()
}
}
sockets.push(sock)
}
})
}
function send(messageObject, limit, queryId, timeout) {
if(!timeout){
timeout = 1000
}
const st = new Date().getTime()
const query = JSON.stringify(messageObject)
const events = []
const activeSockets = [...sockets]
return new Promise(resolve => {
let resolved = false
for(const socket of sockets){
const sst = new Date().getTime()
socket.addEventListener("message", function(e){
if(resolved){
return
}
const data = JSON.parse(e.data)
if(data[0] == "EOSE" || data[0] == "OK"){
const sockIndex = activeSockets.indexOf(this)
if(sockIndex != -1){
activeSockets.splice(sockIndex, 1)
}
console.log("query @ " + this.url + " took ", new Date().getTime() - sst, "ms")
}
else if(data[0] == "EVENT" && data[1] == queryId && data.length > 2){
if(events.find(e => e.id == data[2].id)){
return
}
events.push(data[2])
}
if(activeSockets.length == 0){
console.log("send ok (2), query took ", new Date().getTime() - st, "ms", Array.from(events))
resolved = true
resolve(events)
return
}
})
socket.addEventListener("close", function(){
console.log("closed", this.url)
})
socket.send(query)
}
setTimeout(() => {
if(resolved){
return
}
console.log("send ok (3), query took ", new Date().getTime() - st, "ms")
console.log("timeouted relays", activeSockets.map(s => s.url))
resolved = true
resolve(Array.from(events))
}, timeout)
})
}
function text(str, multiline){
if(!str){
return str
}
str = str.replaceAll("<", "<").replaceAll("'", "‘").replaceAll("\"", """)
if(multiline){
str = str.replaceAll("\n", "<br/>")
}
return str
}
function showNewCategoryForm() {
const form = document.getElementById('new-category-form');
form.style.display = 'block';
}
function goToHashtag(){
const hashTags = document.querySelector("#hashtaginput").value.split(" ")
if(document.querySelector("#hashtaginput").value.length == 0){
return
}
const uri = hashTags.map(value => value.match(/^(#|)(.*)$/)[2]).join(",")
location.hash = "#tag/" + uri
}
function renderCategories() {
const usedNames = []
const contentDiv = document.getElementById('content')
const feedLink = localStorage.getItem("feed") && '<a href="#feed">view feed</a>' || ''
contentDiv.innerHTML = `
<h2>Topics</h2>
<form action="#" onsubmit="goToHashtag(); return false" autocomplete="on">
<p><input id="hashtaginput" type="text" placeholder="zchan aiart loli" required/></p>
<p><button>View threads</button></p>
</form>
${feedLink}
`
}
function saveFeed(){
localStorage.setItem("feed", location.hash.match("#tag/(.*)")[1])
location.hash = "#feed"
}
function createMessageElement(message, threadMessages, reply, reactions){
let threadTitleSafe = text(message.content)
const threadElement = document.createElement('li');
const date = text(new Date(message.created_at * 1000).toLocaleString("en-uk"))
threadElement.className = 'thread'
let repliesHtml = ""
if(reply){
threadElement.classList.add("reply")
}else{
repliesHtml = `
${reactions.length > 0 && `<span style="font-weight: bold">${reactions.length}</span> reactions` || ''}
<span style="${threadMessages.length > 1 ? "font-weight: bold": ""}">${threadMessages.length-1}</span> replies
`
}
threadElement.innerHTML = `
<a class="threadTitle" href="#thread/${text(threadMessages[0].id)}">${threadTitleSafe}</a> <span class="date">${date}${repliesHtml}</span>
<div class="messageContent">${links(text(message.content, false))}</div>
`
return threadElement
}
function escapeHref(uri){
return uri.replaceAll("'", "")
}
function makeUntilHash(categories, until){
if(location.hash.startsWith("#feed")){
return `#feed//${until}`
}
const tagsUri = categories.map(c => c.match(/^[^, ]+$/i)[0]).join(",")
return `#tag/${escapeHref(tagsUri)}/${until}`
}
function renderThreads(categories, threads, threadsMessages, threadsReactions) {
const categoriesHtml = makeCategoriesHtml(categories)
const categoriesValue = categories.map(c => c.match(/^[^, ]+$/i)[0]).join(" ")
const contentDiv = document.getElementById('content')
const feed = location.hash.startsWith("#feed") && localStorage.getItem("feed")
let feedLink = !feed && '<a href="#feed">view feed</a> ' || `<a href='#tag/${escapeHref(feed)}'>feed link</a>`
const feedButtonHtml = ! location.hash.startsWith("#feed") && '<button onclick="saveFeed()">Save feed</button>' || ''
let firstPageLinkHtml = ""
if(getUntil()){
firstPageLinkHtml = `<a href="${makeUntilHash(categories, "")}">first page</a>`
}
const lastTimestamp = Math.min(threads[threads.length-1].created_at, threadsMessages.length > 0 && threadsMessages[threadsMessages.length-1].created_at || Infinity) - 1
contentDiv.innerHTML = `
<h2><a href="#">Home</a> > ${categoriesHtml}</h2>
<div id="actions">${feedLink}${feedButtonHtml}</div>
<button onclick="showNewThreadForm('${categories[0]}', '${categories[0]}')">New Thread</button>
<div id="new-thread-form" class="new-thread-form">
<p><input type="text" id="new-thread-categories" placeholder="categories" value="${categoriesValue}"/>
<p><textarea id="new-thread-message" placeholder="First Message"></textarea></p>
<p><input id="new-thread-file" type="file"/> ${makeFileServerSelectionHtml()}</p>
<p><button onclick='createNewThread()'>Create Thread</button></p>
</div>
<ul id="list"></ul>
${firstPageLinkHtml}
<a onclick="window.scrollTo(0,0)" href="${location.origin + location.pathname + makeUntilHash(categories, lastTimestamp)}">next page >></a>
`
const list = document.getElementById("list")
threads.forEach(thread => {
let messages = [thread, ...threadsMessages.filter(m => m.tags.find(t => t[0] == "e" && t[1] == thread.id))]
let reactions = threadsReactions.filter(m => m.tags.find(t => t[0] == "e" && t[1] == thread.id))
const messageElement = createMessageElement(thread, messages, false, reactions)
list.appendChild(messageElement)
if(messages.length > 1){
const replyList = document.createElement("ul")
replyList.classList.add("replies")
const countDiff = messages.length - 4
if(countDiff > 0){
const el = document.createElement("li")
const link = document.createElement("a")
el.classList.add("more")
link.href = `#thread/${thread.id}`
link.innerText = `${countDiff} more messages`
el.append(link)
replyList.appendChild(el)
}
for(let message of messages.slice(1).slice(-3)){
//let reactions = threadsReactions.filter(m => m.tags.find(t => t[0] == "e" && t[1] == message.id))
const messageElement = createMessageElement(message, messages, true)
replyList.appendChild(messageElement)
}
list.append(replyList)
}
})
}
function showNewThreadForm(categoryId, categoryTitle) {
const form = document.getElementById('new-thread-form');
form.style.display = 'block';
}
async function sha256(blob) {
const uint8Array = new Uint8Array(await blob.arrayBuffer())
const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map((h) => h.toString(16).padStart(2, '0')).join('')
}
async function load_image(image_blob){
return new Promise((resolve) => {
let reader = new FileReader()
let data_url = reader.readAsDataURL(image_blob)
reader.onload = function(e){
const img = new Image()
img.src = e.target.result
img.onload = async () => resolve(img)
}
})
}
async function resize(max_size, quality, blob){
let resized_img = await reduce.toBlob(blob, { max: max_size })
return await reduce.pica.toBlob(await reduce.toCanvas(resized_img), 'image/webp', quality)
}
async function resizeImg(blob){
let quality = 0.90
let max_size = 3000
let min_size = 900
let min_quality = 0.75
let resized_img = null
while(resized_img == null || resized_img.size > max_file_size){
resized_img = await resize(max_size, quality, blob)
if(quality > min_quality){
quality = Math.max(min_quality, quality - 0.5)
}
else if(max_size > min_size){
max_size = Math.max(min_size, max_size - 500)
}
else {
break
}
}
let img = null
if(resized_img.size <= max_file_size){
img = await load_image(resized_img)
console.log("resized image file size: " + resized_img.size + " bytes, w: " + img.width + ", h: " + img.height + ", quality: " + quality)
}else{
throw "error: could not resize image within size limitations"
return
}
return resized_img
}
function blobToDataURL(blob, callback) {
return new Promise(resolve => {
var a = new FileReader()
a.onload = function(e) {
resolve(e.target.result)
}
a.readAsDataURL(blob)
})
}
async function uploadFile(fileBlob, privateKey){
const imgBlob = await resizeImg(fileBlob)
const fileDataUri = await blobToDataURL(imgBlob)
let fileEvent = {
"kind": 1063,
"content": "image",
"created_at": Math.floor(Date.now() / 1000),
"tags": [
["url", fileDataUri],
["m", imgBlob.type],
]
}
fileEvent = window.nostr.finalizeEvent(fileEvent, privateKey)
const fileId = fileEvent.id
await send(["EVENT", fileEvent], 1, "sendq", 5000)
const nevent_obj = Object.create(fileEvent)
nevent_obj.relays = fileRelays
const neventStr = window.nostr.nip19.neventEncode(nevent_obj)
return neventStr
}
async function uploadBlob(fileBlob, fileHash, privateKey, serverUrl){
let auth = {
"kind": 24242,
"content": "",
"created_at": Math.floor(new Date().getTime() / 1000), //Math.floor(new Date().getTime() / 1000 - Math.random() * 84000),
"tags": [
["t", "upload"],
["x", fileHash],
["expiration", (Math.floor(new Date().getTime() / 1000) + 10000).toString()]
]
}
auth = window.nostr.finalizeEvent(auth, privateKey)
const formData = new FormData()
formData.append("files", fileBlob)
const body = new Blob([fileBlob], { type: 'application/octet-stream' })
return await (await fetch(`${serverUrl}/upload`, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
'Authorization': 'Nostr ' + btoa(JSON.stringify(auth)),
},
body
})).json()
}
async function uploadAnyFile(fileBlob, fileHash, fileServers, privateKey){
const fileServerType = document.getElementById('form-file-server').value
if(fileBlob && fileBlob.type.startsWith("image/") && fileServerType == "event"){
const neventStr = await uploadFile(fileBlob, privateKey)
return ["nostr:" + neventStr]
}
const fileUrls = []
for(let serverUrl of fileServers){
const match = fileBlob.name.match(/\.[a-z0-9]{1,10}/)
fileExtension = match && match[0] || ""
const uploadResult = await uploadBlob(fileBlob, fileHash, privateKey, serverUrl)
fileUrls.push(`${serverUrl}/${uploadResult.sha256}${fileExtension}`)
}
return fileUrls
}
async function createNewThread() {
const categories = document.querySelector("#new-thread-categories").value.split(" ")
const messageInput = document.getElementById('new-thread-message')
const fileBlob = document.getElementById('new-thread-file').files[0]
let firstMessage = messageInput.value
if (!firstMessage && !fileBlob){
alert("message or file required")
return
}
const privateKey = window.nostr.generateSecretKey()
const uploadResult = fileBlob && await handleFileUpload(fileBlob, privateKey)
if(uploadResult && uploadResult.urls && uploadResult.urls.length > 0){
firstMessage = uploadResult.urls[0] + "\n" + firstMessage
}
const fileTags = uploadResult && uploadResult.tags || []
let messageEvent = {
kind: 1,
tags: [
...categories.map(category => ["t", category]),
...fileTags
],
content: firstMessage,
created_at: Math.floor(Date.now() / 1000),
pubkey: window.nostr.getPublicKey(privateKey),
}
messageEvent.id = window.nostr.getEventHash(messageEvent)
messageEvent = window.nostr.finalizeEvent(messageEvent, privateKey)
await send(["EVENT", messageEvent])
loadThreads(categories)
}
function links(str){
const img = '<a class="imgLink" href="$1" target="_blank"><img src="$1"/></a>'
str = str.replace(/(http(s|):\/\/\S+\.(jpg|jpeg|webp|png|gif)|blob:http(s|):\/\/\S+)/gi, img)
const embedHtml = '<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/$1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>'
str = str.replace(/https:\/\/www.youtube.com\/watch\?v=([a-zA-Z0-9]{11})/g, embedHtml)
str = (" "+str).replace(/(<br\/>| )(http(s|):\/\/[a-z0-9-_.+?%/&]+)/gi, '$1<a href="$2">$2</a>')
str = str.substr(1)
str = str.replaceAll("\n", "<br/>")
return str
}
function makeCategoriesHtml(categories){
return categories.map(category => {
const prefix = "tag"
const match = category.match(/^(#|)(.*)$/)
return `<a href="#${prefix}/${text(match[2])}">${text(match[2])}</a>`
}).join(" / ")
}
function makeFileServerSelectionHtml(idServer, idCount){
return `preferred server
<select id="form-file-server">
<option value="-1">default</option>
<option value="blob">http server</option>
<option value="event">event (base64)</option>
</select>
no of servers
<select id="form-file-server-count">
<option value="2">2 (default)</option>
<option value="1">1</option>
</select>
`
}
function renderMessages(messages, categories) {
console.log("renderMessages")
const categoriesHtml = makeCategoriesHtml(categories)
const contentDiv = document.getElementById('content');
contentDiv.innerHTML = `
<h2><a href="#">Home</a> > ${categoriesHtml} > ${text(messages[0].content)}</h2>
<ul id="list"></ul>
`
const list = document.getElementById("list")
messages.forEach(message => {
console.log("message", message)
const messageElement = document.createElement('li');
messageElement.className = 'message'
messageElement.innerHTML = `<div>${links(text(message.content, false))}</div>`
list.appendChild(messageElement);
})
activeThread = messages[0]
contentDiv.innerHTML += `
<button onclick="showNewMessageForm()">New Message</button>
<div id="new-message-form" class="new-message-form">
<p><textarea id="new-message-content" placeholder="New Message"></textarea></p>
<p><input id="new-message-file" type="file"/> ${makeFileServerSelectionHtml()}</p>
<p><button onclick='createNewMessage()'>Post Message</button></p>
</div>
`
}
function displayHtmlError(htmlError){
const contentDiv = document.getElementById('content')
contentDiv.innerHTML = `
<h2><a href="#">Home</a> > error</h2>
<p>${htmlError}</p>
`
}
function showNewMessageForm() {
const form = document.getElementById('new-message-form');
form.style.display = 'block';
}
function pickFileServers(){
const fileServerCount = document.getElementById('form-file-server-count').value
const selectedFileServers = []
const myFileServers = [...fileServers]
for(let i = 0; i < fileServerCount; i++){
const fileServerIndex = Math.round(Math.random()*(myFileServers.length-1))
selectedFileServers.push(...myFileServers.splice(fileServerIndex, 1))
}
return selectedFileServers
}
async function handleFileUpload(fileBlob, privateKey){
const tags = []
let urls
if(fileBlob){
const fileHash = await sha256(fileBlob)
const fileServers = pickFileServers()
urls = await uploadAnyFile(fileBlob, fileHash, fileServers, privateKey)
tags.push(["x", fileHash])
for(let fileServer of fileServers){
tags.push(["server", fileServer])
}
}
return {
tags,
urls
}
}
async function createNewMessage() {
const thread = activeThread
const messageInput = document.getElementById('new-message-content');
let messageContent = messageInput.value;
const fileBlob = document.getElementById('new-message-file').files[0]
if (!messageContent && !fileBlob){
alert("message or file required")
return
}
const privateKey = window.nostr.generateSecretKey()
const uploadResult = await handleFileUpload(fileBlob, privateKey)
if(uploadResult && uploadResult.urls && uploadResult.urls.length > 0){
messageContent = uploadResult.urls[0] + "\n" + messageContent
}
if(fileBlob && uploadResult && (!uploadResult.urls || uploadResult.urls.length == 0)){
//console.log("error", uploadResult, fileBlob)
alert("error")
return
}
const fileTags = uploadResult && uploadResult.tags || []
let event = {
kind: 1,
tags: [
['p', thread.pubkey],
['e', thread.id, relays[0], "root"],
...fileTags,
...clientTags
],
content: messageContent,
created_at: Math.floor(Date.now() / 1000),
pubkey: window.nostr.getPublicKey(privateKey),
};
event.id = window.nostr.getEventHash(event);
event = window.nostr.finalizeEvent(event, privateKey);
await send(["EVENT", event])
loadMessages(thread)
}
function fetchLatestMessages(kinds, limit) {
const queryId = (++queryIdCounter).toString()
return send(["REQ", queryId, { kinds: kinds, limit: limit }], limit, queryId)
}
function threadIdsFromMessages(messages){
const idsUnique = new Set(messages.map(msg => {
const root = msg.tags.find((t, i) => {
return t[0] == "e"
})
return root && root[1]
}).filter(id => id !== undefined && id.match(/^[a-z0-9]{64}$/)))
return Array.from(idsUnique)
}
function findThreadUpdateTime(thread, threadsMessages){
let latestTimestamp = 0
for(let message of [thread, ...threadsMessages]){
if(message != thread && !message.tags.find(t => t[0] == "e" && t[1] == thread.id)){
continue
}
if(message.created_at >= latestTimestamp){
latestTimestamp = message.created_at
}
}
return latestTimestamp
}
async function loadThreads(categories, redirect, until) {
const filters = { limit: 50 }
filters.kinds = [1]
filters["#t"] = categories
if(until){
filters["until"] = until
}
const latestMessages = await fetchLatestMessages([1], 500);
let queryId = (++queryIdCounter).toString()
let threads = await send(["REQ", queryId, filters], filters.limit, queryId)
threads = threads.filter(t => !t.tags.find(tag => tag[0] == "e"))
filters["ids"] = threadIdsFromMessages(latestMessages)
queryId = (++queryIdCounter).toString()
threads.push(...await send(["REQ", queryId, filters], filters.limit, queryId))
const threadsIds = []
let threadsUnique = []
for(let thread of threads){
if(!threadsIds.includes(thread.id)){
threadsIds.push(thread.id)
threadsUnique.push(thread)
}
}
queryId = (++queryIdCounter).toString()
const threadsEvents = await send(["REQ", queryId, { kinds: [1, 7], '#e': threadsIds, limit: 1000 }], 1000, queryId)
const threadsMessages = threadsEvents.filter(e => e.kind == 1)
const threadsReactions = threadsEvents.filter(e => e.kind == 7)
threadsMessages.sort((a, b) => a.created_at - b.created_at)
threadsUnique.sort((a, b) => {
aTimestamp = findThreadUpdateTime(a, threadsMessages)
bTimestamp = findThreadUpdateTime(b, threadsMessages)
return bTimestamp - aTimestamp;
})
await resolveEmbeddedMedia([...threadsUnique, ...threadsMessages])
if(threadsUnique.length == 0){
const feedLinkHtml = localStorage.getItem("feed") && ' or <a href="#feed">feed</a>' || ''
displayHtmlError(`no threads. try <a href="#">going to home</a>${feedLinkHtml}`)
return
}
renderThreads(categories, threadsUnique, threadsMessages, threadsReactions)
if(redirect != false){
const prefix = "tag"
const untilStr = until && `/${until}` || ""
window.location.hash = `#${prefix}/${categories.join(",")}${untilStr}`
}
}
async function resolveEmbeddedMedia(messages){
const matches = messages.map(m => m.content).join().matchAll(/nostr:(nevent[a-z0-9]+)/g)
const embeddedEventIds = []
const eventMap = {}
for(let m of [...matches]){
const neventStr = m[1]
let id
try {
id = nostr.nip19.decode(neventStr).data.id
}catch(e){
console.log("invalid nevent", neventStr)
}
eventMap[id] = neventStr
embeddedEventIds.push(id)
}
let queryId = (++queryIdCounter).toString()
const embeddedEvents = await send(["REQ", queryId, { ids: embeddedEventIds }], 200, queryId, 3000)
for(let event of embeddedEvents){
const urlTag = event.tags.find(t => t[0] == "url")
if(!urlTag){
continue
}
const dataUri = urlTag[1]
const neventStr = eventMap[event.id]
for(let message of messages){
const blob = await (await fetch(dataUri)).blob()
let blobUrl = URL.createObjectURL(blob)
message.content = message.content.replaceAll("nostr:" + neventStr, blobUrl)
}
}
}
async function loadMessages(thread) {
let queryId = (++queryIdCounter).toString()
let messages = await send(["REQ", queryId, { kinds: [1], '#e': [thread.id], limit: 200 }], 200, queryId)
messages.sort((a, b) => a.created_at - b.created_at)
const categories = []
for(let tag of thread.tags){
if(tag[0] == "t"){
categories.push(tag[1])
}
}
messages = [thread, ...messages]
await resolveEmbeddedMedia(messages)
renderMessages(messages, categories);
window.location.hash = `#thread/${thread.id}`
}
async function getThread(threadId) {
let queryId = (++queryIdCounter).toString()
const threads = await send(["REQ", queryId, { ids: [threadId], limit: 1 }], 1, queryId)
return threads.length > 0 ? threads[0] : null;
}
function getUntil(){
const hash = decodeURIComponent(window.location.hash)
const hashParts = hash.split('/')
if(hashParts.length > 2 && hashParts[2].match(/^\d+$/)){
return parseInt(hashParts[2], 10)
}
}
async function initializeFromHash() {
const hash = decodeURIComponent(window.location.hash)
const hashParts = hash.split('/')
let until = getUntil()
if (hash.startsWith('#thread/')) {
const threadId = hashParts[1]
const thread = await getThread(threadId)
if(!thread){
displayHtmlError('could not load thread. try <a href="javascript:location.reload()">reloading</a>')
return
}
await loadMessages(thread)
}
else if (hash.startsWith('#tag/')) {
const hashTagsStr = hashParts[1]
const categories = hashTagsStr.split(",")
await loadThreads(categories, false, until)
}
else if (hash.startsWith('#feed')){
const hashTagsStr = localStorage.getItem("feed")
const categories = hashTagsStr.split(",")
await loadThreads(categories, false, until)
}
else {
await renderCategories();
}
}
onhashchange = async function(){
document.body.classList.add("loading")
await initializeFromHash()
document.body.classList.remove("loading")
}
document.addEventListener('DOMContentLoaded', async () => {
await init()
await initializeFromHash();
});
</script>
</body>
</html>
-
@ c7eda660:efd97c86
2025-01-28 13:34:45
Powerful. Thanks for the reminder about Dust. I used to watch regularly.
-
@ c7eda660:efd97c86
2025-01-28 13:05:57
“The unsettling truth exposed by the DeepSeek breakthrough is this: The traditional protections of industrial and military supremacy are far weaker than we thought.”
https://www.thefp.com/p/deepseek-is-a-wake-up-call-is-america
-
@ c7eda660:efd97c86
2025-01-28 12:48:11
gm, good people. ☕ 🌅
https://video.nostr.build/d9aca1f9280929a10b1f06e808e925e2b459d193f3880d4def254a6b8f0872c3.mp4
-
@ f03df3d4:a4d4f676
2025-01-28 11:40:19
Block 881193
4 - high priority
3 - medium priority
3 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ a39d19ec:3d88f61e
2025-01-28 10:50:26
Sun and time?
-
@ 4f983dcb:d6acbeb0
2025-01-28 08:27:28
GM frens..
-
@ fad6540c:2883a447
2025-01-28 07:30:23
It is customary to have cheese
https://image.nostr.build/712d7683804ce38220d4bc190d924a0c565c5aab830508205428f111c66d1431.jpg https://image.nostr.build/712d7683804ce38220d4bc190d924a0c565c5aab830508205428f111c66d1431.jpg
-
@ c0e0c427:a1f63a16
2025-01-28 07:09:36
Seeking fellow cheese makers to learn from and share ideas with. I'm a novice cheese maker and have much to learn but I can understand why people devote their lives to becoming master cheese makers. It's so fun and delicious!
Starting #curdnerds to find others like me.
#homesteading #cooking #homecooked #cheese
-
@ 1ae011cb:1257a556
2025-01-28 07:00:23
Block 881164
3 - high priority
3 - medium priority
3 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ f03df3d4:a4d4f676
2025-01-28 07:00:23
Block 881164
3 - high priority
3 - medium priority
3 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ f03df3d4:a4d4f676
2025-01-28 06:10:18
Block 881158
2 - high priority
2 - medium priority
2 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ e7bf8dad:839ef3db
2025-01-28 05:40:47
Block 881151
2 - high priority
2 - medium priority
2 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ df3b8a38:40ca8150
2025-01-28 05:27:44
GN frens... 🧸🥱
-
@ 1ae011cb:1257a556
2025-01-28 05:00:47
Block 881146
5 - high priority
5 - medium priority
5 - low priority
4 - no priority
2 - purging
#bitcoinfees #mempool
-
@ ac5c59bc:9afde5b2
2025-01-28 04:54:28
Gun Morning frens...
103334 USD for one #Bitcoin
Coffee time ☕
-
@ 2d9873b2:efee87f2
2025-01-28 04:53:27
NWC is ok, I guess https://image.nostr.build/e101f7a4b591d741c5c2c753245aeb85ea69c8b0c0fb9ca863ee3aefc8679d85.png
-
@ e7bf8dad:839ef3db
2025-01-28 04:40:47
Block 881145
4 - high priority
3 - medium priority
3 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ e7bf8dad:839ef3db
2025-01-28 04:10:47
Block 881143
2 - high priority
2 - medium priority
2 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ df3b8a38:40ca8150
2025-01-27 23:04:54
-
@ c7eda660:efd97c86
2025-01-27 22:42:24
Brokerage accounts are like Hotel California. You can check out, but you can never leave. https://m.primal.net/ODWN.png
-
@ c7eda660:efd97c86
2025-01-27 21:51:28
I've been using it for a few months; great product.
But caveat emptor, it's Eric Vorhees.
https://venice.ai/blog/introducing-the-venice-token-vvv
-
@ c7eda660:efd97c86
2025-01-27 21:41:00
A cottage industry, seemingly overnight.
https://newsletter.languagemodels.co/p/the-illustrated-deepseek-r1
-
@ c7eda660:efd97c86
2025-01-27 21:17:27
Color me intrigued.
https://repebble.com/
-
@ a39d19ec:3d88f61e
2025-01-27 21:16:22
When China LLM for #Umbrel? 🤔
-
@ c7eda660:efd97c86
2025-01-27 21:14:56
Another blow against the globalists.
https://apnews.com/article/cdc-who-trump-548cf18b1c409c7d22e17311ccdfe1f6
-
@ ac5c59bc:9afde5b2
2025-01-27 20:55:23
Gun night frens...
Block height: 881097
#Bitcoin price 100855 USD
-
@ f03df3d4:a4d4f676
2025-01-27 19:50:06
Block 881092
9 - high priority
8 - medium priority
6 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ c7eda660:efd97c86
2025-01-27 19:01:50
“That’s why we see all the early AI products from the companies spending billions taking one of two approaches: bundling or mostly open source. Those outside those two models are in a sense competing against bundles and against those companies trying to de-monetize the bundles. Those outside are caught in the middle.
The cost of AI, like the cost of mainframe computing to X.25 connectivity, literally **forces** the market to develop an alternative that scales without the massive direct capital.”
https://hardcoresoftware.learningbyshipping.com/p/228-deepseek-has-been-inevitable
-
@ e7bf8dad:839ef3db
2025-01-27 19:00:07
Block 881092
2 - high priority
2 - medium priority
2 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ 1ae011cb:1257a556
2025-01-27 19:00:07
Block 881092
2 - high priority
2 - medium priority
2 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ f03df3d4:a4d4f676
2025-01-27 18:55:06
Block 881091
4 - high priority
3 - medium priority
3 - low priority
2 - no priority
1 - purging
#bitcoinfees #mempool
-
@ c7eda660:efd97c86
2025-01-27 18:51:02
💪
-
@ c7eda660:efd97c86
2025-01-27 18:47:25
Business #protip: make and take more voice and video calls. Text is fine for the transactional and technical, but it lacks depth. Depth is what you need to build and maintain relationships.
-
@ a39d19ec:3d88f61e
2025-01-27 17:30:09
I guess I saw the photo you posted and was eager to look it up.
Thanks for posting about it again so I get the name right.
-
@ a39d19ec:3d88f61e
2025-01-27 17:27:45
He ate her after the photo didn't he?
-
@ a39d19ec:3d88f61e
2025-01-27 17:24:26
I used Antix some years ago on an even older laptop (2004 model). I was surprised that I was able to browse the internet on it. Windows wont even load Firefox on that laptop.
I remember the desktop being kind of ugly. But it worked perfectly
-
@ c7eda660:efd97c86
2025-01-27 15:54:55
As the man says. #tunestr
https://music.apple.com/us/album/get-up/1484901828?i=1484901830
-
@ c7eda660:efd97c86
2025-01-27 15:49:23
It will be glorious to see Larsen fall into ruin, which is inevitable.
Meanwhile, tick tock…
https://protos.com/ripple-scores-victory-against-the-bitcoin-strategic-reserve/
-
@ c7eda660:efd97c86
2025-01-27 12:50:36
gm, good people. ☕
https://video.nostr.build/a2a1e92178d4ae7ae45b4c5372d0056dddf19a555d0cdc0c3238b14b19a28fff.mp4
-
@ a39d19ec:3d88f61e
2025-01-27 12:07:06
Das sowieso. Worauf ich hinaus wollte ist, dass die nicht verbeamtet wird, weil sie kommunistische Ansichten hat. Das hätte ich jetzt nicht erwartet
-
@ a39d19ec:3d88f61e
2025-01-27 11:30:31
Wer sagt den Münchner denn, das unsere Währung so aufgebaut ist, wie im kommunistischen Manifest beschrieben?
Unsere Währung verstößt somit gegen die freiheitlich demokratische Grundordnung!
nostr:nevent1qqsphr3aq3xtda4pyey4hsdfdyd7l5wj3pjuqxxv376rd200u8krc3cpz3mhxue69uhhwmm59ehx7um5wghxuet59upzpguar8kz8zh3k99ceumpsk5n8v2plx2w9lm6zv36fjaktu7c3as7qvzqqqqqqy4rwtms
-
@ a39d19ec:3d88f61e
2025-01-27 11:24:35
Das "Tausendjährige Reich" scheint auch eine Wendung von sozialistischen Ideengebern/Vordenker zu sein.
-
@ a39d19ec:3d88f61e
2025-01-27 11:23:20
Das mag sein. Aber gerade in der Schule sollte solch eine Indoktrination nicht stattfinden.
-
@ a39d19ec:3d88f61e
2025-01-27 11:19:24
Ihr müsst echt aufpassen...sagt ihr etwas differenziertes gegen die Migrationspolitik seid ihr Nazi.
Wenn ihr das Wort "Profitmaximierung" nutzt könntet ihr als Kommunist gelten!
Glaubt ihr nicht? Sehr hier:
https://www.n-tv.de/politik/Bayern-schliesst-angehende-Lehrerin-vom-Referendariat-aus-article25516842.html
-
@ a39d19ec:3d88f61e
2025-01-27 10:40:48
Kein Wähler der AfD aber diese Indoktrination geht gar nicht. Wenn man die Aussagen der Kinder liest, merkt man, dass sie die Worte in den Mund gelegt bekommen haben.
#indoktrination
https://rp-online.de/nrw/staedte/geldern/geldern-schueler-ausstellung-zur-afd-im-friedrich-spee-gymnasium_aid-123496283
-
@ 4f983dcb:d6acbeb0
2025-01-27 08:28:32
GM frens..
-
@ a39d19ec:3d88f61e
2025-01-27 08:18:11
#GM Leute!
Na habt ihr auch Bock auf die neue Woche? 🫣
-
@ 25dc2682:0c1c94df
2025-01-27 08:10:08
GM
Actual Bitcoin price: 99206 USD
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
1 year ago: 41471 USD
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
Five years ago: 9281 USD
▪▪▪▪▪▪▪▪▪
Ten years ago: 248 USD
▪
-
@ df3b8a38:40ca8150
2025-01-27 05:25:38
GN frens... 🧸🥱
-
@ ac5c59bc:9afde5b2
2025-01-27 04:54:58
Gun Morning frens...
100490 USD for one #Bitcoin
Coffee time ☕
-
@ c7eda660:efd97c86
2025-01-27 02:21:02
Good sir, nostr:nprofile1qyt8wumn8ghj7etyv4hzumn0wd68ytnvv9hxgtcprpmhxue69uhkv6tvw3jhytnwdaehgu3wwa5kuef0qqsrjxqeute0zwusetrjp9qeadt5aa7q686wsxr8lsjvg73uuh52yjq7qs24c and nostr:nprofile1qyxhwumn8ghj7mn0wvhxcmmvqyg8wumn8ghj7mn0wd68ytnvv9hxgqpqke470rdgnxg4gjs9cw3tv0dp690wl68f5xak5smflpsksedadd7q6regqd would likely know better than I.
-
@ c7eda660:efd97c86
2025-01-27 01:36:24
This is #wisdom.
-
@ c7eda660:efd97c86
2025-01-27 01:19:41
Came in $120 under my predicted Costco damage. It was a good day.
-
@ df3b8a38:40ca8150
2025-01-26 23:05:21
-
@ c7eda660:efd97c86
2025-01-26 20:57:32
Feel that? It’s momentum.
-
@ ac5c59bc:9afde5b2
2025-01-26 20:56:09
Gun night frens...
Block height: 880947
#Bitcoin price 105058 USD
-
@ c7eda660:efd97c86
2025-01-26 20:29:46
Running Arch on the latest 13" AMD. Had some problems before the latest BIOS update, but since then, it's been super smooth. Looking forward to better fan control.
-
@ c7eda660:efd97c86
2025-01-26 20:19:11
Proxmox on a Raspberry pi was a fun challenge, but ultimately, yeah, don't do it.
-
@ c7eda660:efd97c86
2025-01-26 20:16:36
Unstoppable money + uncensorable communications. Nothing less will do.
-
@ c7eda660:efd97c86
2025-01-26 19:02:04
Good. More self-sovereign hodlers is what we need. Meanwhile, Vanguard is NGMI.
“The easy step for us would have been just to allow full access to crypto-related products. But as a firm and a brokerage platform, we’re purposely structured to meet the needs of our investor-owners, most of whom are long-term, buy-and-hold investors.”
https://corporate.vanguard.com/content/corporatesite/us/en/corp/articles/no-bitcoin-etfs-at-vanguard-heres-why.html
-
@ c7eda660:efd97c86
2025-01-26 17:08:59
Yes.
-
@ c7eda660:efd97c86
2025-01-26 16:34:38
Side note: dark af, but brilliant film. Possibly, his best.
https://www.imdb.com/title/tt0113627/
-
@ c7eda660:efd97c86
2025-01-26 16:13:54
My new go-to rss reader. Calm.
https://github.com/guyfedwards/nom
-
@ c7eda660:efd97c86
2025-01-26 15:26:41
Same, same, but > 100 nodes.
-
@ c7eda660:efd97c86
2025-01-26 15:13:52
Just before the final boss.
https://www.popularfintech.com/p/no-one-is-disrupting-banks
-
@ c7eda660:efd97c86
2025-01-26 15:12:02
This sparks joy.
https://apnews.com/article/rfk-senate-confirmation-vaccines-trump-health-f000bbb5c5f2c800299a7ff8e64fee0b
-
@ a39d19ec:3d88f61e
2025-01-26 14:07:11
Hatte ich jetzt das erste Mal. Naja dann eben kein Reddit mehr
-
@ c7eda660:efd97c86
2025-01-26 13:54:30
gm, good people. ☕
https://v.nostr.build/qtZryxzc7pAsxwmf.mp4
-
@ a39d19ec:3d88f61e
2025-01-26 13:05:41
GM!
-
@ a39d19ec:3d88f61e
2025-01-26 12:46:35
Reddit geht nun auch nicht via VPN? 😕
-
@ a39d19ec:3d88f61e
2025-01-26 12:20:14
GM
-
@ a39d19ec:3d88f61e
2025-01-26 10:58:41
Fordert er etwa Wehrsportgruppen? Ist das nicht Nazi?
-
@ a39d19ec:3d88f61e
2025-01-26 10:58:02
Der Sozialist Fourier schrieb:
"Insbesondere die Jüdische Frage: Sie soll verschwinden, sobald finanzielle Operationen und Schacher unmöglich werden.
Die chimärische Nationalität des Juden ist die Nationalität des Kaufmanns, überhaupt des Geldmenschen.
"
#sozialismus #antisemitismus
-
@ a39d19ec:3d88f61e
2025-01-26 10:47:02
Ich hab von Anfang an nur Testnet Coins gestakt.
Ich mach doch nichts, hinter dem meine Regierung nicht steht....
-
@ 4f983dcb:d6acbeb0
2025-01-26 08:27:52
GM frens..
-
@ a39d19ec:3d88f61e
2025-01-26 07:07:05
Guten Morgen Leute!
#gm
-
@ df3b8a38:40ca8150
2025-01-26 05:24:41
GN frens... 🧸🥱
-
@ ac5c59bc:9afde5b2
2025-01-26 04:55:09
Gun Morning frens...
105152 USD for one #Bitcoin
Coffee time ☕
-
@ c7eda660:efd97c86
2025-01-26 03:24:21
What song do you have on repeat this weekend? #tunestr
-
@ c7eda660:efd97c86
2025-01-26 03:09:01
MADE IN THE U.S.A., GUY!
-
@ df3b8a38:40ca8150
2025-01-25 23:04:44
-
@ c7eda660:efd97c86
2025-01-25 21:49:22
"Rules engines," back in the day.
-
@ c7eda660:efd97c86
2025-01-25 21:11:36
https://m.primal.net/OAFJ.jpg
-
@ ac5c59bc:9afde5b2
2025-01-25 20:55:04
Gun night frens...
Block height: 880806
#Bitcoin price 104956 USD
-
@ c7eda660:efd97c86
2025-01-25 20:51:32
You forgot to tag this as #nsfw.
-
@ c7eda660:efd97c86
2025-01-25 19:23:16
Which part of this exceedingly detailed and clear statement confuses you?
-
@ c7eda660:efd97c86
2025-01-25 18:42:04
🤡
-
@ c7eda660:efd97c86
2025-01-25 18:40:34
I use tailscale on everything. This is specifically for full KVM functionality.
-
@ c7eda660:efd97c86
2025-01-25 18:35:27
Attention, fellow nerds: this is killer little device.
https://jetkvm.com/
-
@ c7eda660:efd97c86
2025-01-25 18:34:03
https://media1.tenor.com/m/JFBGlDSduLQAAAAd/weareback-wereback.gif
-
@ 1742a672:b8e30e07
2025-01-25 18:32:50
Last 7 days zaps: 677906.
https://nostroz.com/zap_stats_7_new_20250125.png
Last 30 days zaps: 1025969.
https://nostroz.com/zap_stats_30_new_20250125.png
Last 90 days zaps: 2920323.
https://nostroz.com/zap_stats_90_new_20250125.png
2025-01-25