Move pool out of worker

This commit is contained in:
Jonathan Staab 2022-12-16 21:44:50 -08:00
parent 9f6601cbdc
commit f4a706e12b
16 changed files with 225 additions and 478 deletions

View File

@ -12,13 +12,13 @@
import {hasParent} from 'src/util/html'
import {timedelta} from 'src/util/misc'
import {store as toast} from "src/state/toast"
import {channels, epoch} from "src/state/nostr"
import {channels} from "src/state/nostr"
import {modal, logout, alerts} from "src/state/app"
import {user} from 'src/state/user'
import Anchor from 'src/partials/Anchor.svelte'
import NoteDetail from "src/partials/NoteDetail.svelte"
import NotFound from "src/routes/NotFound.svelte"
import Search from "src/routes/Search.svelte"
// import Search from "src/routes/Search.svelte"
import Alerts from "src/routes/Alerts.svelte"
import Notes from "src/routes/Notes.svelte"
import Login from "src/routes/Login.svelte"
@ -27,12 +27,12 @@
import Keys from "src/routes/Keys.svelte"
import RelayList from "src/routes/RelayList.svelte"
import AddRelay from "src/routes/AddRelay.svelte"
import UserDetail from "src/routes/UserDetail.svelte"
import UserAdvanced from "src/routes/UserAdvanced.svelte"
// import UserDetail from "src/routes/UserDetail.svelte"
// import UserAdvanced from "src/routes/UserAdvanced.svelte"
import NoteCreate from "src/routes/NoteCreate.svelte"
import Chat from "src/routes/Chat.svelte"
import ChatRoom from "src/routes/ChatRoom.svelte"
import ChatEdit from "src/routes/ChatEdit.svelte"
// import Chat from "src/routes/Chat.svelte"
// import ChatRoom from "src/routes/ChatRoom.svelte"
// import ChatEdit from "src/routes/ChatEdit.svelte"
const menuIsOpen = writable(false)
const toggleMenu = () => menuIsOpen.update(x => !x)
@ -43,7 +43,7 @@
let menuIcon
let scrollY
let suspendedSubs = []
let mostRecentAlert = epoch
let mostRecentAlert = 0
export let url = ""
@ -96,13 +96,16 @@
<div use:links class="h-full">
<div class="pt-16 text-white h-full">
<Route path="/alerts" component={Alerts} />
<!--
<Route path="/search/:type" let:params>
{#key params.type}
<Search {...params} />
{/key}
</Route>
-->
<Route path="/notes/:activeTab" component={Notes} />
<Route path="/notes/new" component={NoteCreate} />
<!--
<Route path="/chat" component={Chat} />
<Route path="/chat/new" component={ChatEdit} />
<Route path="/chat/:room" let:params>
@ -116,6 +119,7 @@
<UserDetail {...params} />
{/key}
</Route>
-->
<Route path="/keys" component={Keys} />
<Route path="/relays" component={RelayList} />
<Route path="/profile" component={Profile} />

View File

@ -1,27 +0,0 @@
import {last} from 'ramda'
import {ensurePlural, first} from 'hurdak/lib/hurdak'
export const filterTags = (where, events) =>
ensurePlural(events)
.flatMap(
e => e.tags.filter(t => {
if (where.tag && where.tag !== t[0]) {
return false
}
if (where.type && where.type !== last(t)) {
return false
}
return true
}).map(t => t[1])
)
export const findTag = (where, events) => first(filterTags(where, events))
// Support the deprecated version where tags are not marked as replies
export const findReply = e =>
findTag({tag: "e", type: "reply"}, e) || findTag({tag: "e"}, e)
export const findRoot = e =>
findTag({tag: "e", type: "root"}, e)

View File

@ -4,13 +4,12 @@
import {slide} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {hasParent, findLink} from 'src/util/html'
import {renderNote} from 'src/util/notes'
import {findReply} from "src/util/nostr"
import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import relay from 'src/relay'
import {dispatch} from "src/state/dispatch"
import {findReply} from "src/state/nostr"
import {accounts, settings, modal} from "src/state/app"
import {settings, modal} from "src/state/app"
import {user} from "src/state/user"
import {formatTimestamp} from 'src/util/misc'
import UserBadge from "src/partials/UserBadge.svelte"
@ -32,6 +31,7 @@
const likes = liveQuery(() => relay.countReactions(note.id, {content: "+"}))
const flags = liveQuery(() => relay.countReactions(note.id, {content: "-"}))
const replies = liveQuery(() => relay.filterReplies(note.id))
const account = liveQuery(() => relay.db.users.get(note.pubkey))
relay.ensureContext(note)
@ -98,7 +98,7 @@
<Card on:click={onClick} {interactive} {invertColors}>
<div class="flex gap-4 items-center justify-between">
<UserBadge user={{...$accounts[note.pubkey], pubkey: note.pubkey}} />
<UserBadge user={{...$account, pubkey: note.pubkey}} />
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
</div>
<div class="ml-6 flex flex-col gap-2">
@ -114,7 +114,10 @@
</p>
{:else}
<p class="text-ellipsis overflow-hidden">
{@html renderNote(note, {showEntire})}
{#await relay.renderNote(note, {showEntire})}
{:then content}
{@html content}
{/await}
{#if link}
<div class="mt-2" on:click={e => e.stopPropagation()}>
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />

View File

@ -1,8 +1,7 @@
<script>
import {liveQuery} from 'dexie'
import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
import {createMap} from 'hurdak/lib/hurdak'
import {findReply, findRoot} from 'src/nostr/tags'
import {findReply, findRoot} from 'src/util/nostr'
import {fly} from 'svelte/transition'
import Note from "src/partials/Note.svelte"
import relay from 'src/relay'
@ -39,22 +38,22 @@
}
const notes = relay.lq(async () => {
let data = relay.filterEvents(filter).limit(10)
if (shouldMuffle) {
data = data
}
return await toThread(await data.reverse().sortBy('created_at'))
return await toThread(
await relay.filterEvents(filter).limit(10).reverse().sortBy('created_at')
)
})
</script>
<!-- <svelte:window on:scroll={scroller?.start} /> -->
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
{#each ($notes || []) as n (n.id)}
<li><Note interactive note={n} depth={2} /></li>
{:else}
<li class="p-20 text-center" in:fly={{y: 20}}>No notes found.</li>
{/each}
</ul>
{#if $notes?.length === 0}
<div in:fly={{y: 20}} class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-md">
No notes found.
</div>
</div>
{/if}

View File

@ -1,7 +1,5 @@
import Dexie from 'dexie'
import {defmulti} from 'hurdak/lib/hurdak'
import {filterTags} from 'src/nostr/tags'
import {worker} from 'src/relay/worker'
import {filterTags} from 'src/util/nostr'
export const db = new Dexie('coracle/relay')
@ -36,13 +34,3 @@ db.events.hook('creating', (id, e, t) => {
}
})
})
// Listen to worker
const withPayload = f => e => f(e.data.payload)
worker.onmessage = defmulti('onmessage', e => e.data.topic)
worker.onmessage.addMethod('events/put', withPayload(e => {
db.events.put(e)
}))

View File

@ -1,9 +1,11 @@
import {liveQuery} from 'dexie'
import {pluck, isNil} from 'ramda'
import {ensurePlural, first} from 'hurdak/lib/hurdak'
import {ensurePlural, createMap, ellipsize, first} from 'hurdak/lib/hurdak'
import {now, timedelta} from 'src/util/misc'
import {escapeHtml} from 'src/util/html'
import {filterTags} from 'src/util/nostr'
import {db} from 'src/relay/db'
import {worker} from 'src/relay/worker'
import pool from 'src/relay/pool'
// Livequery appears to swallow errors
const lq = f => liveQuery(async () => {
@ -20,11 +22,11 @@ const ensureContext = async e => {
// Throttle updates for users
if (!user || user.updated_at < now() - timedelta(1, 'hours')) {
worker.post('user/update', user || {pubkey: e.pubkey, updated_at: 0})
await pool.updateUser(user || {pubkey: e.pubkey, updated_at: 0})
}
// TODO optimize this like user above so we're not double-fetching
worker.post('event/fetchContext', e)
await pool.fetchContext(e)
}
const prefilterEvents = filter => {
@ -77,7 +79,31 @@ const findReaction = async (id, filter) =>
const countReactions = async (id, filter) =>
(await filterReactions(id, filter)).length
export default {
db, worker, lq, ensureContext, filterEvents, filterReactions, countReactions,
findReaction, filterReplies,
const renderNote = async (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire
const content = shouldEllipsize ? ellipsize(note.content, 500) : note.content
const accounts = await db.users.where('pubkey').anyOf(filterTags({tag: "p"}, note)).toArray()
const accountsByPubkey = createMap('pubkey', accounts)
return escapeHtml(content)
.replace(/\n/g, '<br />')
.replace(/https?:\/\/([\w.-]+)[^ ]*/g, (url, domain) => {
return `<a href="${url}" target="_blank noopener" class="underline">${domain}</a>`
})
.replace(/#\[(\d+)\]/g, (tag, i) => {
if (!note.tags[parseInt(i)]) {
return tag
}
const pubkey = note.tags[parseInt(i)][1]
const user = accountsByPubkey[pubkey]
const name = user?.name || pubkey.slice(0, 8)
return `@<a href="/users/${pubkey}/notes" class="underline">${name}</a>`
})
}
export default {
db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions,
findReaction, filterReplies, renderNote,
}

View File

@ -1,7 +1,8 @@
import {relayPool} from 'nostr-tools'
import {defmulti, noop, uuid} from 'hurdak/lib/hurdak'
import {relayPool, getPublicKey} from 'nostr-tools'
import {noop, switcherFn, uuid} from 'hurdak/lib/hurdak'
import {now, timedelta} from "src/util/misc"
import {filterTags} from "src/nostr/tags"
import {filterTags} from "src/util/nostr"
import {db} from 'src/relay/db'
// ============================================================================
// Utils/config
@ -49,24 +50,24 @@ req({
// ============================================================================
// Listen to messages posted from the main application
const withPayload = f => e => f(e.data.payload)
const getPubkey = () => {
return pool._pubkey || getPublicKey(pool._privkey)
}
onmessage = defmulti('self', e => e.data.topic)
onmessage.addMethod('pool/addRelay', withPayload(url => {
const addRelay = url => {
pool.addRelay(url)
}))
}
onmessage.addMethod('pool/removeRelay', withPayload(url => {
const removeRelay = url => {
pool.removeRelay(url)
}))
}
onmessage.addMethod('pool/setPrivateKey', withPayload(privkey => {
const setPrivateKey = privkey => {
pool.setPrivateKey(privkey)
pool._privkey = privkey
}))
}
onmessage.addMethod('pool/setPublicKey', withPayload(pubkey => {
const setPublicKey = pubkey => {
// TODO fix this, it ain't gonna work
pool.registerSigningFunction(async event => {
const {sig} = await window.nostr.signEvent(event)
@ -75,35 +76,41 @@ onmessage.addMethod('pool/setPublicKey', withPayload(pubkey => {
})
pool._pubkey = pubkey
}))
}
onmessage.addMethod('event/publish', withPayload(event => {
const publishEvent = event => {
pool.publish(event)
post('events/put', event)
}))
db.events.put(event)
}
onmessage.addMethod('user/update', withPayload(async user => {
const updateUser = async user => {
if (!user.pubkey) throw new Error("Invalid user")
user = {muffle: [], petnames: [], ...user}
const sub = req({
filter: {kinds: [0], authors: [user.pubkey], since: user.updated_at},
filter: {
kinds: [0, 3, 12165],
authors: [user.pubkey],
since: user.updated_at,
},
onEvent: e => {
try {
Object.assign(user, JSON.parse(e.content))
} catch (e) {
// pass
}
switcherFn(e.kind, {
0: () => Object.assign(user, JSON.parse(e.content)),
3: () => Object.assign(user, {petnames: e.tags}),
12165: () => Object.assign(user, {muffle: e.tags}),
})
},
onEose: () => {
sub.unsub()
post('users/put', {...user, updated_at: now()})
db.users.put({...user, updated_at: now()})
},
})
}))
}
onmessage.addMethod('event/fetchContext', withPayload(async event => {
const fetchContext = async event => {
const sub = req({
filter: [
{kinds: [5, 7], '#e': [event.id]},
@ -112,4 +119,9 @@ onmessage.addMethod('event/fetchContext', withPayload(async event => {
onEvent: e => post('events/put', e),
onEose: () => sub.unsub(),
})
}))
}
export default {
getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey,
publishEvent, updateUser, fetchContext,
}

View File

@ -1,7 +0,0 @@
const url = new URL('src/worker/index.js', import.meta.url)
export const worker = new Worker(url, {type: 'module'})
worker.post = (topic, payload) => worker.postMessage({topic, payload})
window.worker = worker

View File

@ -2,8 +2,7 @@
import {fly} from 'svelte/transition'
import {registerRelay} from 'src/state/nostr'
import toast from 'src/state/toast'
import {user} from 'src/state/user'
import {modal, ensureAccounts} from 'src/state/app'
import {modal} from 'src/state/app'
import {dispatch} from 'src/state/dispatch'
import Input from 'src/partials/Input.svelte'
import Button from 'src/partials/Button.svelte'
@ -21,10 +20,6 @@
registerRelay(url)
dispatch("relay/join", url)
modal.set(null)
if ($user) {
ensureAccounts([$user.pubkey], {force: true})
}
}
</script>

View File

@ -1,74 +1,35 @@
<script>
import {onMount, onDestroy} from 'svelte'
import {fly} from 'svelte/transition'
import {writable} from 'svelte/store'
import {sortBy, uniqBy, prop} from 'ramda'
import {now} from 'src/util/misc'
import {annotateAlerts, notesListener, createScroller} from 'src/util/notes'
import {findReply} from 'src/util/nostr'
import {ellipsize} from 'hurdak/src/core'
import relay from 'src/relay'
import {user} from 'src/state/user'
import {Cursor, epoch} from 'src/state/nostr'
import {alerts, modal} from 'src/state/app'
import Spinner from "src/partials/Spinner.svelte"
import UserBadge from "src/partials/UserBadge.svelte"
import Note from 'src/partials/Note.svelte'
let cursor
let listener
let scroller
let interval
let modalUnsub
let loading = true
let events = writable([])
const events = relay.lq(async () => {
const events = await relay
.filterEvents({kinds: [1, 7], '#p': [$user.pubkey]})
.limit(10)
.reverse()
.sortBy('created_at')
onMount(async () => {
// Clear notification badge
alerts.set({...$alerts, since: now()})
cursor = new Cursor({kinds: [1, 7], '#p': [$user.pubkey]})
listener = await notesListener(events, [{kinds: [1, 5, 7]}], {repliesOnly: true})
scroller = createScroller(cursor, async chunk => {
// Add chunk context
chunk = await annotateAlerts(chunk)
// Sort and deduplicate
events.set(sortBy(n => -n.created_at, uniqBy(prop('id'), $events.concat(chunk))))
})
// Track loading based on cursor cutoff date
interval = setInterval(() => {
loading = cursor.since > epoch
}, 1000)
modalUnsub = modal.subscribe(async $modal => {
if ($modal) {
cursor.stop()
listener.stop()
scroller.stop()
} else {
cursor.start()
listener.start()
scroller.start()
}
})
return events
// Add parent in
.map(e => ({...e, parent: relay.filterEvents({ids: [findReply(e)]}).first()}))
// Only show stuff if it's a direct reply to my note
.filter(e => e.parent?.pubkey === $user.pubkey)
})
onDestroy(() => {
cursor?.stop()
listener?.stop()
scroller?.stop()
modalUnsub?.()
clearInterval(interval)
})
// Clear notification badge
alerts.set({...$alerts, since: now()})
</script>
<svelte:window on:scroll={scroller?.start} />
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
{#each $events as e (e.id)}
{#each ($events || []) as e (e.id)}
{#if e.kind === 7}
<!-- don't show alerts for likes of replies to this user's notes -->
{#if e.parent?.pubkey === $user.pubkey}
<li
in:fly={{y: 20}}
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all
@ -82,16 +43,14 @@
{ellipsize(e.parent.content, 240)}
</div>
</li>
{/if}
{:else}
<li in:fly={{y: 20}}><Note showParent note={e} /></li>
{/if}
{/each}
</ul>
{#if loading}
<div in:fly={{y: 20}}><Spinner /></div>
{:else if $events.length === 0}
{#if $events?.length === 0}
<div in:fly={{y: 20}} class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-md">
No recent activity found.

View File

@ -1,11 +1,9 @@
import {uniq} from 'ramda'
import {writable, get} from 'svelte/store'
import {writable} from 'svelte/store'
import {navigate} from "svelte-routing"
import {globalHistory} from "svelte-routing/src/history"
import {switcherFn} from 'hurdak/lib/hurdak'
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
import {user} from 'src/state/user'
import {channels, relays} from 'src/state/nostr'
import {relays} from 'src/state/nostr'
export const modal = {
subscribe: cb => {
@ -55,60 +53,3 @@ export const logout = () => {
navigate("/login")
}, 200)
}
// Accounts
export const accounts = writable(getLocalJson("coracle/accounts") || {})
accounts.subscribe($accounts => {
setLocalJson("coracle/accounts", $accounts)
})
user.subscribe($user => {
if ($user) {
accounts.update($accounts => ({...$accounts, [$user.pubkey]: $user}))
}
})
export const ensureAccounts = async (pubkeys, {force = false} = {}) => {
const $accounts = get(accounts)
// Don't request accounts we recently updated
pubkeys = pubkeys.filter(
k => force || !$accounts[k] || $accounts[k].refreshed < now() - timedelta(10, 'minutes')
)
if (pubkeys.length) {
const events = await channels.getter.all({kinds: [0, 3, 12165], authors: uniq(pubkeys)})
await accounts.update($accounts => {
events.forEach(e => {
const values = {
muffle: [],
petnames: [],
...$accounts[e.pubkey],
pubkey: e.pubkey,
refreshed: now(),
isUser: true,
}
switcherFn(e.kind, {
0: () => {
$accounts[e.pubkey] = {...values, ...JSON.parse(e.content)}
},
3: () => {
$accounts[e.pubkey] = {...values, petnames: e.tags}
},
12165: () => {
$accounts[e.pubkey] = {...values, muffle: e.tags}
},
})
})
return $accounts
})
}
// Keep our user in sync
user.update($user => $user ? {...$user, ...get(accounts)[$user.pubkey]} : null)
}

View File

@ -3,8 +3,7 @@ import {get} from 'svelte/store'
import {first, defmulti} from "hurdak/lib/hurdak"
import {user} from "src/state/user"
import relay from 'src/relay'
import {nostr, relays} from 'src/state/nostr'
import {ensureAccounts} from 'src/state/app'
import {relays} from 'src/state/nostr'
// Commands are processed in two layers:
// - App-oriented commands are created via dispatch
@ -35,7 +34,7 @@ dispatch.addMethod("account/update", async (topic, updates) => {
user.set({...get(user), ...updates})
// Tell the network
await relay.worker.post('event/publish', nostr.event(0, JSON.stringify(updates)))
await relay.pool.publishEvent(makeEvent(0, JSON.stringify(updates)))
})
dispatch.addMethod("account/petnames", async (topic, petnames) => {
@ -45,7 +44,7 @@ dispatch.addMethod("account/petnames", async (topic, petnames) => {
user.set({...$user, petnames})
// Tell the network
await relay.worker.post('event/publish', nostr.event(3, '', petnames))
await relay.pool.publishEvent(makeEvent(3, '', petnames))
})
dispatch.addMethod("account/muffle", async (topic, muffle) => {
@ -55,17 +54,13 @@ dispatch.addMethod("account/muffle", async (topic, muffle) => {
user.set({...$user, muffle})
// Tell the network
await relay.worker.post('event/publish', nostr.event(12165, '', muffle))
await relay.pool.publishEvent(makeEvent(12165, '', muffle))
})
dispatch.addMethod("relay/join", async (topic, url) => {
const $user = get(user)
relays.update(r => r.concat(url))
if ($user) {
await ensureAccounts([$user.pubkey], {force: true})
}
})
dispatch.addMethod("relay/leave", (topic, url) => {
@ -73,59 +68,59 @@ dispatch.addMethod("relay/leave", (topic, url) => {
})
dispatch.addMethod("room/create", async (topic, room) => {
const event = nostr.event(40, JSON.stringify(room))
const event = makeEvent(40, JSON.stringify(room))
await relay.worker.post('event/publish', event)
await relay.pool.publishEvent(event)
return event
})
dispatch.addMethod("room/update", async (topic, {id, ...room}) => {
const event = nostr.event(41, JSON.stringify(room), [t("e", id)])
const event = makeEvent(41, JSON.stringify(room), [t("e", id)])
await relay.worker.post('event/publish', event)
await relay.pool.publishEvent(event)
return event
})
dispatch.addMethod("message/create", async (topic, roomId, content) => {
const event = nostr.event(42, content, [t("e", roomId, "root")])
const event = makeEvent(42, content, [t("e", roomId, "root")])
await relay.worker.post('event/publish', event)
await relay.pool.publishEvent(event)
return event
})
dispatch.addMethod("note/create", async (topic, content, tags=[]) => {
const event = nostr.event(1, content, tags)
const event = makeEvent(1, content, tags)
await relay.worker.post('event/publish', event)
await relay.pool.publishEvent(event)
return event
})
dispatch.addMethod("reaction/create", async (topic, content, e) => {
const tags = copyTags(e, [t("p", e.pubkey), t("e", e.id, 'reply')])
const event = nostr.event(7, content, tags)
const event = makeEvent(7, content, tags)
await relay.worker.post('event/publish', event)
await relay.pool.publishEvent(event)
return event
})
dispatch.addMethod("reply/create", async (topic, content, e) => {
const tags = copyTags(e, [t("p", e.pubkey), t("e", e.id, 'reply')])
const event = nostr.event(1, content, tags)
const event = makeEvent(1, content, tags)
await relay.worker.post('event/publish', event)
await relay.pool.publishEvent(event)
return event
})
dispatch.addMethod("event/delete", async (topic, ids) => {
const event = nostr.event(5, '', ids.map(id => t("e", id)))
const event = makeEvent(5, '', ids.map(id => t("e", id)))
await relay.worker.post('event/publish', event)
await relay.pool.publishEvent(event)
return event
})
@ -149,3 +144,10 @@ export const t = (type, content, marker) => {
return tag
}
export const makeEvent = (kind, content = '', tags = []) => {
const pubkey = relay.pool.getPubkey()
const createdAt = Math.round(new Date().valueOf() / 1000)
return {kind, content, tags, pubkey, created_at: createdAt}
}

View File

@ -1,56 +1,9 @@
import {writable, get} from 'svelte/store'
import {relayPool, getPublicKey} from 'nostr-tools'
import {assoc, last, find, intersection, uniqBy, prop} from 'ramda'
import {first, noop, ensurePlural} from 'hurdak/lib/hurdak'
import {assoc, uniqBy, prop} from 'ramda'
import {noop, ensurePlural} from 'hurdak/lib/hurdak'
import relay from 'src/relay'
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
export const nostr = relayPool()
export const epoch = 1633046400
export const filterTags = (where, events) =>
ensurePlural(events)
.flatMap(
e => e.tags.filter(t => {
if (where.tag && where.tag !== t[0]) {
return false
}
if (where.type && where.type !== last(t)) {
return false
}
return true
}).map(t => t[1])
)
export const findTag = (where, events) => first(filterTags(where, events))
// Support the deprecated version where tags are not marked as replies
export const findReply = e =>
findTag({tag: "e", type: "reply"}, e) || findTag({tag: "e"}, e)
export const findRoot = e =>
findTag({tag: "e", type: "root"}, e)
export const filterMatches = (filter, e) => {
return Boolean(find(
f => {
return (
(!f.ids || f.ids.includes(e.id))
&& (!f.authors || f.authors.includes(e.pubkey))
&& (!f.kinds || f.kinds.includes(e.kind))
&& (!f['#e'] || intersection(f['#e'], e.tags.filter(t => t[0] === 'e').map(t => t[1])))
&& (!f['#p'] || intersection(f['#p'], e.tags.filter(t => t[0] === 'p').map(t => t[1])))
&& (!f.since || f.since >= e.created_at)
&& (!f.until || f.until <= e.created_at)
)
},
ensurePlural(filter)
))
}
export class Channel {
constructor(name) {
this.name = name
@ -72,7 +25,7 @@ export class Channel {
let resolve
const eoseRelays = []
const sub = nostr.sub({filter, cb}, this.name, r => {
const sub = relay.pool.sub({filter, cb}, this.name, r => {
eoseRelays.push(r)
if (eoseRelays.length === get(relays).length) {
@ -223,28 +176,6 @@ export class Listener {
}
}
// Augment nostr with some extra methods
nostr.login = privkey => {
nostr.setPrivateKey(privkey)
nostr._privkey = privkey
}
nostr.pubkeyLogin = pubkey => {
nostr.registerSigningFunction( async (event) => {
const {sig} = await window.nostr.signEvent(event)
return sig
})
nostr._pubkey = pubkey
}
nostr.event = (kind, content = '', tags = []) => {
const pubkey = nostr._pubkey || getPublicKey(nostr._privkey)
const createdAt = Math.round(new Date().valueOf() / 1000)
return {kind, content, tags, pubkey, created_at: createdAt}
}
// Keep track of known relays
export const knownRelays = writable((getLocalJson("coracle/knownRelays") || [
@ -293,15 +224,13 @@ let prevRelays = []
relays.subscribe($relays => {
prevRelays.forEach(url => {
if (!$relays.includes(url)) {
nostr.removeRelay(url)
relay.worker.post('pool/removeRelay', url)
relay.pool.removeRelay(url)
}
})
$relays.forEach(url => {
if (!prevRelays.includes(url)) {
nostr.addRelay(url)
relay.worker.post('pool/addRelay', url)
relay.pool.addRelay(url)
}
})

View File

@ -1,6 +1,5 @@
import {writable} from "svelte/store"
import {getLocalJson, setLocalJson} from "src/util/misc"
import {nostr} from 'src/state/nostr'
import relay from 'src/relay'
export const user = writable(getLocalJson("coracle/user"))
@ -10,11 +9,9 @@ user.subscribe($user => {
// Keep nostr in sync
if ($user?.privkey) {
nostr.login($user.privkey)
relay.worker.post('pool/setPrivateKey', $user.privkey)
relay.pool.setPrivateKey($user.privkey)
} else if ($user?.pubkey) {
nostr.pubkeyLogin($user.pubkey)
relay.worker.post('pool/setPublicKey', $user.pubkey)
relay.pool.setPublicKey($user.pubkey)
}
// Migrate data from old formats

62
src/util/nostr.js Normal file
View File

@ -0,0 +1,62 @@
import {last, intersection} from 'ramda'
import {ensurePlural, first} from 'hurdak/lib/hurdak'
export const epoch = 1633046400
export const filterTags = (where, events) =>
ensurePlural(events)
.flatMap(
e => e.tags.filter(t => {
if (where.tag && where.tag !== t[0]) {
return false
}
if (where.type && where.type !== last(t)) {
return false
}
return true
}).map(t => t[1])
)
export const findTag = (where, events) => first(filterTags(where, events))
// Support the deprecated version where tags are not marked as replies
export const findReply = e =>
findTag({tag: "e", type: "reply"}, e) || findTag({tag: "e"}, e)
export const findRoot = e =>
findTag({tag: "e", type: "root"}, e)
export const filterMatches = (filter, e) => {
return Boolean(find(
f => {
return (
(!f.ids || f.ids.includes(e.id))
&& (!f.authors || f.authors.includes(e.pubkey))
&& (!f.kinds || f.kinds.includes(e.kind))
&& (!f['#e'] || intersection(f['#e'], e.tags.filter(t => t[0] === 'e').map(t => t[1])))
&& (!f['#p'] || intersection(f['#p'], e.tags.filter(t => t[0] === 'p').map(t => t[1])))
&& (!f.since || f.since >= e.created_at)
&& (!f.until || f.until <= e.created_at)
)
},
ensurePlural(filter)
))
}
export const getMuffleValue = pubkey => {
const $user = get(user)
if (!$user) {
return 1
}
const tag = find(t => t[1] === pubkey, $user.muffle)
if (!tag) {
return 1
}
return parseFloat(last(tag))
}

View File

@ -1,111 +1,10 @@
import {identity, uniq, concat, propEq, uniqBy, prop, groupBy, find, last, pluck} from 'ramda'
import {identity, uniq, propEq, uniqBy, prop, groupBy, pluck} from 'ramda'
import {debounce} from 'throttle-debounce'
import {get} from 'svelte/store'
import {switcherFn, ellipsize, createMap} from 'hurdak/lib/hurdak'
import {getMuffleValue, epoch, filterMatches, findReply} from 'src/util/nostr'
import {switcherFn, createMap} from 'hurdak/lib/hurdak'
import {timedelta, sleep} from "src/util/misc"
import {escapeHtml} from 'src/util/html'
import {user} from 'src/state/user'
import {epoch, filterMatches, Listener, channels, findReply, findRoot} from 'src/state/nostr'
import {accounts, ensureAccounts} from 'src/state/app'
export const renderNote = (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire
const content = shouldEllipsize ? ellipsize(note.content, 500) : note.content
const $accounts = get(accounts)
return escapeHtml(content)
.replace(/\n/g, '<br />')
.replace(/https?:\/\/([\w.-]+)[^ ]*/g, (url, domain) => {
return `<a href="${url}" target="_blank noopener" class="underline">${domain}</a>`
})
.replace(/#\[(\d+)\]/g, (tag, i) => {
if (!note.tags[parseInt(i)]) {
return tag
}
const pubkey = note.tags[parseInt(i)][1]
const user = $accounts[pubkey]
const name = user?.name || pubkey.slice(0, 8)
return `@<a href="/users/${pubkey}/notes" class="underline">${name}</a>`
})
}
export const getMuffleValue = pubkey => {
const $user = get(user)
if (!$user) {
return 1
}
const tag = find(t => t[1] === pubkey, $user.muffle)
if (!tag) {
return 1
}
return parseFloat(last(tag))
}
export const threadify = async notes => {
if (notes.length === 0) {
return []
}
const noteIds = pluck('id', notes)
const rootIds = notes.map(findReply)
const parentIds = notes.map(findRoot)
const ancestorIds = concat(rootIds, parentIds).filter(identity)
// Find all direct parents and thread roots
const filters = ancestorIds.length === 0
? [{kinds: [1, 7], '#e': noteIds}]
: [{kinds: [1], ids: ancestorIds},
{kinds: [1, 7], '#e': noteIds.concat(ancestorIds)}]
const events = await channels.getter.all(filters)
await ensureAccounts(uniq(pluck('pubkey', notes.concat(events))))
const $accounts = get(accounts)
const reactionsByParent = groupBy(findReply, events.filter(propEq('kind', 7)))
const allNotes = uniqBy(prop('id'), notes.concat(events.filter(propEq('kind', 1))))
const notesById = createMap('id', allNotes)
const notesByRoot = groupBy(
n => {
const rootId = findRoot(n)
const parentId = findReply(n)
// Actually dereference the notes in case we weren't able to retrieve them
if (notesById[rootId]) {
return rootId
}
if (notesById[parentId]) {
return parentId
}
return n.id
},
allNotes
)
const threads = []
for (const [rootId, _notes] of Object.entries(notesByRoot)) {
const annotate = note => {
return {
...note,
user: $accounts[note.pubkey],
reactions: reactionsByParent[note.id] || [],
children: uniqBy(prop('id'), _notes.filter(n => findReply(n) === note.id)).map(annotate),
}
}
threads.push(annotate(notesById[rootId]))
}
return threads
}
import {Listener, channels} from 'src/state/nostr'
export const annotateNotes = async (notes, {showParent = false} = {}) => {
if (notes.length === 0) {
@ -149,41 +48,6 @@ export const annotateNotes = async (notes, {showParent = false} = {}) => {
})
}
export const annotateAlerts = async events => {
if (events.length === 0) {
return []
}
const eventIds = pluck('id', events)
const parentIds = events.map(findReply).filter(identity)
const filters = [
{kinds: [1], ids: parentIds},
{kinds: [7], '#e': parentIds},
{kinds: [1, 7], '#e': eventIds},
]
const relatedEvents = await channels.getter.all(filters)
await ensureAccounts(uniq(pluck('pubkey', events.concat(relatedEvents))))
const $accounts = get(accounts)
const reactionsByParent = groupBy(findReply, relatedEvents.filter(e => e.kind === 7 && e.content === '+'))
const allNotes = uniqBy(prop('id'), events.concat(relatedEvents).filter(propEq('kind', 1)))
const notesById = createMap('id', allNotes)
const annotate = note => ({
...note,
user: $accounts[note.pubkey],
reactions: reactionsByParent[note.id] || [],
children: uniqBy(prop('id'), allNotes.filter(n => findReply(n) === note.id)).map(annotate),
})
return uniqBy(e => e.parent?.id || e.id, events.map(event => {
const parentId = findReply(event)
return {...annotate(event), parent: annotate(notesById[parentId])}
}))
}
export const annotateNewNote = async (note) => {
await ensureAccounts([note.pubkey])