mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-19 11:43:35 +00:00
Move pool out of worker
This commit is contained in:
parent
9f6601cbdc
commit
f4a706e12b
@ -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} />
|
||||
|
@ -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)
|
@ -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} />
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
}))
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
@ -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
|
@ -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>
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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
62
src/util/nostr.js
Normal 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))
|
||||
}
|
@ -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])
|
||||
|
Loading…
Reference in New Issue
Block a user