mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Re-write everything again
This commit is contained in:
parent
e3cf09ce50
commit
3dae3494dd
24
README.md
24
README.md
@ -32,6 +32,8 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
- https://github.com/nbd-wtf/nostr-tools/blob/master/nip19.ts
|
||||
- [ ] Support key delegation
|
||||
- https://github.com/nbd-wtf/nostr-tools/blob/master/nip26.ts
|
||||
- [ ] Add relay selector when publishing a note
|
||||
- [ ] Add keyword mutes
|
||||
|
||||
# Bugs
|
||||
|
||||
@ -46,9 +48,25 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
|
||||
## Current
|
||||
|
||||
- [ ] Upgrade nostr-tools
|
||||
- [ ] Close connections that haven't been used in a while
|
||||
- [ ] Move key management stuff
|
||||
- [x] Upgrade nostr-tools
|
||||
- [ ] Publish user relays using nip 23
|
||||
- [ ] Use user relays for feeds
|
||||
- [ ] Publish to user relays + target relays:
|
||||
- If a reply or reaction, publish to the parent event's best relay, which is:
|
||||
- e tag relay
|
||||
- p tag relay
|
||||
- or pubkey's recommended relays
|
||||
- [ ] Add recommended relay to tags
|
||||
- [ ] Close connections that haven't been used in a while
|
||||
- [ ] Support some read/write config on relays page
|
||||
- [ ] Get real home relays for default pubkeys
|
||||
- [ ] Add settings storage
|
||||
- [ ] Use hexToBech32 from nostr-tools
|
||||
- [ ] Warn that everything will be cleared on logout
|
||||
- [ ] Clear dexie on page load, we don't need any persistence other than people/relays
|
||||
- [ ] Clean up login page to prefer extension, make private key entry "advanced"
|
||||
- [ ] Do I need to implement re-connecting now?
|
||||
- [ ] handle localstorage limits https://stackoverflow.com/questions/2989284/what-is-the-max-size-of-localstorage-values
|
||||
|
||||
## 0.2.6
|
||||
|
||||
|
@ -6,15 +6,13 @@
|
||||
import {writable, get} from "svelte/store"
|
||||
import {fly, fade} from "svelte/transition"
|
||||
import {cubicInOut} from "svelte/easing"
|
||||
import {throttle} from 'throttle-debounce'
|
||||
import {Router, Route, links, navigate} from "svelte-routing"
|
||||
import {globalHistory} from "svelte-routing/src/history"
|
||||
import {hasParent} from 'src/util/html'
|
||||
import {displayPerson, isLike} from 'src/util/nostr'
|
||||
import {timedelta, now} from 'src/util/misc'
|
||||
import {store as toast} from "src/state/toast"
|
||||
import {modal, settings, alerts} from "src/state/app"
|
||||
import relay, {user, connections} from 'src/relay'
|
||||
import {user} from 'src/agent'
|
||||
import {modal, toast, settings, alerts} from "src/app"
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import NoteDetail from "src/views/NoteDetail.svelte"
|
||||
import PersonSettings from "src/views/PersonSettings.svelte"
|
||||
@ -32,8 +30,6 @@
|
||||
import Person from "src/routes/Person.svelte"
|
||||
import NoteCreate from "src/routes/NoteCreate.svelte"
|
||||
|
||||
window.relay = relay
|
||||
|
||||
export let url = ""
|
||||
|
||||
const menuIsOpen = writable(false)
|
||||
@ -45,7 +41,7 @@
|
||||
let menuIcon
|
||||
let scrollY
|
||||
let suspendedSubs = []
|
||||
let mostRecentAlert = $alerts.since
|
||||
let {since, latest} = alerts
|
||||
|
||||
onMount(() => {
|
||||
// Close menu on click outside
|
||||
@ -55,35 +51,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
let prevPubkey = null
|
||||
|
||||
const unsubUser = user.subscribe($user => {
|
||||
if ($user && $user.pubkey !== prevPubkey) {
|
||||
relay.pool.syncNetwork()
|
||||
relay.pool.listenForEvents(
|
||||
'App/alerts',
|
||||
[{kinds: [1, 7], '#p': [$user.pubkey], since: mostRecentAlert}],
|
||||
e => {
|
||||
// Don't alert about people's own stuff
|
||||
if (e.pubkey === $user.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only notify users about positive reactions
|
||||
if (e.kind === 7 && !isLike(e.content)) {
|
||||
return
|
||||
}
|
||||
|
||||
relay.loadNotesContext([e])
|
||||
|
||||
mostRecentAlert = Math.max(e.created_at, mostRecentAlert)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
prevPubkey = $user?.pubkey
|
||||
})
|
||||
|
||||
const unsubModal = modal.subscribe($modal => {
|
||||
// Keep scroll position on body, but don't allow scrolling
|
||||
if ($modal) {
|
||||
@ -101,7 +68,6 @@
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubUser()
|
||||
unsubModal()
|
||||
}
|
||||
})
|
||||
@ -153,7 +119,7 @@
|
||||
<li class="cursor-pointer relative">
|
||||
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/alerts">
|
||||
<i class="fa-solid fa-bell mr-2" /> Alerts
|
||||
{#if mostRecentAlert > $alerts.since}
|
||||
{#if $latest > $since}
|
||||
<div class="w-2 h-2 rounded bg-accent absolute top-3 left-6" />
|
||||
{/if}
|
||||
</a>
|
||||
@ -209,7 +175,7 @@
|
||||
<img src="/images/favicon.png" class="w-8" />
|
||||
<h1 class="staatliches text-3xl">Coracle</h1>
|
||||
</Anchor>
|
||||
{#if mostRecentAlert > $alerts.since}
|
||||
{#if $latest > $since}
|
||||
<div class="w-2 h-2 rounded bg-accent absolute top-4 left-12" />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Dexie from 'dexie'
|
||||
import {writable, get} from 'svelte/store'
|
||||
import {writable} from 'svelte/store'
|
||||
import {groupBy, prop, flatten, pick} from 'ramda'
|
||||
import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
|
||||
import {now, timedelta, getLocalJson, setLocalJson} from 'src/util/misc'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
import {filterTags, findReply, findRoot} from 'src/util/nostr'
|
||||
|
||||
export const db = new Dexie('coracle/relay')
|
||||
export const db = new Dexie('agent/data/db')
|
||||
|
||||
db.version(6).stores({
|
||||
relays: '++url, name',
|
||||
@ -13,23 +13,20 @@ db.version(6).stores({
|
||||
tags: '++key, event, value, created_at, loaded_at',
|
||||
})
|
||||
|
||||
window.db = db
|
||||
|
||||
// Some things work better as observables than database tables
|
||||
|
||||
db.user = writable(getLocalJson("db/user"))
|
||||
db.people = writable(getLocalJson('db/people') || {})
|
||||
db.network = writable(getLocalJson('db/network') || [])
|
||||
db.connections = writable(getLocalJson("db/connections") || [])
|
||||
export const people = writable({})
|
||||
|
||||
db.user.subscribe($user => setLocalJson("db/user", $user))
|
||||
db.people.subscribe($people => setLocalJson("db/people", $people))
|
||||
db.network.subscribe($network => setLocalJson("db/network", $network))
|
||||
db.connections.subscribe($connections => setLocalJson("db/connections", $connections))
|
||||
let $people = {}
|
||||
people.subscribe($p => {
|
||||
$people = $p
|
||||
})
|
||||
|
||||
export const getPerson = pubkey => $people[pubkey]
|
||||
|
||||
// Hooks
|
||||
|
||||
db.events.process = async events => {
|
||||
export const processEvents = async events => {
|
||||
// Only persist ones we care about, the rest can be ephemeral and used to update people etc
|
||||
const eventsByKind = groupBy(prop('kind'), ensurePlural(events))
|
||||
const notesAndReactions = flatten(Object.values(pick([1, 7], eventsByKind)))
|
||||
@ -70,9 +67,7 @@ db.events.process = async events => {
|
||||
}
|
||||
|
||||
// Update our people
|
||||
db.people.update($people => {
|
||||
let $user = get(db.user)
|
||||
|
||||
people.update($people => {
|
||||
for (const event of profileUpdates) {
|
||||
const {pubkey, kind, content, tags} = event
|
||||
const putPerson = data => {
|
||||
@ -82,10 +77,6 @@ db.events.process = async events => {
|
||||
pubkey,
|
||||
updated_at: now(),
|
||||
}
|
||||
|
||||
if ($user?.pubkey === pubkey) {
|
||||
$user = {...$user, ...data}
|
||||
}
|
||||
}
|
||||
|
||||
switcherFn(kind, {
|
||||
@ -98,8 +89,6 @@ db.events.process = async events => {
|
||||
})
|
||||
}
|
||||
|
||||
db.user.set($user)
|
||||
|
||||
return $people
|
||||
})
|
||||
}
|
@ -1,4 +1,48 @@
|
||||
import keys from 'src/agent/keys'
|
||||
import {derived} from 'svelte/store'
|
||||
import pool from 'src/agent/pool'
|
||||
import keys from 'src/agent/keys'
|
||||
import {db, people, getPerson, processEvents} from 'src/agent/data'
|
||||
|
||||
export default {pool, keys}
|
||||
Object.assign(window, {pool, db})
|
||||
|
||||
export {pool, keys, db, people, getPerson}
|
||||
|
||||
export const user = derived(
|
||||
[keys.pubkey, people],
|
||||
([pubkey, $people]) => $people[pubkey] || {pubkey}
|
||||
)
|
||||
|
||||
export const publish = async (relays, event) => {
|
||||
const signedEvent = keys.sign(event)
|
||||
|
||||
await Promise.all([
|
||||
pool.publish(relays, signedEvent),
|
||||
processEvents(signedEvent),
|
||||
])
|
||||
|
||||
return signedEvent
|
||||
}
|
||||
|
||||
export const load = async (relays, filter) => {
|
||||
const events = await pool.request(relays, filter)
|
||||
|
||||
await processEvents(events)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
export const listen = async (relays, filter, onEvent, {shouldProcess = true} = {}) => {
|
||||
const sub = await pool.subscribe(relays, filter)
|
||||
|
||||
sub.onEvent(e => {
|
||||
if (shouldProcess) {
|
||||
processEvents(e)
|
||||
}
|
||||
|
||||
if (onEvent) {
|
||||
onEvent(e)
|
||||
}
|
||||
})
|
||||
|
||||
return sub
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import {getPublicKey, getEventHash, signEvent} from 'nostr-tools'
|
||||
import {get} from 'svelte/store'
|
||||
import {synced} from 'src/util/misc'
|
||||
|
||||
let pubkey
|
||||
let privkey
|
||||
let signingFunction
|
||||
|
||||
const getPubkey = () => {
|
||||
return pubkey || getPublicKey(privkey)
|
||||
}
|
||||
const pubkey = synced('agent/user/pubkey')
|
||||
const privkey = synced('agent/user/privkey')
|
||||
|
||||
const setPrivateKey = _privkey => {
|
||||
privkey = _privkey
|
||||
privkey.set(_privkey)
|
||||
pubkey.set(getPublicKey(_privkey))
|
||||
}
|
||||
|
||||
const setPublicKey = _pubkey => {
|
||||
@ -19,15 +19,22 @@ const setPublicKey = _pubkey => {
|
||||
return sig
|
||||
}
|
||||
|
||||
pubkey = _pubkey
|
||||
pubkey.set(_pubkey)
|
||||
}
|
||||
|
||||
const sign = async event => {
|
||||
event.pubkey = pubkey
|
||||
event.pubkey = get(pubkey)
|
||||
event.id = getEventHash(event)
|
||||
event.sig = signingFunction ? await signingFunction(event) : signEvent(event, privkey)
|
||||
event.sig = signingFunction
|
||||
? await signingFunction(event)
|
||||
: signEvent(event, get(privkey))
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
export default {getPubkey, setPrivateKey, setPublicKey, sign}
|
||||
const clear = () => {
|
||||
pubkey.set(null)
|
||||
privkey.set(null)
|
||||
}
|
||||
|
||||
export default {pubkey, setPrivateKey, setPublicKey, sign, clear}
|
||||
|
@ -1,56 +1,78 @@
|
||||
import {relayInit} from 'nostr-tools'
|
||||
import {partial, uniqBy, prop} from 'ramda'
|
||||
import {uniqBy, filter, identity, prop} from 'ramda'
|
||||
import {ensurePlural} from 'hurdak/lib/hurdak'
|
||||
|
||||
const relays = {}
|
||||
|
||||
const connect = async url => {
|
||||
if (!relays[url]) {
|
||||
relays[url] = relayInit(url)
|
||||
relays[url].url = url
|
||||
relays[url].stats = {
|
||||
count: 0,
|
||||
timer: 0,
|
||||
timeouts: 0,
|
||||
activeCount: 0,
|
||||
}
|
||||
const init = url => {
|
||||
const relay = relayInit(url)
|
||||
|
||||
relays[url].on('disconnect', () => {
|
||||
delete relays[url]
|
||||
})
|
||||
|
||||
relays[url].connected = relays[url].connect()
|
||||
relay.url = url
|
||||
relay.stats = {
|
||||
count: 0,
|
||||
timer: 0,
|
||||
timeouts: 0,
|
||||
activeCount: 0,
|
||||
}
|
||||
|
||||
await relays[url].connected
|
||||
relay.on('error', () => {
|
||||
console.log(`failed to connect to ${url}`)
|
||||
})
|
||||
|
||||
relay.on('disconnect', () => {
|
||||
delete relays[url]
|
||||
})
|
||||
|
||||
// Do initialization synchonously and wait on retrieval
|
||||
// so we don't open multiple connections simultaneously
|
||||
return relay.connect().then(
|
||||
() => relay,
|
||||
e => console.log(`Failed to connect to ${url}: ${e}`)
|
||||
)
|
||||
}
|
||||
|
||||
const connect = url => {
|
||||
if (!relays[url]) {
|
||||
relays[url] = init(url)
|
||||
}
|
||||
|
||||
return relays[url]
|
||||
}
|
||||
|
||||
const publish = (urls, event) => {
|
||||
urls.forEach(async url => {
|
||||
const relay = await connect(url)
|
||||
|
||||
relay.publish(event)
|
||||
})
|
||||
}
|
||||
|
||||
const sub = async (urls, filters) => {
|
||||
const subs = await Promise.all(
|
||||
const publish = async (urls, event) => {
|
||||
return Promise.all(
|
||||
urls.map(async url => {
|
||||
const relay = await connect(url)
|
||||
|
||||
return relay.publish(event)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const subscribe = async (urls, filters) => {
|
||||
const subs = filter(identity, await Promise.all(
|
||||
urls.map(async url => {
|
||||
const relay = await connect(url)
|
||||
|
||||
// If the relay failed to connect, give up
|
||||
if (!relay) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sub = relay.sub(ensurePlural(filters))
|
||||
|
||||
sub.relay = relay
|
||||
sub.relay.stats.activeCount += 1
|
||||
|
||||
if (sub.relay.stats.activeCount > 10) {
|
||||
console.warning(`Relay ${url} has >10 active subscriptions`)
|
||||
console.warn(`Relay ${url} has >10 active subscriptions`)
|
||||
}
|
||||
|
||||
return sub
|
||||
})
|
||||
)
|
||||
))
|
||||
|
||||
const seen = new Set()
|
||||
|
||||
return {
|
||||
unsub: () => {
|
||||
@ -59,42 +81,62 @@ const sub = async (urls, filters) => {
|
||||
sub.relay.stats.activeCount -= 1
|
||||
})
|
||||
},
|
||||
on: (type, cb) => {
|
||||
onEvent: cb => {
|
||||
subs.forEach(sub => {
|
||||
sub.on(type, partial(cb, [sub.relay.url]))
|
||||
sub.on('event', e => {
|
||||
if (!seen.has(e.id)) {
|
||||
e.seen_on = sub.relay.url
|
||||
seen.add(e.id)
|
||||
cb(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
onEose: cb => {
|
||||
subs.forEach(sub => {
|
||||
sub.on('eose', () => cb(sub.relay.url))
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const all = (urls, filters) => {
|
||||
const request = (urls, filters) => {
|
||||
return new Promise(async resolve => {
|
||||
const subscription = await sub(urls, filters)
|
||||
const subscription = await subscribe(urls, filters)
|
||||
const now = Date.now()
|
||||
const events = []
|
||||
const eose = []
|
||||
|
||||
const done = () => {
|
||||
subscription.unsub()
|
||||
|
||||
resolve(uniqBy(prop('id'), events))
|
||||
|
||||
// Keep track of relay timeouts
|
||||
urls.forEach(url => {
|
||||
urls.forEach(async url => {
|
||||
if (!eose.includes(url)) {
|
||||
relays[url].stats.count += 1
|
||||
relays[url].stats.timer += Date.now() - now
|
||||
relays[url].stats.timeouts += 1
|
||||
const relay = await connect(url)
|
||||
|
||||
// Relay may be undefined if we failed to connect
|
||||
if (relay) {
|
||||
relay.stats.count += 1
|
||||
relay.stats.timer += Date.now() - now
|
||||
relay.stats.timeouts += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
subscription.on('event', (url, e) => events.push(e))
|
||||
subscription.onEvent(e => events.push(e))
|
||||
|
||||
subscription.onEose(async url => {
|
||||
const relay = await relays[url]
|
||||
|
||||
subscription.on('eose', url => {
|
||||
eose.push(url)
|
||||
|
||||
// Keep track of relay timing stats
|
||||
relays[url].stats.count += 1
|
||||
relays[url].stats.timer += Date.now() - now
|
||||
relay.stats.count += 1
|
||||
relay.stats.timer += Date.now() - now
|
||||
|
||||
if (eose.length === urls.length) {
|
||||
done()
|
||||
@ -106,4 +148,4 @@ const all = (urls, filters) => {
|
||||
})
|
||||
}
|
||||
|
||||
export default {relays, connect, publish, sub, all}
|
||||
export default {relays, connect, publish, subscribe, request}
|
||||
|
33
src/app/alerts.js
Normal file
33
src/app/alerts.js
Normal file
@ -0,0 +1,33 @@
|
||||
import {get} from 'svelte/store'
|
||||
import {synced, now, timedelta} from 'src/util/misc'
|
||||
import {isAlert} from 'src/util/nostr'
|
||||
import {listen as _listen} from 'src/agent'
|
||||
import {getRelays} from 'src/app'
|
||||
import loaders from 'src/app/loaders'
|
||||
|
||||
let listener
|
||||
|
||||
const start = now() - timedelta(30, 'days')
|
||||
|
||||
export const since = synced("app/alerts/since", start)
|
||||
export const latest = synced("app/alerts/latest", start)
|
||||
|
||||
export const listen = async (relays, pubkey) => {
|
||||
if (listener) {
|
||||
listener.unsub()
|
||||
}
|
||||
|
||||
listener = await _listen(
|
||||
relays,
|
||||
[{kinds: [1, 7], '#p': [pubkey], since: get(since)}],
|
||||
e => {
|
||||
if (isAlert(e, pubkey)) {
|
||||
loaders.loadNotesContext(getRelays(), [e])
|
||||
|
||||
latest.set(Math.max(e.created_at, get(latest)))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default {latest, listen}
|
@ -1,14 +1,13 @@
|
||||
import {isNil, uniqBy, last} from 'ramda'
|
||||
import {get} from 'svelte/store'
|
||||
import {first} from "hurdak/lib/hurdak"
|
||||
import relay from 'src/relay'
|
||||
import {keys, publish, user} from 'src/agent'
|
||||
|
||||
const updateUser = updates => publishEvent(0, JSON.stringify(updates))
|
||||
|
||||
const addPetname = (person, pubkey, name) =>
|
||||
publishEvent(3, '', uniqBy(t => t[1], person.petnames.concat([t("p", pubkey, name)])))
|
||||
const setRelays = relays => publishEvent(10001, "", relays.map(url => [url, "", ""]))
|
||||
|
||||
const removePetname = (person, pubkey) =>
|
||||
publishEvent(3, '', uniqBy(t => t[1], person.petnames.filter(t => t[1] !== pubkey)))
|
||||
const setPetnames = petnames => publishEvent(3, "", petnames)
|
||||
|
||||
const muffle = (person, pubkey, value) => {
|
||||
const muffle = person.muffle
|
||||
@ -53,7 +52,8 @@ const copyTags = (e, newTags = []) => {
|
||||
}
|
||||
|
||||
export const t = (type, content, marker) => {
|
||||
const tag = [type, content, first(relay.pool.getRelays())]
|
||||
const relays = get(user).relays || []
|
||||
const tag = [type, content, first(relays)]
|
||||
|
||||
if (!isNil(marker)) {
|
||||
tag.push(marker)
|
||||
@ -63,21 +63,23 @@ export const t = (type, content, marker) => {
|
||||
}
|
||||
|
||||
const makeEvent = (kind, content = '', tags = []) => {
|
||||
const pubkey = relay.pool.getPubkey()
|
||||
const pubkey = get(keys.pubkey)
|
||||
const createdAt = Math.round(new Date().valueOf() / 1000)
|
||||
|
||||
return {kind, content, tags, pubkey, created_at: createdAt}
|
||||
}
|
||||
|
||||
const publishEvent = async (...args) => {
|
||||
const event = makeEvent(...args)
|
||||
const publishEvent = (...args) => {
|
||||
const relays = get(user).relays || []
|
||||
|
||||
await relay.pool.publishEvent(event)
|
||||
if (relays.length === 0) {
|
||||
throw new Error("Unable to publish, user has no relays")
|
||||
}
|
||||
|
||||
return event
|
||||
publish(relays, makeEvent(...args))
|
||||
}
|
||||
|
||||
export default {
|
||||
updateUser, addPetname, removePetname, muffle, createRoom, updateRoom, createMessage, createNote,
|
||||
updateUser, setRelays, setPetnames, muffle, createRoom, updateRoom, createMessage, createNote,
|
||||
createReaction, createReply, deleteEvent, publishEvent,
|
||||
}
|
16
src/app/defaults.js
Normal file
16
src/app/defaults.js
Normal file
@ -0,0 +1,16 @@
|
||||
export default {
|
||||
petnames: [
|
||||
["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "fiatjaf", "wss://relay.damus.io"],
|
||||
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", "jb55", "wss://relay.damus.io"],
|
||||
["p", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", "hodlbod", "wss://relay.damus.io"],
|
||||
["p", "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", "MartyBent", "wss://relay.damus.io"],
|
||||
["p", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "jack", "wss://relay.damus.io"],
|
||||
["p", "85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204", "preston", "wss://relay.damus.io"],
|
||||
],
|
||||
relays: [
|
||||
// 'wss://nostr.rocks',
|
||||
// 'wss://astral.ninja',
|
||||
// 'wss://relay.damus.io',
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
],
|
||||
}
|
90
src/app/index.js
Normal file
90
src/app/index.js
Normal file
@ -0,0 +1,90 @@
|
||||
import {without, reject} from 'ramda'
|
||||
import {first} from 'hurdak/lib/hurdak'
|
||||
import {get} from 'svelte/store'
|
||||
import {getPerson, keys, db} from 'src/agent'
|
||||
import {toast, modal, settings} from 'src/app/ui'
|
||||
import cmd from 'src/app/cmd'
|
||||
import alerts from 'src/app/alerts'
|
||||
import loaders from 'src/app/loaders'
|
||||
import defaults from 'src/app/defaults'
|
||||
|
||||
export {toast, modal, settings, alerts}
|
||||
|
||||
export const getRelays = pubkey => {
|
||||
const person = getPerson(pubkey)
|
||||
|
||||
return person && person.relays.length > 0 ? person.relays : defaults.relays
|
||||
}
|
||||
|
||||
export const getBestRelay = pubkey => {
|
||||
const person = getPerson(pubkey)
|
||||
|
||||
if (!person) {
|
||||
return null
|
||||
}
|
||||
|
||||
return person.relays.length > 0 ? first(person.relays) : person.recommendedRelay
|
||||
}
|
||||
|
||||
export const login = async ({privkey, pubkey}) => {
|
||||
if (privkey) {
|
||||
keys.setPrivateKey(privkey)
|
||||
} else {
|
||||
keys.setPublicKey(pubkey)
|
||||
}
|
||||
|
||||
await loaders.loadNetwork(getRelays(), pubkey)
|
||||
await alerts.listen(getRelays(), pubkey)
|
||||
}
|
||||
|
||||
export const logout = async () => {
|
||||
keys.clear()
|
||||
|
||||
await db.tags.clear()
|
||||
await db.events.clear()
|
||||
}
|
||||
|
||||
export const addRelay = async url => {
|
||||
const pubkey = get(keys.pubkey)
|
||||
const person = getPerson(pubkey)
|
||||
const relays = person?.relays || []
|
||||
|
||||
await cmd.setRelays(relays.concat(url))
|
||||
await loaders.loadNetwork(relays, pubkey)
|
||||
await alerts.listen(relays, pubkey)
|
||||
}
|
||||
|
||||
export const removeRelay = async url => {
|
||||
const pubkey = get(keys.pubkey)
|
||||
const person = getPerson(pubkey)
|
||||
const relays = person?.relays || []
|
||||
|
||||
await cmd.setRelays(without([url], relays))
|
||||
}
|
||||
|
||||
export const follow = async targetPubkey => {
|
||||
const pubkey = get(keys.pubkey)
|
||||
const person = getPerson(pubkey)
|
||||
const petnames = person?.petnames || []
|
||||
const target = getPerson(targetPubkey)
|
||||
const relay = (
|
||||
getBestRelay(targetPubkey)
|
||||
|| getBestRelay(pubkey)
|
||||
|| first(defaults.relays)
|
||||
)
|
||||
|
||||
await cmd.setPetnames(
|
||||
reject(t => t[1] === targetPubkey, petnames)
|
||||
.concat([["p", pubkey, relay, target?.name || ""]])
|
||||
)
|
||||
|
||||
await loaders.loadNetwork(getRelays(), pubkey)
|
||||
}
|
||||
|
||||
export const unfollow = async targetPubkey => {
|
||||
const pubkey = get(keys.pubkey)
|
||||
const person = getPerson(pubkey)
|
||||
const petnames = person?.petnames || []
|
||||
|
||||
await cmd.setPetnames(reject(t => t[1] === targetPubkey, petnames))
|
||||
}
|
77
src/app/loaders.js
Normal file
77
src/app/loaders.js
Normal file
@ -0,0 +1,77 @@
|
||||
import {uniq, pluck, groupBy, prop, identity} from 'ramda'
|
||||
import {ensurePlural, createMap} from 'hurdak/lib/hurdak'
|
||||
import {filterTags, findReply} from 'src/util/nostr'
|
||||
import {load, db, getPerson} from 'src/agent'
|
||||
import defaults from 'src/app/defaults'
|
||||
|
||||
const personKinds = [0, 2, 3, 10001, 12165]
|
||||
|
||||
const loadPeople = (relays, pubkeys, {kinds = personKinds, ...opts} = {}) =>
|
||||
pubkeys.length > 0 ? load(relays, {kinds, authors: pubkeys}, opts) : []
|
||||
|
||||
const loadNetwork = async (relays, pubkey) => {
|
||||
// Get this user's profile to start with. This may update what relays
|
||||
// are available, so don't assign relays to a variable here.
|
||||
let events = pubkey ? await loadPeople(relays, [pubkey]) : []
|
||||
let petnames = events.filter(e => e.kind === 3).flatMap(e => e.tags.filter(t => t[0] === "p"))
|
||||
|
||||
// Default to some cool guys we know
|
||||
if (petnames.length === 0) {
|
||||
petnames = defaults.petnames
|
||||
}
|
||||
|
||||
// Get the user's follows, with a fallback if we have no pubkey, then use nip-2 recommended
|
||||
// relays to load our user's second-order follows in order to bootstrap our social graph
|
||||
await Promise.all(
|
||||
Object.entries(groupBy(t => t[2], petnames))
|
||||
.map(([relay, petnames]) => loadPeople([relay], petnames.map(t => t[1])))
|
||||
)
|
||||
}
|
||||
|
||||
const loadNotesContext = async (relays, notes, {loadParents = false} = {}) => {
|
||||
notes = ensurePlural(notes)
|
||||
|
||||
if (notes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const authors = uniq(pluck('pubkey', notes)).filter(k => !getPerson(k))
|
||||
const parentIds = loadParents ? uniq(notes.map(findReply).filter(identity)) : []
|
||||
const filter = [{kinds: [1, 5, 7], '#e': pluck('id', notes)}]
|
||||
|
||||
// Load authors if needed
|
||||
if (authors.length > 0) {
|
||||
filter.push({kinds: personKinds, authors})
|
||||
}
|
||||
|
||||
// Load the note parents
|
||||
if (parentIds.length > 0) {
|
||||
filter.push({kinds: [1], ids: parentIds})
|
||||
}
|
||||
|
||||
// Load the events
|
||||
const events = await load(relays, filter)
|
||||
const eventsById = createMap('id', events)
|
||||
const parents = parentIds.map(id => eventsById[id]).filter(identity)
|
||||
|
||||
// Load the parents' context as well
|
||||
if (parents.length > 0) {
|
||||
await loadNotesContext(relays, parents)
|
||||
}
|
||||
}
|
||||
|
||||
const getOrLoadNote = async (relays, id) => {
|
||||
if (!await db.events.get(id)) {
|
||||
await load(relays, {kinds: [1], ids: [id]})
|
||||
}
|
||||
|
||||
const note = await db.events.get(id)
|
||||
|
||||
if (note) {
|
||||
await loadNotesContext(relays, [note], {loadParent: true})
|
||||
}
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
export default {getOrLoadNote, loadNotesContext, loadNetwork, loadPeople}
|
@ -1,24 +1,9 @@
|
||||
import {liveQuery} from 'dexie'
|
||||
import {get} from 'svelte/store'
|
||||
import {uniq, pluck, intersection, sortBy, propEq, uniqBy, groupBy, concat, without, prop, isNil, identity} from 'ramda'
|
||||
import {intersection, sortBy, propEq, uniqBy, groupBy, concat, prop, isNil, identity} from 'ramda'
|
||||
import {ensurePlural, createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {renderContent} from 'src/util/html'
|
||||
import {filterTags, displayPerson, getTagValues, findReply, findRoot} from 'src/util/nostr'
|
||||
import {db} from 'src/relay/db'
|
||||
import pool from 'src/relay/pool'
|
||||
import cmd from 'src/relay/cmd'
|
||||
|
||||
// Livequery appears to swallow errors
|
||||
|
||||
const lq = f => liveQuery(async () => {
|
||||
try {
|
||||
return await f()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
// Utils for filtering db - nothing below should load events from the network
|
||||
import {db, people, getPerson} from 'src/agent'
|
||||
|
||||
const filterEvents = async ({limit, ...filter}) => {
|
||||
let events = db.events
|
||||
@ -77,7 +62,7 @@ const findNote = async (id, {showEntire = false, depth = 1} = {}) => {
|
||||
|
||||
const reactions = await filterReactions(note.id)
|
||||
const replies = await filterReplies(note.id)
|
||||
const person = prop(note.pubkey, get(db.people))
|
||||
const person = getPerson(note.pubkey)
|
||||
const html = await renderNote(note, {showEntire})
|
||||
|
||||
let parent = null
|
||||
@ -89,7 +74,7 @@ const findNote = async (id, {showEntire = false, depth = 1} = {}) => {
|
||||
parent = {
|
||||
...parent,
|
||||
reactions: await filterReactions(parent.id),
|
||||
person: prop(parent.pubkey, get(db.people)),
|
||||
person: getPerson(parent.pubkey),
|
||||
html: await renderNote(parent, {showEntire}),
|
||||
}
|
||||
}
|
||||
@ -143,7 +128,7 @@ const annotateChunk = async chunk => {
|
||||
|
||||
const renderNote = async (note, {showEntire = false}) => {
|
||||
const shouldEllipsize = note.content.length > 500 && !showEntire
|
||||
const $people = get(db.people)
|
||||
const $people = get(people)
|
||||
const peopleByPubkey = createMap(
|
||||
'pubkey',
|
||||
filterTags({tag: "p"}, note).map(k => $people[k]).filter(identity)
|
||||
@ -174,119 +159,4 @@ const renderNote = async (note, {showEntire = false}) => {
|
||||
return content
|
||||
}
|
||||
|
||||
// Synchronization
|
||||
|
||||
const login = ({privkey, pubkey}) => {
|
||||
db.user.set({relays: [], muffle: [], petnames: [], updated_at: 0, pubkey, privkey})
|
||||
|
||||
pool.syncNetwork()
|
||||
}
|
||||
|
||||
const addRelay = url => {
|
||||
db.connections.update($connections => $connections.concat(url))
|
||||
|
||||
pool.syncNetwork()
|
||||
}
|
||||
|
||||
const removeRelay = url => {
|
||||
db.connections.update($connections => without([url], $connections))
|
||||
}
|
||||
|
||||
const follow = async pubkey => {
|
||||
db.network.update($network => $network.concat(pubkey))
|
||||
|
||||
pool.syncNetwork()
|
||||
}
|
||||
|
||||
const unfollow = async pubkey => {
|
||||
db.network.update($network => $network.concat(pubkey))
|
||||
}
|
||||
|
||||
// Methods that wil attempt to load from the database and fall back to the network.
|
||||
// This is intended only for bootstrapping listeners
|
||||
|
||||
const loadNotesContext = async (notes, {loadParents = false} = {}) => {
|
||||
notes = ensurePlural(notes)
|
||||
|
||||
if (notes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const $people = get(people)
|
||||
const authors = uniq(pluck('pubkey', notes)).filter(k => !$people[k])
|
||||
const parentIds = loadParents ? uniq(notes.map(findReply).filter(identity)) : []
|
||||
const filter = [{kinds: [1, 5, 7], '#e': pluck('id', notes)}]
|
||||
|
||||
// Load authors if needed
|
||||
if (authors.length > 0) {
|
||||
filter.push({kinds: [0], authors})
|
||||
}
|
||||
|
||||
// Load the note parents
|
||||
if (parentIds.length > 0) {
|
||||
filter.push({kinds: [1], ids: parentIds})
|
||||
}
|
||||
|
||||
// Load the events
|
||||
const events = await pool.loadEvents(filter)
|
||||
const eventsById = createMap('id', events)
|
||||
const parents = parentIds.map(id => eventsById[id]).filter(identity)
|
||||
|
||||
// Load the parents' context as well
|
||||
if (parents.length > 0) {
|
||||
await loadNotesContext(parents)
|
||||
}
|
||||
}
|
||||
|
||||
const getOrLoadNote = async id => {
|
||||
if (!await db.events.get(id)) {
|
||||
await pool.loadEvents({kinds: [1], ids: [id]})
|
||||
}
|
||||
|
||||
const note = await db.events.get(id)
|
||||
|
||||
if (note) {
|
||||
await loadNotesContext([note], {loadParent: true})
|
||||
}
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
// Initialization
|
||||
|
||||
db.user.subscribe($user => {
|
||||
if ($user?.privkey) {
|
||||
pool.setPrivateKey($user.privkey)
|
||||
} else if ($user?.pubkey) {
|
||||
pool.setPublicKey($user.pubkey)
|
||||
}
|
||||
})
|
||||
|
||||
db.connections.subscribe($connections => {
|
||||
const poolRelays = pool.getRelays()
|
||||
|
||||
for (const url of $connections) {
|
||||
if (!poolRelays.includes(url)) {
|
||||
pool.addRelay(url)
|
||||
}
|
||||
}
|
||||
|
||||
for (const url of poolRelays) {
|
||||
if (!$connections.includes(url)) {
|
||||
pool.removeRelay(url)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Export stores on their own for convenience
|
||||
|
||||
export const user = db.user
|
||||
export const people = db.people
|
||||
export const network = db.network
|
||||
export const connections = db.connections
|
||||
|
||||
export default {
|
||||
db, pool, cmd, lq, filterEvents, getOrLoadNote, filterReplies, findNote,
|
||||
annotateChunk, renderNote, login, addRelay, removeRelay,
|
||||
follow, unfollow, loadNotesContext,
|
||||
}
|
||||
export default {filterEvents, filterReplies, filterReactions, annotateChunk, renderNote, findNote}
|
52
src/app/ui.js
Normal file
52
src/app/ui.js
Normal file
@ -0,0 +1,52 @@
|
||||
import {prop} from "ramda"
|
||||
import {uuid} from "hurdak/lib/hurdak"
|
||||
import {navigate} from "svelte-routing"
|
||||
import {writable, get} from "svelte/store"
|
||||
import {globalHistory} from "svelte-routing/src/history"
|
||||
import {synced} from "src/util/misc"
|
||||
|
||||
// Toast
|
||||
|
||||
export const toast = writable(null)
|
||||
|
||||
toast.show = (type, message, timeout = 5) => {
|
||||
const id = uuid()
|
||||
|
||||
toast.set({id, type, message})
|
||||
|
||||
setTimeout(() => {
|
||||
if (prop("id", get(toast)) === id) {
|
||||
toast.set(null)
|
||||
}
|
||||
}, timeout * 1000)
|
||||
}
|
||||
|
||||
// Modals
|
||||
|
||||
export const modal = {
|
||||
subscribe: cb => {
|
||||
const getModal = () =>
|
||||
location.hash.includes('#modal=')
|
||||
? JSON.parse(decodeURIComponent(escape(atob(location.hash.replace('#modal=', '')))))
|
||||
: null
|
||||
|
||||
cb(getModal())
|
||||
|
||||
return globalHistory.listen(() => cb(getModal()))
|
||||
},
|
||||
set: data => {
|
||||
let path = location.pathname
|
||||
if (data) {
|
||||
path += '#modal=' + btoa(unescape(encodeURIComponent(JSON.stringify(data))))
|
||||
}
|
||||
|
||||
navigate(path)
|
||||
},
|
||||
}
|
||||
|
||||
// Settings, alerts, etc
|
||||
|
||||
export const settings = synced("coracle/settings", {
|
||||
showLinkPreviews: true,
|
||||
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
||||
})
|
@ -3,12 +3,16 @@
|
||||
import {switcher} from "hurdak/lib/hurdak"
|
||||
|
||||
export let external = false
|
||||
export let loading = false
|
||||
export let type = "anchor"
|
||||
export let href = null
|
||||
|
||||
const className = cx(
|
||||
let className
|
||||
|
||||
$: className = cx(
|
||||
$$props.class,
|
||||
"cursor-pointer",
|
||||
"cursor-pointer transition-all",
|
||||
{"opacity-50": loading},
|
||||
switcher(type, {
|
||||
anchor: "underline",
|
||||
button: "py-2 px-4 rounded bg-white text-accent",
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {fromParentOffset} from "src/util/html"
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import {people} from "src/relay"
|
||||
import {people} from "src/agent/data"
|
||||
|
||||
export let onSubmit
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import {ellipsize, quantify} from 'hurdak/src/core'
|
||||
import {ellipsize, quantify} from 'hurdak/lib/hurdak'
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import {killEvent} from 'src/util/html'
|
||||
import {modal} from 'src/state/app'
|
||||
import {modal} from 'src/app'
|
||||
|
||||
export let note
|
||||
|
||||
|
@ -9,12 +9,13 @@
|
||||
import {findReply, isLike} from "src/util/nostr"
|
||||
import Preview from 'src/partials/Preview.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import {settings, modal} from "src/state/app"
|
||||
import {settings, modal} from "src/app"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import Compose from "src/partials/Compose.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import relay, {user} from 'src/relay'
|
||||
import {user} from 'src/agent'
|
||||
import cmd from 'src/app/cmd'
|
||||
|
||||
export let note
|
||||
export let until = Infinity
|
||||
@ -53,7 +54,7 @@
|
||||
return navigate('/login')
|
||||
}
|
||||
|
||||
const event = await relay.cmd.createReaction(content, note)
|
||||
const event = await cmd.createReaction(content, note)
|
||||
|
||||
if (content === '+') {
|
||||
likes = likes.concat(event)
|
||||
@ -65,7 +66,7 @@
|
||||
}
|
||||
|
||||
const deleteReaction = e => {
|
||||
relay.cmd.deleteEvent([e.id])
|
||||
cmd.deleteEvent([e.id])
|
||||
|
||||
if (e.content === '+') {
|
||||
likes = reject(propEq('pubkey', $user.pubkey), likes)
|
||||
@ -88,7 +89,7 @@
|
||||
const {content, mentions} = reply.parse()
|
||||
|
||||
if (content) {
|
||||
relay.cmd.createReply(note, content, mentions)
|
||||
cmd.createReply(note, content, mentions)
|
||||
|
||||
reply = null
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import {liveQuery} from 'dexie'
|
||||
import {sortBy, pluck, reject} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {slide} from 'svelte/transition'
|
||||
@ -7,18 +8,18 @@
|
||||
import {findReply} from 'src/util/nostr'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import relay from 'src/relay'
|
||||
import query from 'src/app/query'
|
||||
|
||||
export let loadNotes
|
||||
export let queryNotes
|
||||
|
||||
const notes = relay.lq(async () => {
|
||||
const notes = liveQuery(async () => {
|
||||
// Hacky way to wait for the loader to adjust the cursor so we have a nonzero duration
|
||||
await sleep(100)
|
||||
|
||||
return sortBy(
|
||||
e => -pluck('created_at', e.replies).concat(e.created_at).reduce((a, b) => Math.max(a, b)),
|
||||
await relay.annotateChunk(await queryNotes())
|
||||
await query.annotateChunk(await queryNotes())
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -9,13 +9,17 @@
|
||||
let preview
|
||||
|
||||
onMount(async () => {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({url}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({url}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
const json = await res.json()
|
||||
|
||||
|
@ -1,217 +0,0 @@
|
||||
import {uniqBy, sortBy, find, propEq, prop, uniq} from 'ramda'
|
||||
import {get} from 'svelte/store'
|
||||
import {noop, range, sleep} from 'hurdak/lib/hurdak'
|
||||
import {getTagValues, filterTags} from "src/util/nostr"
|
||||
import agent from 'src/agent'
|
||||
import {db} from 'src/relay/db'
|
||||
|
||||
// ============================================================================
|
||||
// Utils/config
|
||||
|
||||
class Channel {
|
||||
constructor(name) {
|
||||
this.name = name
|
||||
this.status = 'idle'
|
||||
}
|
||||
claim() {
|
||||
this.status = 'busy'
|
||||
}
|
||||
release() {
|
||||
this.status = 'idle'
|
||||
}
|
||||
async sub(filter, onEvent, onEose = noop, opts = {}) {
|
||||
const relays = getRelays()
|
||||
|
||||
// If we don't have any relays, we'll wait forever for an eose, but
|
||||
// we already know we're done. Use a timeout since callers are
|
||||
// expecting this to be async and we run into errors otherwise.
|
||||
if (relays.length === 0) {
|
||||
setTimeout(onEose)
|
||||
|
||||
return {unsub: noop}
|
||||
}
|
||||
|
||||
// Start our subscription, wait for only our fastest relays to eose before calling it done.
|
||||
// We were waiting for all before, but that made the slowest relay a bottleneck. Waiting for
|
||||
// only one meant we might be settling for very incomplete data
|
||||
const lastEvent = {}
|
||||
const eoseRelays = []
|
||||
|
||||
// Create our subscription
|
||||
const sub = await agent.pool.sub(relays, filter)
|
||||
|
||||
// Keep track of when we last heard from each relay, and close unresponsive ones
|
||||
sub.on('event', (r, e) => {
|
||||
lastEvent[r] = new Date().valueOf()
|
||||
onEvent(e)
|
||||
})
|
||||
|
||||
// If we have lots of relays, ignore the slowest ones
|
||||
sub.on('eose', r => {
|
||||
eoseRelays.push(r)
|
||||
|
||||
// If we have only a few, wait for all of them, otherwise ignore the slowest 1/5
|
||||
const threshold = Math.round(relays.length / 10)
|
||||
if (eoseRelays.length >= relays.length - threshold) {
|
||||
onEose()
|
||||
}
|
||||
})
|
||||
|
||||
// Clean everything up when we're done
|
||||
const done = () => {
|
||||
if (this.status === 'busy') {
|
||||
sub.unsub()
|
||||
this.release()
|
||||
}
|
||||
}
|
||||
|
||||
return {unsub: done}
|
||||
}
|
||||
all(filter, opts = {}) {
|
||||
/* eslint no-async-promise-executor: 0 */
|
||||
return new Promise(async resolve => {
|
||||
const result = []
|
||||
|
||||
const sub = await this.sub(
|
||||
filter,
|
||||
e => result.push(e),
|
||||
r => {
|
||||
sub.unsub()
|
||||
|
||||
resolve(uniqBy(prop('id'), result))
|
||||
},
|
||||
{timeout: 3000, ...opts},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const channels = range(0, 10).map(i => new Channel(i.toString()))
|
||||
|
||||
const getChannel = async () => {
|
||||
/*eslint no-constant-condition: 0*/
|
||||
|
||||
// Find a channel that isn't busy, or wait for one to become available
|
||||
while (true) {
|
||||
const channel = find(propEq('status', 'idle'), channels)
|
||||
|
||||
if (channel) {
|
||||
channel.claim()
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
await sleep(300)
|
||||
}
|
||||
}
|
||||
|
||||
const req = async (...args) => (await getChannel()).all(...args)
|
||||
const sub = async (...args) => (await getChannel()).sub(...args)
|
||||
|
||||
const getRelays = () => {
|
||||
return get(db.connections)
|
||||
}
|
||||
|
||||
const addRelay = url => {
|
||||
agent.pool.connect(url)
|
||||
}
|
||||
|
||||
const removeRelay = async url => {
|
||||
const relay = await agent.pool.connect(url)
|
||||
|
||||
relay.close()
|
||||
}
|
||||
|
||||
const publishEvent = event => {
|
||||
const relays = getRelays()
|
||||
|
||||
event = agent.keys.sign(event)
|
||||
agent.publish(relays, event)
|
||||
db.events.process(event)
|
||||
}
|
||||
|
||||
const loadEvents = async filter => {
|
||||
const events = await req(filter)
|
||||
|
||||
await db.events.process(events)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
const listenForEvents = async (key, filter, onEvent, {shouldProcess = true} = {}) => {
|
||||
if (listenForEvents.subs[key]) {
|
||||
listenForEvents.subs[key].unsub()
|
||||
}
|
||||
|
||||
listenForEvents.subs[key] = await sub(filter, e => {
|
||||
if (shouldProcess) {
|
||||
db.events.process(e)
|
||||
}
|
||||
|
||||
if (onEvent) {
|
||||
onEvent(e)
|
||||
}
|
||||
})
|
||||
|
||||
return listenForEvents.subs[key]
|
||||
}
|
||||
|
||||
listenForEvents.subs = {}
|
||||
|
||||
const loadPeople = (pubkeys, {kinds = [0, 3, 12165], ...opts} = {}) => {
|
||||
if (pubkeys.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return loadEvents({kinds, authors: pubkeys}, opts)
|
||||
}
|
||||
|
||||
const syncNetwork = async () => {
|
||||
const $user = get(db.user)
|
||||
|
||||
let pubkeys = []
|
||||
if ($user) {
|
||||
// Get this user's profile to start with
|
||||
await loadPeople([$user.pubkey])
|
||||
|
||||
// Get our refreshed person
|
||||
const people = get(db.people)
|
||||
|
||||
// Merge the new info into our user
|
||||
Object.assign($user, people[$user.pubkey])
|
||||
|
||||
// Update our user store
|
||||
db.user.update(() => $user)
|
||||
|
||||
// Get n degreees of separation using petnames
|
||||
pubkeys = uniq(getTagValues($user.petnames))
|
||||
}
|
||||
|
||||
// Fall back to some pubkeys we like so we can support new users
|
||||
if (pubkeys.length === 0) {
|
||||
pubkeys = [
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf
|
||||
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", // jb55
|
||||
"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", // hodlbod
|
||||
"472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", // Marty Bent
|
||||
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", // Jack
|
||||
"85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204", // Preston
|
||||
"c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0", // Jack Mallers
|
||||
]
|
||||
}
|
||||
|
||||
// Grab second-order follows. Randomize and limit so we're not blasting relays.
|
||||
// This will result in people getting a different list of second-order follows every load
|
||||
const events = await loadPeople(pubkeys)
|
||||
const secondOrderFollows = filterTags({kind: "p"}, events.filter(e => e.kind === 3))
|
||||
const randomSecondOrderFollows = sortBy(() => Math.random(), secondOrderFollows)
|
||||
const authors = uniq(pubkeys.concat(randomSecondOrderFollows.slice(0, 50)))
|
||||
|
||||
// Save this for next time
|
||||
db.network.set(authors)
|
||||
}
|
||||
|
||||
export default {
|
||||
getRelays, addRelay, removeRelay, publishEvent, loadEvents, listenForEvents,
|
||||
syncNetwork, loadPeople,
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import toast from 'src/state/toast'
|
||||
import {modal} from 'src/state/app'
|
||||
import {toast, modal, addRelay} from "src/app"
|
||||
import Input from 'src/partials/Input.svelte'
|
||||
import Button from 'src/partials/Button.svelte'
|
||||
import relay from 'src/relay'
|
||||
|
||||
let url = $modal.url
|
||||
|
||||
@ -16,8 +14,7 @@
|
||||
return toast.show("error", 'That isn\'t a valid websocket url - relay urls should start with "wss://"')
|
||||
}
|
||||
|
||||
relay.db.relays.put({url})
|
||||
relay.addRelay(url)
|
||||
addRelay(url)
|
||||
modal.set(null)
|
||||
}
|
||||
</script>
|
||||
|
@ -2,10 +2,11 @@
|
||||
import {propEq, sortBy} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {alerts} from 'src/state/app'
|
||||
import {findReply, isLike} from 'src/util/nostr'
|
||||
import relay, {people, user} from 'src/relay'
|
||||
import {now} from 'src/util/misc'
|
||||
import {findReply, isLike} from 'src/util/nostr'
|
||||
import {getPerson, user} from 'src/agent'
|
||||
import {alerts} from 'src/app'
|
||||
import query from 'src/app/query'
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
import Like from 'src/partials/Like.svelte'
|
||||
@ -13,9 +14,9 @@
|
||||
let annotatedNotes = []
|
||||
|
||||
onMount(async () => {
|
||||
alerts.set({since: now()})
|
||||
alerts.since.set(now())
|
||||
|
||||
const events = await relay.filterEvents({
|
||||
const events = await query.filterEvents({
|
||||
kinds: [1, 7],
|
||||
'#p': [$user.pubkey],
|
||||
customFilter: e => {
|
||||
@ -33,7 +34,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
const notes = await relay.annotateChunk(
|
||||
const notes = await query.annotateChunk(
|
||||
events.filter(propEq('kind', 1))
|
||||
)
|
||||
|
||||
@ -42,8 +43,8 @@
|
||||
.filter(e => e.kind === 7)
|
||||
.map(async e => ({
|
||||
...e,
|
||||
person: $people[e.pubkey] || {pubkey: e.pubkey},
|
||||
parent: await relay.findNote(findReply(e)),
|
||||
person: getPerson(e.pubkey),
|
||||
parent: await query.findNote(findReply(e)),
|
||||
}))
|
||||
)
|
||||
|
||||
@ -64,8 +65,6 @@
|
||||
.concat(Object.values(likesById))
|
||||
)
|
||||
})
|
||||
|
||||
alerts.set({since: now()})
|
||||
</script>
|
||||
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
|
@ -6,8 +6,8 @@
|
||||
import {hexToBech32} from "src/util/misc"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import toast from "src/state/toast"
|
||||
import {user} from "src/relay"
|
||||
import {user} from "src/agent"
|
||||
import {toast} from "src/app"
|
||||
|
||||
const keypairUrl = 'https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/'
|
||||
const delegationUrl = 'https://github.com/nostr-protocol/nips/blob/b62aa418dee13aac1899ea7c6946a0f55dd7ee84/26.md'
|
||||
|
@ -7,11 +7,11 @@
|
||||
import {copyToClipboard} from "src/util/html"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import toast from "src/state/toast"
|
||||
import relay, {connections} from 'src/relay'
|
||||
import {toast, login} from "src/app"
|
||||
|
||||
let nsec = ''
|
||||
let hasExtension = false
|
||||
let loading = false
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
@ -32,13 +32,11 @@
|
||||
}
|
||||
|
||||
const logIn = async ({privkey, pubkey}) => {
|
||||
relay.login({privkey, pubkey})
|
||||
loading = true
|
||||
|
||||
if ($connections.length === 0) {
|
||||
navigate('/relays')
|
||||
} else {
|
||||
navigate('/notes/network')
|
||||
}
|
||||
await login({privkey, pubkey})
|
||||
|
||||
navigate('/notes/network')
|
||||
}
|
||||
|
||||
const logInWithExtension = async () => {
|
||||
@ -47,7 +45,7 @@
|
||||
if (!pubkey.match(/[a-z0-9]{64}/)) {
|
||||
toast.show("error", "Sorry, but that's an invalid public key.")
|
||||
} else {
|
||||
logIn({pubkey})
|
||||
await logIn({pubkey})
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +55,7 @@
|
||||
if (!privkey.match(/[a-z0-9]{64}/)) {
|
||||
toast.show("error", "Sorry, but that's an invalid private key.")
|
||||
} else {
|
||||
logIn({privkey, pubkey: getPublicKey(privkey)})
|
||||
await logIn({privkey, pubkey: getPublicKey(privkey)})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -98,6 +96,8 @@
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<Anchor class="text-center" type="button" on:click={logInWithPrivateKey}>Log In</Anchor>
|
||||
<Anchor class="text-center" type="button" on:click={logInWithPrivateKey} {loading}>
|
||||
Log In
|
||||
</Anchor>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,23 +1,14 @@
|
||||
<script>
|
||||
import {get} from 'svelte/store'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {settings} from "src/state/app"
|
||||
import relay, {connections} from 'src/relay'
|
||||
import {db} from 'src/agent'
|
||||
|
||||
setTimeout(async () => {
|
||||
const $connections = get(connections)
|
||||
const $settings = get(settings)
|
||||
|
||||
// Clear localstorage
|
||||
localStorage.clear()
|
||||
|
||||
// Keep relays around, but delete events/tags
|
||||
await relay.db.events.clear()
|
||||
await relay.db.tags.clear()
|
||||
|
||||
// Remember the user's relay selection and settings
|
||||
connections.set($connections)
|
||||
settings.set($settings)
|
||||
await db.events.clear()
|
||||
await db.tags.clear()
|
||||
|
||||
// do a hard refresh so everything gets totally cleared
|
||||
window.location = '/login'
|
||||
|
@ -4,8 +4,9 @@
|
||||
import {navigate} from "svelte-routing"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import Compose from "src/partials/Compose.svelte"
|
||||
import toast from "src/state/toast"
|
||||
import relay, {user} from "src/relay"
|
||||
import {user} from "src/agent"
|
||||
import {toast} from "src/app"
|
||||
import cmd from "src/app/cmd"
|
||||
|
||||
let input = null
|
||||
|
||||
@ -13,7 +14,7 @@
|
||||
const {content, mentions} = input.parse()
|
||||
|
||||
if (content) {
|
||||
await relay.cmd.createNote(content, mentions)
|
||||
await cmd.createNote(content, mentions)
|
||||
|
||||
toast.show("info", `Your note has been created!`)
|
||||
|
||||
|
@ -4,34 +4,25 @@
|
||||
import Tabs from "src/partials/Tabs.svelte"
|
||||
import Network from "src/views/notes/Network.svelte"
|
||||
import Global from "src/views/notes/Global.svelte"
|
||||
import {connections, user} from 'src/relay'
|
||||
import {user} from 'src/agent'
|
||||
|
||||
export let activeTab
|
||||
|
||||
const setActiveTab = tab => navigate(`/notes/${tab}`)
|
||||
</script>
|
||||
|
||||
{#if $connections.length === 0}
|
||||
<div class="flex w-full justify-center items-center py-16">
|
||||
<div class="text-center max-w-md">
|
||||
You aren't yet connected to any relays. Please click <Anchor href="/relays"
|
||||
>here</Anchor
|
||||
> to get started.
|
||||
</div>
|
||||
</div>
|
||||
{:else if $user}
|
||||
<Tabs tabs={['network', 'global']} {activeTab} {setActiveTab} />
|
||||
{#if activeTab === 'network'}
|
||||
<Network />
|
||||
{:else}
|
||||
<Global />
|
||||
{/if}
|
||||
{:else}
|
||||
{#if !$user}
|
||||
<div class="flex w-full justify-center items-center py-16">
|
||||
<div class="text-center max-w-sm">
|
||||
Don't have an account? Click <Anchor href="/login">here</Anchor> to join the nostr network.
|
||||
</div>
|
||||
</div>
|
||||
<Global />
|
||||
{/if}
|
||||
|
||||
<Tabs tabs={['network', 'global']} {activeTab} {setActiveTab} />
|
||||
|
||||
{#if activeTab === 'network'}
|
||||
<Network />
|
||||
{:else}
|
||||
<Global />
|
||||
{/if}
|
||||
|
@ -11,32 +11,34 @@
|
||||
import Notes from "src/views/person/Notes.svelte"
|
||||
import Likes from "src/views/person/Likes.svelte"
|
||||
import Network from "src/views/person/Network.svelte"
|
||||
import {modal} from "src/state/app"
|
||||
import relay, {user, people} from 'src/relay'
|
||||
import {getPerson, listen, user} from "src/agent"
|
||||
import {modal, getRelays} from "src/app"
|
||||
import loaders from "src/app/loaders"
|
||||
import cmd from "src/app/cmd"
|
||||
|
||||
export let pubkey
|
||||
export let activeTab
|
||||
|
||||
let subs = []
|
||||
let following = $user && find(t => t[1] === pubkey, $user.petnames)
|
||||
let following = find(t => t[1] === pubkey, $user?.petnames || [])
|
||||
let followers = new Set()
|
||||
let followersCount = 0
|
||||
let person
|
||||
|
||||
$: {
|
||||
person = $people[pubkey] || {pubkey}
|
||||
person = getPerson(pubkey) || {pubkey}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
subs.push(await relay.pool.listenForEvents(
|
||||
'routes/Person',
|
||||
subs.push(await listen(
|
||||
getRelays(),
|
||||
[{kinds: [1, 5, 7], authors: [pubkey], since: now()},
|
||||
{kinds: [0, 3, 12165], authors: [pubkey]}],
|
||||
when(propEq('kind', 1), relay.loadNoteContext)
|
||||
when(propEq('kind', 1), loaders.loadNoteContext)
|
||||
))
|
||||
|
||||
subs.push(await relay.pool.listenForEvents(
|
||||
'routes/Person/followers',
|
||||
subs.push(await listen(
|
||||
getRelays(),
|
||||
[{kinds: [3], '#p': [pubkey]}],
|
||||
e => {
|
||||
followers.add(e.pubkey)
|
||||
@ -58,18 +60,18 @@
|
||||
following = true
|
||||
|
||||
// Make sure our follow list is up to date
|
||||
await relay.pool.loadPeople([$user.pubkey], {kinds: [3]})
|
||||
await loaders.loadPeople([$user.pubkey], {kinds: [3]})
|
||||
|
||||
relay.cmd.addPetname($user, pubkey, person.name)
|
||||
cmd.addPetname($user, pubkey, person.name)
|
||||
}
|
||||
|
||||
const unfollow = async () => {
|
||||
following = false
|
||||
|
||||
// Make sure our follow list is up to date
|
||||
await relay.pool.loadPeople([$user.pubkey], {kinds: [3]})
|
||||
await loaders.loadPeople([$user.pubkey], {kinds: [3]})
|
||||
|
||||
relay.cmd.removePetname($user, pubkey)
|
||||
cmd.removePetname($user, pubkey)
|
||||
}
|
||||
|
||||
const openAdvanced = () => {
|
||||
@ -97,7 +99,7 @@
|
||||
<a href="/profile" class="cursor-pointer text-sm">
|
||||
<i class="fa-solid fa-edit" /> Edit
|
||||
</a>
|
||||
{:else}
|
||||
{:else if $user.petnames}
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
{#if following}
|
||||
<Button on:click={unfollow}>Unfollow</Button>
|
||||
@ -123,7 +125,7 @@
|
||||
{:else if activeTab === 'likes'}
|
||||
<Likes {pubkey} />
|
||||
{:else if activeTab === 'network'}
|
||||
{#if person}
|
||||
{#if person?.petnames}
|
||||
<Network person={person} />
|
||||
{:else}
|
||||
<div class="py-16 max-w-xl m-auto flex justify-center">
|
||||
|
@ -8,8 +8,9 @@
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import toast from "src/state/toast"
|
||||
import relay, {user} from "src/relay"
|
||||
import {user} from "src/agent"
|
||||
import {toast} from "src/app"
|
||||
import cmd from "src/app/cmd"
|
||||
|
||||
let values = {picture: null, about: null, name: null}
|
||||
|
||||
@ -37,7 +38,7 @@
|
||||
const submit = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
await relay.cmd.updateUser(values)
|
||||
await cmd.updateUser(values)
|
||||
|
||||
navigate(`/people/${$user.pubkey}/profile`)
|
||||
|
||||
|
@ -1,25 +1,33 @@
|
||||
<script>
|
||||
import {liveQuery} from 'dexie'
|
||||
import {without} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {modal} from "src/state/app"
|
||||
import relay, {connections} from 'src/relay'
|
||||
import {db, user} from "src/agent"
|
||||
import {modal, addRelay, removeRelay} from "src/app"
|
||||
import defaults from "src/app/defaults"
|
||||
|
||||
let q = ""
|
||||
let search
|
||||
let relays = $user?.relays || []
|
||||
|
||||
const knownRelays = relay.lq(() => relay.db.relays.toArray())
|
||||
const knownRelays = liveQuery(() => db.relays.toArray())
|
||||
|
||||
$: search = fuzzy($knownRelays, {keys: ["name", "description", "url"]})
|
||||
|
||||
const join = url => {
|
||||
relay.addRelay(url)
|
||||
relays = relays.concat(url)
|
||||
addRelay(url)
|
||||
|
||||
document.querySelector('input').select()
|
||||
}
|
||||
|
||||
const leave = url => relay.removeRelay(url)
|
||||
const leave = url => {
|
||||
relays = without([url], relays)
|
||||
removeRelay(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center py-8 px-4" in:fly={{y: 20}}>
|
||||
@ -34,7 +42,7 @@
|
||||
<div class="flex flex-col gap-6 overflow-auto flex-grow -mx-6 px-6">
|
||||
<h2 class="staatliches text-2xl">Your relays</h2>
|
||||
{#each ($knownRelays || []) as r}
|
||||
{#if $connections.includes(r.url)}
|
||||
{#if relays.includes(r.url)}
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div>
|
||||
<strong>{r.name || r.url}</strong>
|
||||
@ -59,7 +67,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#each (search(q) || []).slice(0, 50) as r}
|
||||
{#if !$connections.includes(r.url)}
|
||||
{#if !relays.includes(r.url)}
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div>
|
||||
<strong>{r.name || r.url}</strong>
|
||||
|
@ -5,9 +5,8 @@
|
||||
import Toggle from "src/partials/Toggle.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import {settings} from "src/state/app"
|
||||
import toast from "src/state/toast"
|
||||
import {user} from "src/relay"
|
||||
import {user} from 'src/agent'
|
||||
import {toast, settings} from "src/app"
|
||||
|
||||
let values = {...$settings}
|
||||
|
||||
|
@ -1,96 +0,0 @@
|
||||
import {writable, get} from 'svelte/store'
|
||||
import {navigate} from "svelte-routing"
|
||||
import {globalHistory} from "svelte-routing/src/history"
|
||||
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
|
||||
import relay from 'src/relay'
|
||||
|
||||
// Modals
|
||||
|
||||
export const modal = {
|
||||
subscribe: cb => {
|
||||
const getModal = () =>
|
||||
location.hash.includes('#modal=')
|
||||
? JSON.parse(decodeURIComponent(escape(atob(location.hash.replace('#modal=', '')))))
|
||||
: null
|
||||
|
||||
cb(getModal())
|
||||
|
||||
return globalHistory.listen(() => cb(getModal()))
|
||||
},
|
||||
set: data => {
|
||||
let path = location.pathname
|
||||
if (data) {
|
||||
path += '#modal=' + btoa(unescape(encodeURIComponent(JSON.stringify(data))))
|
||||
}
|
||||
|
||||
navigate(path)
|
||||
},
|
||||
}
|
||||
|
||||
// Settings, alerts, etc
|
||||
|
||||
export const settings = writable({
|
||||
showLinkPreviews: true,
|
||||
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
||||
...getLocalJson("coracle/settings"),
|
||||
})
|
||||
|
||||
settings.subscribe($settings => {
|
||||
setLocalJson("coracle/settings", $settings)
|
||||
})
|
||||
|
||||
export const alerts = writable({
|
||||
since: now() - timedelta(30, 'days'),
|
||||
...getLocalJson("coracle/alerts"),
|
||||
})
|
||||
|
||||
alerts.subscribe($alerts => {
|
||||
setLocalJson("coracle/alerts", $alerts)
|
||||
})
|
||||
|
||||
// Populate relays initially. Hardcode some, load the rest asynchronously
|
||||
|
||||
fetch(get(settings).dufflepudUrl + '/relay').then(r => r.json()).then(({relays}) => {
|
||||
for (const url of relays) {
|
||||
relay.db.relays.put({url})
|
||||
}
|
||||
})
|
||||
|
||||
const defaultRelays = [
|
||||
'wss://no.contry.xyz',
|
||||
'wss://nostr.ethtozero.fr',
|
||||
'wss://relay.nostr.ro',
|
||||
'wss://nostr.actn.io',
|
||||
'wss://relay.realsearch.cc',
|
||||
'wss://nostr.mrbits.it',
|
||||
'wss://relay.nostr.vision',
|
||||
'wss://nostr.massmux.com',
|
||||
'wss://nostr.robotechy.com',
|
||||
'wss://satstacker.cloud',
|
||||
'wss://relay.kronkltd.net',
|
||||
'wss://nostr.developer.li',
|
||||
'wss://nostr.vulpem.com',
|
||||
'wss://nostr.openchain.fr',
|
||||
'wss://nostr-01.bolt.observer',
|
||||
'wss://nostr.oxtr.dev',
|
||||
'wss://nostr.zebedee.cloud',
|
||||
'wss://nostr-verif.slothy.win',
|
||||
'wss://nostr.rewardsbunny.com',
|
||||
'wss://nostr.onsats.org',
|
||||
'wss://relay.boring.surf',
|
||||
'wss://no.str.watch',
|
||||
'wss://relay.nostr.pro',
|
||||
'wss://nostr.ono.re',
|
||||
'wss://nostr.rocks',
|
||||
'wss://btc.klendazu.com',
|
||||
'wss://nostr-relay.untethr.me',
|
||||
'wss://nostr.orba.ca',
|
||||
'wss://sg.qemura.xyz',
|
||||
'wss://nostr.hyperlingo.com',
|
||||
'wss://nostr.d11n.net',
|
||||
'wss://relay.nostr.express',
|
||||
]
|
||||
|
||||
for (const url of defaultRelays) {
|
||||
relay.db.relays.put({url})
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import prop from "ramda/src/prop"
|
||||
import {uuid} from "hurdak/lib/hurdak"
|
||||
import {writable, get} from "svelte/store"
|
||||
|
||||
export const store = writable(null)
|
||||
|
||||
export default {
|
||||
show: (type, message, timeout = 5) => {
|
||||
const id = uuid()
|
||||
|
||||
store.set({id, type, message})
|
||||
|
||||
setTimeout(() => {
|
||||
if (prop("id", get(store)) === id) {
|
||||
store.set(null)
|
||||
}
|
||||
}, timeout * 1000)
|
||||
},
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
const fees = {
|
||||
items: [
|
||||
{amount: 10, type: "content/rich"},
|
||||
{amount: 100, type: "content/image"},
|
||||
{amount: 1000, type: "content/video"},
|
||||
{amount: 1, type: "vote/create"},
|
||||
],
|
||||
}
|
||||
|
||||
export const servers = [
|
||||
{
|
||||
name: "Test Relay",
|
||||
url: "http://localhost:8485",
|
||||
description: "My local relay",
|
||||
fees,
|
||||
},
|
||||
{
|
||||
name: "Bitcoin Hackers",
|
||||
url: "http://localhost:3001",
|
||||
description: "Dudes who build software on and around the soundest money on earth.",
|
||||
fees,
|
||||
},
|
||||
{
|
||||
name: "Moscow Kirk",
|
||||
url: "http://localhost:3002",
|
||||
description: "The Moscow, ID Church Community.",
|
||||
fees,
|
||||
},
|
||||
{
|
||||
name: "Dogwood Meta",
|
||||
url: "http://localhost:3003",
|
||||
description: "A place to talk about the network itself.",
|
||||
fees,
|
||||
},
|
||||
]
|
@ -1,7 +1,10 @@
|
||||
import {Buffer} from 'buffer'
|
||||
import {bech32} from 'bech32'
|
||||
import {pluck} from "ramda"
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import {pluck, sortBy} from "ramda"
|
||||
import Fuse from "fuse.js/dist/fuse.min.js"
|
||||
import {writable} from 'svelte/store'
|
||||
import {isObject} from 'hurdak/lib/hurdak'
|
||||
|
||||
export const fuzzy = (data, opts = {}) => {
|
||||
const fuse = new Fuse(data, opts)
|
||||
@ -17,6 +20,8 @@ export const getLocalJson = k => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(k))
|
||||
} catch (e) {
|
||||
console.warn(`Unable to parse ${k}: ${e}`)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -25,7 +30,7 @@ export const setLocalJson = (k, v) => {
|
||||
try {
|
||||
localStorage.setItem(k, JSON.stringify(v))
|
||||
} catch (e) {
|
||||
// pass
|
||||
console.warn(`Unable to set ${k}: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,3 +151,28 @@ export const hexToBech32 = (prefix, hex) =>
|
||||
export const bech32ToHex = b32 =>
|
||||
Buffer.from(bech32.fromWords(bech32.decode(b32).words)).toString('hex')
|
||||
|
||||
|
||||
export const synced = (key, defaultValue = null) => {
|
||||
// If it's an object, merge defaults
|
||||
const store = writable(
|
||||
isObject(defaultValue)
|
||||
? {...defaultValue, ...getLocalJson(key)}
|
||||
: (getLocalJson(key) || defaultValue)
|
||||
)
|
||||
|
||||
store.subscribe($value => setLocalJson(key, $value))
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const shuffle = sortBy(() => Math.random() > 0.5)
|
||||
|
||||
export const batch = (t, f) => {
|
||||
const xs = []
|
||||
const cb = debounce(t, () => f(xs.splice(0)))
|
||||
|
||||
return x => {
|
||||
xs.push(x)
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
@ -57,3 +57,17 @@ export const displayPerson = p => {
|
||||
}
|
||||
|
||||
export const isLike = content => ['', '+', '🤙', '👍', '❤️'].includes(content)
|
||||
|
||||
export const isAlert = (e, pubkey) => {
|
||||
// Don't show people's own stuff
|
||||
if (e.pubkey === pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only notify users about positive reactions
|
||||
if (e.kind === 7 && !isLike(e.content)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
<script>
|
||||
import {liveQuery} from 'dexie'
|
||||
import {when, propEq} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {now} from 'src/util/misc'
|
||||
import relay from 'src/relay'
|
||||
import {listen} from 'src/agent'
|
||||
import {getRelays} from 'src/app'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
|
||||
@ -12,16 +16,18 @@
|
||||
let observable, sub
|
||||
|
||||
onMount(async () => {
|
||||
note = await relay.getOrLoadNote(note.id)
|
||||
note = await loaders.getOrLoadNote(getRelays(), note.id)
|
||||
|
||||
// Log this for debugging purposes
|
||||
console.log('NoteDetail', note)
|
||||
|
||||
if (note) {
|
||||
sub = await relay.pool.listenForEvents(
|
||||
'routes/NoteDetail',
|
||||
sub = await listen(
|
||||
getRelays(),
|
||||
[{kinds: [1, 5, 7], '#e': [note.id], since: now()}],
|
||||
when(propEq('kind', 1), relay.loadNotesContext)
|
||||
when(propEq('kind', 1), e => {
|
||||
loaders.loadNotesContext(getRelays(), e)
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
@ -33,7 +39,7 @@
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
observable = relay.lq(() => relay.findNote(note.id, {showEntire: true, depth: 5}))
|
||||
observable = liveQuery(() => query.findNote(note.id, {showEntire: true, depth: 5}))
|
||||
|
||||
return () => {
|
||||
if (sub) {
|
||||
|
@ -4,8 +4,9 @@
|
||||
import {fly} from 'svelte/transition'
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import SelectButton from "src/partials/SelectButton.svelte"
|
||||
import {modal} from "src/state/app"
|
||||
import relay, {user} from 'src/relay'
|
||||
import {user} from 'src/agent'
|
||||
import {modal} from 'src/app'
|
||||
import cmd from 'src/app/cmd'
|
||||
|
||||
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']
|
||||
const muffleValue = parseFloat(first($user.muffle.filter(t => t[1] === $modal.person.pubkey).map(last)) || 1)
|
||||
@ -21,7 +22,7 @@
|
||||
// Scale back down to a decimal based on string value
|
||||
const muffleValue = muffleOptions.indexOf(values.muffle) / 3
|
||||
|
||||
relay.cmd.muffle($user, $modal.person.pubkey, muffleValue)
|
||||
cmd.muffle($user, $modal.person.pubkey, muffleValue)
|
||||
|
||||
modal.set(null)
|
||||
}
|
||||
|
@ -6,7 +6,8 @@
|
||||
import {getTagValues} from "src/util/nostr"
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import relay, {user} from 'src/relay'
|
||||
import {user} from 'src/agent'
|
||||
import query from 'src/app/query'
|
||||
|
||||
export let q
|
||||
|
||||
@ -15,13 +16,13 @@
|
||||
onMount(async () => {
|
||||
const filter = {kinds: [1], muffle: getTagValues($user?.muffle || [])}
|
||||
|
||||
search = fuzzy(take(5000, await relay.filterEvents(filter)), {keys: ["content"]})
|
||||
search = fuzzy(take(5000, await query.filterEvents(filter)), {keys: ["content"]})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if search}
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#await Promise.all(search(q).slice(0, 30).map(n => relay.findNote(n.id)))}
|
||||
{#await Promise.all(search(q).slice(0, 30).map(n => query.findNote(n.id)))}
|
||||
<Spinner />
|
||||
{:then results}
|
||||
{#each results as e (e.id)}
|
||||
|
@ -5,7 +5,7 @@
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {renderContent} from "src/util/html"
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import {user, people} from 'src/relay'
|
||||
import {user, people} from 'src/agent'
|
||||
|
||||
export let q
|
||||
|
||||
|
@ -1,24 +1,25 @@
|
||||
<script>
|
||||
import {when, propEq} from 'ramda'
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {onMount} from 'svelte'
|
||||
import Notes from "src/partials/Notes.svelte"
|
||||
import {timedelta, Cursor} from 'src/util/misc'
|
||||
import {getTagValues} from 'src/util/nostr'
|
||||
import relay, {user} from 'src/relay'
|
||||
import {listen, user, load} from 'src/agent'
|
||||
import {getRelays} from 'src/app'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
|
||||
let sub
|
||||
|
||||
onMount(async () => {
|
||||
sub = await relay.pool.listenForEvents(
|
||||
'views/notes/Global',
|
||||
onMount(() => {
|
||||
const sub = listen(
|
||||
getRelays(),
|
||||
[{kinds: [1, 5, 7], since: cursor.since}],
|
||||
when(propEq('kind', 1), relay.loadNotesContext)
|
||||
when(propEq('kind', 1), e => {
|
||||
loaders.loadNotesContext(getRelays(), e)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (sub) {
|
||||
sub.unsub()
|
||||
return () => {
|
||||
sub.then(s => s.unsub())
|
||||
}
|
||||
})
|
||||
|
||||
@ -27,12 +28,13 @@
|
||||
const loadNotes = async () => {
|
||||
const [since, until] = cursor.step()
|
||||
const filter = {kinds: [1], since, until}
|
||||
const notes = await relay.pool.loadEvents(filter)
|
||||
await relay.loadNotesContext(notes, {loadParents: true})
|
||||
const notes = await load(getRelays(), filter)
|
||||
|
||||
await loaders.loadNotesContext(getRelays(), notes, {loadParents: true})
|
||||
}
|
||||
|
||||
const queryNotes = () => {
|
||||
return relay.filterEvents({
|
||||
return query.filterEvents({
|
||||
kinds: [1],
|
||||
since: cursor.since,
|
||||
muffle: getTagValues($user?.muffle || []),
|
||||
|
@ -1,31 +1,39 @@
|
||||
<script>
|
||||
import {when, propEq} from 'ramda'
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {when, identity, nth, propEq} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import Notes from "src/partials/Notes.svelte"
|
||||
import {timedelta, Cursor} from 'src/util/misc'
|
||||
import {timedelta, shuffle, Cursor} from 'src/util/misc'
|
||||
import {getTagValues} from 'src/util/nostr'
|
||||
import relay, {user, network} from 'src/relay'
|
||||
import {user, getPerson, listen, load} from 'src/agent'
|
||||
import {getRelays} from 'src/app'
|
||||
import defaults from 'src/app/defaults'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
|
||||
let sub, networkUnsub
|
||||
const getFollows = pubkey => {
|
||||
const person = getPerson(pubkey)
|
||||
const petnames = person?.petnames || defaults.petnames
|
||||
|
||||
return petnames.map(nth(1))
|
||||
}
|
||||
|
||||
// Get first- and second-order follows. shuffle and slice network so we're not
|
||||
// sending too many pubkeys. This will also result in some variety.
|
||||
const follows = getFollows($user?.pubkey)
|
||||
const network = shuffle(follows.flatMap(getFollows)).slice(0, 50)
|
||||
const authors = follows.concat(network)
|
||||
|
||||
onMount(() => {
|
||||
// We need to re-create the sub when network changes, since this is where
|
||||
// we land when we first log in, but before network is loaded, leading to
|
||||
// a forever spinner.
|
||||
networkUnsub = network.subscribe(async $network => {
|
||||
sub = await relay.pool.listenForEvents(
|
||||
'views/notes/Network',
|
||||
[{kinds: [1, 5, 7], authors: $network, since: cursor.since}],
|
||||
when(propEq('kind', 1), relay.loadNotesContext)
|
||||
)
|
||||
})
|
||||
})
|
||||
const sub = listen(
|
||||
getRelays(),
|
||||
[{kinds: [1, 5, 7], authors, since: cursor.since}],
|
||||
when(propEq('kind', 1), e => {
|
||||
loaders.loadNotesContext(getRelays(), e)
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(() => {
|
||||
networkUnsub()
|
||||
|
||||
if (sub) {
|
||||
sub.unsub()
|
||||
return () => {
|
||||
sub.then(s => s.unsub())
|
||||
}
|
||||
})
|
||||
|
||||
@ -33,16 +41,17 @@
|
||||
|
||||
const loadNotes = async () => {
|
||||
const [since, until] = cursor.step()
|
||||
const filter = {kinds: [1, 7], authors: $network, since, until}
|
||||
const notes = await relay.pool.loadEvents(filter)
|
||||
await relay.loadNotesContext(notes, {loadParents: true})
|
||||
const filter = {kinds: [1, 7], authors, since, until}
|
||||
const notes = await load(getRelays(), filter)
|
||||
|
||||
await loaders.loadNotesContext(getRelays(), notes, {loadParents: true})
|
||||
}
|
||||
|
||||
const queryNotes = () => {
|
||||
return relay.filterEvents({
|
||||
return query.filterEvents({
|
||||
kinds: [1],
|
||||
since: cursor.since,
|
||||
authors: $network.concat($user.pubkey),
|
||||
authors: authors.concat($user?.pubkey).filter(identity),
|
||||
muffle: getTagValues($user?.muffle || []),
|
||||
})
|
||||
}
|
||||
|
@ -2,7 +2,10 @@
|
||||
import Notes from "src/partials/Notes.svelte"
|
||||
import {timedelta, Cursor} from 'src/util/misc'
|
||||
import {getTagValues} from 'src/util/nostr'
|
||||
import relay, {user} from 'src/relay'
|
||||
import {user, load} from 'src/agent'
|
||||
import {getRelays} from 'src/app'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
|
||||
export let pubkey
|
||||
|
||||
@ -11,15 +14,13 @@
|
||||
const loadNotes = async () => {
|
||||
const [since, until] = cursor.step()
|
||||
const filter = {kinds: [7], authors: [pubkey], since, until}
|
||||
const notes = await load(getRelays(), filter)
|
||||
|
||||
await relay.loadNotesContext(
|
||||
await relay.pool.loadEvents(filter),
|
||||
{loadParents: true}
|
||||
)
|
||||
await loaders.loadNotesContext(getRelays(), notes, {loadParents: true})
|
||||
}
|
||||
|
||||
const queryNotes = () => {
|
||||
return relay.filterEvents({
|
||||
return query.filterEvents({
|
||||
kinds: [7],
|
||||
since: cursor.since,
|
||||
authors: [pubkey],
|
||||
|
@ -2,7 +2,10 @@
|
||||
import Notes from "src/partials/Notes.svelte"
|
||||
import {timedelta, Cursor} from 'src/util/misc'
|
||||
import {getTagValues} from 'src/util/nostr'
|
||||
import relay, {user} from 'src/relay'
|
||||
import {load, user} from 'src/agent'
|
||||
import {getRelays} from 'src/app'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
|
||||
export let person
|
||||
|
||||
@ -10,17 +13,16 @@
|
||||
|
||||
const loadNotes = async () => {
|
||||
const [since, until] = cursor.step()
|
||||
console.log(person)
|
||||
const authors = getTagValues(person.petnames)
|
||||
const filter = {since, until, kinds: [1], authors}
|
||||
const events = await load(getRelays(), filter)
|
||||
|
||||
await relay.loadNotesContext(
|
||||
await relay.pool.loadEvents(filter),
|
||||
{loadParents: true}
|
||||
)
|
||||
await loaders.loadNotesContext(getRelays(), events, {loadParents: true})
|
||||
}
|
||||
|
||||
const queryNotes = () => {
|
||||
return relay.filterEvents({
|
||||
return query.filterEvents({
|
||||
kinds: [1],
|
||||
since: cursor.since,
|
||||
authors: getTagValues(person.petnames),
|
||||
|
@ -1,7 +1,10 @@
|
||||
<script>
|
||||
import Notes from "src/partials/Notes.svelte"
|
||||
import {timedelta, Cursor} from 'src/util/misc'
|
||||
import relay from 'src/relay'
|
||||
import {load} from 'src/agent'
|
||||
import {getRelays} from 'src/app'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
|
||||
export let pubkey
|
||||
|
||||
@ -10,15 +13,13 @@
|
||||
const loadNotes = async () => {
|
||||
const [since, until] = cursor.step()
|
||||
const filter = {kinds: [1], authors: [pubkey], since, until}
|
||||
const notes = await load(getRelays(), filter)
|
||||
|
||||
await relay.loadNotesContext(
|
||||
await relay.pool.loadEvents(filter),
|
||||
{loadParents: true}
|
||||
)
|
||||
await loaders.loadNotesContext(getRelays(), notes, {loadParents: true})
|
||||
}
|
||||
|
||||
const queryNotes = () => {
|
||||
return relay.filterEvents({
|
||||
return query.filterEvents({
|
||||
kinds: [1],
|
||||
since: cursor.since,
|
||||
authors: [pubkey],
|
||||
|
Loading…
Reference in New Issue
Block a user