Re-work relay selection

This commit is contained in:
Jonathan Staab 2023-02-09 17:57:37 -06:00
parent 0fe2afb3a8
commit 5f1f9f9b69
24 changed files with 177 additions and 167 deletions

View File

@ -30,6 +30,7 @@ If you like Coracle and want to support its development, you can donate sats via
- [ ] Attachments (a tag w/content type and url) - [ ] Attachments (a tag w/content type and url)
- [ ] Linkify bech32 entities w/ NIP 21 https://github.com/nostr-protocol/nips/blob/master/21.md - [ ] Linkify bech32 entities w/ NIP 21 https://github.com/nostr-protocol/nips/blob/master/21.md
- [ ] Sign in as user with one click to view things from their pubkey's perspective - do this with multiple accounts - [ ] Sign in as user with one click to view things from their pubkey's perspective - do this with multiple accounts
- [ ] QR code generation/scanner to share nprofile https://cdn.jb55.com/s/d966a729777c2021.MP4
# Missions # Missions
@ -73,6 +74,7 @@ If you like Coracle and want to support its development, you can donate sats via
- [ ] Implement gossip model https://bountsr.org/code/2023/02/03/gossip-model.html - [ ] Implement gossip model https://bountsr.org/code/2023/02/03/gossip-model.html
- [ ] Add nip 05 to calculation - [ ] Add nip 05 to calculation
- [ ] Add connection failures to calculation
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP - [ ] Make feeds page customizable. This could potentially use the "lists" NIP
- [ ] Show notification at top of feeds: "Showing notes from 3 relays". Click to customize. - [ ] Show notification at top of feeds: "Showing notes from 3 relays". Click to customize.
- [ ] Click through on relays page to view a feed for only that relay. - [ ] Click through on relays page to view a feed for only that relay.

View File

@ -13,7 +13,7 @@
import {displayPerson, isLike} from 'src/util/nostr' import {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, shuffle, now, sleep} from 'src/util/misc' import {timedelta, shuffle, now, sleep} from 'src/util/misc'
import cmd from 'src/agent/cmd' import cmd from 'src/agent/cmd'
import {user, getRelays} from 'src/agent/helpers' import {user, getUserRelays} from 'src/agent/helpers'
import database from 'src/agent/database' import database from 'src/agent/database'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
import network from 'src/agent/network' import network from 'src/agent/network'
@ -65,6 +65,7 @@
menuIsOpen.set(false) menuIsOpen.set(false)
} }
const {ready} = database
const {lastCheckedAlerts, mostRecentAlert} = alerts const {lastCheckedAlerts, mostRecentAlert} = alerts
const {lastCheckedByPubkey, mostRecentByPubkey} = messages const {lastCheckedByPubkey, mostRecentByPubkey} = messages
@ -97,7 +98,7 @@
const alertSlowConnections = () => { const alertSlowConnections = () => {
// Only notify about relays the user is actually subscribed to // Only notify about relays the user is actually subscribed to
const relayUrls = pluck('url', getRelays()) const relayUrls = pluck('url', getUserRelays('read'))
// Prune connections we haven't used in a while // Prune connections we haven't used in a while
pool.getConnections() pool.getConnections()
@ -201,6 +202,7 @@
<Router {url}> <Router {url}>
<div use:links class="h-full"> <div use:links class="h-full">
{#if $ready}
<div class="pt-16 text-white h-full lg:ml-56"> <div class="pt-16 text-white h-full lg:ml-56">
<Route path="/alerts" component={Alerts} /> <Route path="/alerts" component={Alerts} />
<Route path="/search/:activeTab" component={Search} /> <Route path="/search/:activeTab" component={Search} />
@ -234,6 +236,7 @@
</Route> </Route>
<Route path="*" component={NotFound} /> <Route path="*" component={NotFound} />
</div> </div>
{/if}
<ul <ul
class="py-20 w-56 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl class="py-20 w-56 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl

View File

@ -1,8 +1,8 @@
import {prop, pick, join, uniqBy, last} from 'ramda' import {prop, pick, join, uniqBy, last} from 'ramda'
import {get} from 'svelte/store' import {get} from 'svelte/store'
import {first} from "hurdak/lib/hurdak" import {first} from "hurdak/lib/hurdak"
import {Tags, isRelay, roomAttrs, displayPerson} from 'src/util/nostr' import {roomAttrs, displayPerson} from 'src/util/nostr'
import {getWriteRelays} from 'src/agent/helpers' import {getBestRelay} from 'src/agent/helpers'
import database from 'src/agent/database' import database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
@ -29,15 +29,14 @@ const createChatMessage = (relays, roomId, content) =>
publishEvent(relays, 42, {content, tags: [["e", roomId, prop('url', first(relays)), "root"]]}) publishEvent(relays, 42, {content, tags: [["e", roomId, prop('url', first(relays)), "root"]]})
const createDirectMessage = (relays, pubkey, content) => const createDirectMessage = (relays, pubkey, content) =>
// todo, encrypt messages
publishEvent(relays, 4, {content, tags: [["p", pubkey]]}) publishEvent(relays, 4, {content, tags: [["p", pubkey]]})
const createNote = (relays, content, mentions = [], topics = []) => { const createNote = (relays, content, mentions = [], topics = []) => {
mentions = mentions.map(p => { mentions = mentions.map(pubkey => {
const {url} = first(getWriteRelays(p)) const name = displayPerson(database.getPersonWithFallback(pubkey))
const name = displayPerson(database.getPersonWithFallback(p)) const {url} = getBestRelay(pubkey, 'write')
return ["p", p, url, name] return ["p", pubkey, url, name]
}) })
topics = topics.map(t => ["t", t]) topics = topics.map(t => ["t", t])
@ -46,7 +45,7 @@ const createNote = (relays, content, mentions = [], topics = []) => {
} }
const createReaction = (relays, note, content) => { const createReaction = (relays, note, content) => {
const {url} = getBestRelay(relays, note) const {url} = getBestRelay(note.pubkey, 'write')
const tags = uniqBy( const tags = uniqBy(
join(':'), join(':'),
note.tags note.tags
@ -59,10 +58,10 @@ const createReaction = (relays, note, content) => {
} }
const createReply = (relays, note, content, mentions = [], topics = []) => { const createReply = (relays, note, content, mentions = [], topics = []) => {
mentions = mentions.map(p => ["p", p, prop('url', first(getWriteRelays(p)))]) mentions = mentions.map(pubkey => ["p", pubkey, prop('url', getBestRelay(pubkey))])
topics = topics.map(t => ["t", t]) topics = topics.map(t => ["t", t])
const {url} = getBestRelay(relays, note) const {url} = getBestRelay(note.pubkey, 'write')
const tags = uniqBy( const tags = uniqBy(
join(':'), join(':'),
note.tags note.tags
@ -80,24 +79,6 @@ const deleteEvent = (relays, ids) =>
// Utils // Utils
const getBestRelay = (relays, event) => {
// Find the best relay, based on reply, root, or pubkey. Fall back to a
// relay we're going to send the event to
const tags = Tags.from(event).type("e")
const reply = tags.mark("reply").values().first()
const root = tags.mark("root").values().first()
if (isRelay(reply)) {
return reply
}
if (isRelay(root)) {
return root
}
return first(getWriteRelays(event.pubkey).concat(relays))
}
const publishEvent = (relays, kind, {content = '', tags = []} = {}) => { const publishEvent = (relays, kind, {content = '', tags = []} = {}) => {
if (relays.length === 0) { if (relays.length === 0) {
throw new Error("Unable to publish, no relays specified") throw new Error("Unable to publish, no relays specified")

View File

@ -1,8 +1,8 @@
import {debounce} from 'throttle-debounce' import {debounce} from 'throttle-debounce'
import {is, prop, without} from 'ramda' import {is, prop, find, without, pluck, all, identity} from 'ramda'
import {writable} from 'svelte/store' import {writable, derived} from 'svelte/store'
import {switcherFn, createMap, ensurePlural, first} from 'hurdak/lib/hurdak' import {switcherFn, createMap, ensurePlural} from 'hurdak/lib/hurdak'
import {defer, asyncIterableToArray} from 'src/util/misc' import {defer, where, asyncIterableToArray} from 'src/util/misc'
// Types // Types
@ -18,8 +18,9 @@ type Table = {
patch: (data: object) => void patch: (data: object) => void
bulkPut: (data: object) => void bulkPut: (data: object) => void
bulkPatch: (data: object) => void bulkPatch: (data: object) => void
all: (where?: object) => Promise<any> iter: (spec?: object) => Promise<Array<Record<string, any>>>
get: (key: string) => any all: (spec?: object) => Array<Record<string, any>>
get: (key: string) => Record<string, any>
} }
// Plumbing // Plumbing
@ -142,6 +143,8 @@ const defineTable = (name: string, pk: string): Table => {
let listeners = [] let listeners = []
let data = {} let data = {}
const ready = writable(false)
const subscribe = f => { const subscribe = f => {
listeners.push(f) listeners.push(f)
@ -196,8 +199,10 @@ const defineTable = (name: string, pk: string): Table => {
const put = item => bulkPut(createMap(pk, [item])) const put = item => bulkPut(createMap(pk, [item]))
const patch = item => bulkPatch(createMap(pk, [item])) const patch = item => bulkPatch(createMap(pk, [item]))
const all = (where = {}) => asyncIterableToArray(iterate(name, where), prop('v')) const toArray = () => Object.values(data)
const one = (where = {}) => first(all(where)) const iter = (spec = {}) => asyncIterableToArray(iterate(name, spec), prop('v'))
const all = (spec = {}) => toArray().filter(where(spec))
const one = (spec = {}) => find(where(spec), toArray())
const get = k => data[k] const get = k => data[k]
// Sync from storage initially // Sync from storage initially
@ -208,9 +213,13 @@ const defineTable = (name: string, pk: string): Table => {
} }
setAndNotify(initialData) setAndNotify(initialData)
ready.set(true)
})() })()
registry[name] = {name, subscribe, bulkPut, bulkPatch, put, patch, all, one, get} registry[name] = {
name, subscribe, bulkPut, bulkPatch, put, patch, toArray, iter, all, one, get,
ready,
}
return registry[name] return registry[name]
} }
@ -277,8 +286,10 @@ const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
const clearAll = () => Promise.all(Object.keys(registry).map(clear)) const clearAll = () => Promise.all(Object.keys(registry).map(clear))
const ready = derived(pluck('ready', Object.values(registry)), all(identity))
export default { export default {
getItem, setItem, removeItem, length, clear, keys, iterate, watch, getItem, setItem, removeItem, length, clear, keys, iterate, watch,
getPersonWithFallback, clearAll, people, rooms, messages, alerts, relays, getPersonWithFallback, clearAll, people, rooms, messages, alerts, relays,
routes, routes, ready,
} }

View File

@ -1,6 +1,7 @@
import type {Person} from 'src/util/types' import type {Person} from 'src/util/types'
import type {Readable} from 'svelte/store' import type {Readable} from 'svelte/store'
import {uniq, reject, last, propEq, uniqBy, prop} from 'ramda' import {isEmpty, pick, identity, sortBy, uniq, reject, groupBy, last, propEq, uniqBy, prop} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {derived, get} from 'svelte/store' import {derived, get} from 'svelte/store'
import {Tags} from 'src/util/nostr' import {Tags} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc' import {now, timedelta} from 'src/util/misc'
@ -37,27 +38,33 @@ export const getFollows = pubkey => {
return Tags.wrap(person.petnames || defaults.petnames).values().all() return Tags.wrap(person.petnames || defaults.petnames).values().all()
} }
export const getRelays = (pubkey?: string) => { export const getPersonRelays = (person, mode = 'all') => {
let relays = database.getPersonWithFallback(pubkey).relays const relays = isEmpty(person?.relays || []) ? defaults.relays : person.relays
if (!relays?.length) { return reject(propEq(mode, '!'), relays)
relays = database.getPersonWithFallback(get(keys.pubkey)).relays
}
if (!relays?.length) {
relays = defaults.relays
}
return relays
} }
export const getWriteRelays = (...args) => export const getUserRelays = (mode = 'all') =>
reject(propEq('write', '!'), getRelays(...args)) getPersonRelays(get(user), mode)
export const getPubkeyRelays = (pubkey, mode = 'all') =>
getPersonRelays(database.people.get(pubkey), mode)
export const getTopRelays = (pubkeys, mode = 'all') => {
const routes = database.routes.all({mode, pubkey: pubkeys})
const routesByPubkey = groupBy(prop('pubkey'), routes)
const selectRoute = k => first(sortBy(prop('score'), routesByPubkey[k] || []))
return uniqBy(prop('url'), pubkeys.map(selectRoute).filter(identity)).map(pick(['url']))
}
export const getBestRelay = (pubkey, mode = 'all') =>
first(getTopRelays([pubkey], mode).concat(getPubkeyRelays(pubkey, mode)))
export const getEventRelays = event => { export const getEventRelays = event => {
return uniqBy( return uniqBy(
prop('url'), prop('url'),
getRelays(event.pubkey) getPubkeyRelays(event.pubkey, 'write')
.concat(Tags.from(event).relays()) .concat(Tags.from(event).relays())
.concat({url: event.seen_on}) .concat({url: event.seen_on})
) )

View File

@ -1,7 +1,7 @@
import lf from 'localforage' import lf from 'localforage'
import memoryStorageDriver from 'localforage-memoryStorageDriver' import memoryStorageDriver from 'localforage-memoryStorageDriver'
import {is, complement, equals, isNil, pipe, prop, identity, allPass} from 'ramda'
import {switcherFn} from 'hurdak/lib/hurdak' import {switcherFn} from 'hurdak/lib/hurdak'
import {where} from 'src/util/misc'
// Firefox private mode doesn't have access to any storage options // Firefox private mode doesn't have access to any storage options
lf.defineDriver(memoryStorageDriver) lf.defineDriver(memoryStorageDriver)
@ -28,41 +28,9 @@ addEventListener('message', async ({data: {topic, payload, channel}}) => {
reply('localforage.return', result) reply('localforage.return', result)
}, },
'localforage.iterate': async () => { 'localforage.iterate': async () => {
const {storeName, where} = payload const matchesFilter = where(payload.where)
const matchesFilter = allPass(
Object.entries(where)
.map(([key, value]) => {
let [field, operator = 'eq'] = key.split(':')
let test, modifier = identity
if (operator.startsWith('!')) { getStore(payload.storeName).iterate(
operator = operator.slice(1)
modifier = complement
}
if (operator === 'eq' && is(Array, value)) {
test = v => value.includes(v)
} else if (operator === 'eq') {
test = equals(value)
} else if (operator === 'lt') {
test = v => (v || 0) < value
} else if (operator === 'lte') {
test = v => (v || 0) <= value
} else if (operator === 'gt') {
test = v => (v || 0) > value
} else if (operator === 'gte') {
test = v => (v || 0) >= value
} else if (operator === 'nil') {
test = isNil
} else {
throw new Error(`Invalid operator ${operator}`)
}
return pipe(prop(field), modifier(test))
})
)
getStore(storeName).iterate(
(v, k, i) => { (v, k, i) => {
if (matchesFilter(v)) { if (matchesFilter(v)) {
reply('localforage.item', {v, k, i}) reply('localforage.item', {v, k, i})

View File

@ -5,7 +5,7 @@ import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {get} from 'svelte/store' import {get} from 'svelte/store'
import {renderContent} from 'src/util/html' import {renderContent} from 'src/util/html'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr' import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {user, getRelays, getWriteRelays} from 'src/agent/helpers' import {user, getUserRelays} from 'src/agent/helpers'
import defaults from 'src/agent/defaults' import defaults from 'src/agent/defaults'
import database from 'src/agent/database' import database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
@ -18,11 +18,13 @@ import {toast, routes, modal, settings, logUsage} from 'src/app/ui'
export {toast, modal, settings, alerts, messages, logUsage} export {toast, modal, settings, alerts, messages, logUsage}
export const loadAppData = pubkey => { export const loadAppData = pubkey => {
const relays = getUserRelays('read')
return Promise.all([ return Promise.all([
network.loadNetwork(getRelays(), pubkey), network.loadNetwork(relays, pubkey),
alerts.load(getRelays(), pubkey), alerts.load(relays, pubkey),
alerts.listen(getRelays(), pubkey), alerts.listen(relays, pubkey),
messages.listen(getRelays(), pubkey), messages.listen(relays, pubkey),
]) ])
} }
@ -39,7 +41,7 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
loadAppData(pubkey) loadAppData(pubkey)
// Load our user so we can populate network and show profile info // Load our user so we can populate network and show profile info
await network.loadPeople(getRelays(), [pubkey]) await network.loadPeople(getUserRelays('read'), [pubkey])
// Not ideal, but the network tab depends on the user's social network being // Not ideal, but the network tab depends on the user's social network being
// loaded, so put them on global when they first log in so we're not slowing // loaded, so put them on global when they first log in so we're not slowing
@ -73,7 +75,7 @@ export const removeRelay = async url => {
defaults.relays = modify(defaults.relays) defaults.relays = modify(defaults.relays)
if (person) { if (person) {
await cmd.setRelays(getWriteRelays(), modify(person.relays || [])) await cmd.setRelays(getUserRelays('write'), modify(person.relays || []))
} }
} }
@ -85,7 +87,7 @@ export const setRelayWriteCondition = async (url, write) => {
defaults.relays = modify(defaults.relays) defaults.relays = modify(defaults.relays)
if (person) { if (person) {
await cmd.setRelays(getWriteRelays(), modify(person.relays || [])) await cmd.setRelays(getUserRelays('write'), modify(person.relays || []))
} }
} }

View File

@ -16,7 +16,7 @@
let prevContent = '' let prevContent = ''
let search let search
database.people.all({'name:!nil': null}).then(people => { database.people.iter({'name:!nil': null}).then(people => {
search = fuzzy(people, {keys: ["name", "pubkey"]}) search = fuzzy(people, {keys: ["name", "pubkey"]})
}) })

View File

@ -4,7 +4,7 @@
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
import {navigate} from "svelte-routing" import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc" import {fuzzy} from "src/util/misc"
import {getRelays, user} from 'src/agent/helpers' import {getUserRelays, getTopRelays, user} from 'src/agent/helpers'
import network from 'src/agent/network' import network from 'src/agent/network'
import database from 'src/agent/database' import database from 'src/agent/database'
import {modal, messages} from 'src/app' import {modal, messages} from 'src/app'
@ -24,7 +24,7 @@
const messages = await database.messages.all() const messages = await database.messages.all()
const pubkeys = without([$user.pubkey], uniq(messages.flatMap(m => [m.pubkey, m.recipient]))) const pubkeys = without([$user.pubkey], uniq(messages.flatMap(m => [m.pubkey, m.recipient])))
await network.loadPeople(getRelays(), pubkeys) await network.loadPeople(getTopRelays(pubkeys, 'write'), pubkeys)
return sortBy(k => -(mostRecentByPubkey[k] || 0), pubkeys) return sortBy(k => -(mostRecentByPubkey[k] || 0), pubkeys)
.map(k => ({type: 'npub', id: k, ...database.getPersonWithFallback(k)})) .map(k => ({type: 'npub', id: k, ...database.getPersonWithFallback(k)}))
@ -58,7 +58,7 @@
} }
onMount(() => { onMount(() => {
const sub = network.listen(getRelays(), [{kinds: [40, 41]}]) const sub = network.listen(getUserRelays('read'), [{kinds: [40, 41]}])
return () => { return () => {
sub.then(s => { sub.then(s => {

View File

@ -3,7 +3,7 @@
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
import {now, batch} from 'src/util/misc' import {now, batch} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte' import Channel from 'src/partials/Channel.svelte'
import {getRelays, user} from 'src/agent/helpers' import {getEventRelays, user} from 'src/agent/helpers'
import database from 'src/agent/database' import database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
import {modal} from 'src/app' import {modal} from 'src/app'
@ -11,21 +11,11 @@
export let entity export let entity
let {data: roomId} = nip19.decode(entity) as {data: string} const {data: roomId} = nip19.decode(entity) as {data: string}
let room = database.watch('rooms', rooms => rooms.get(roomId)) const room = database.watch('rooms', rooms => rooms.get(roomId))
const getRoomRelays = () => {
let relays = getRelays()
if ($room) {
relays = relays.concat(getRelays($room.pubkey))
}
return relays
}
const listenForMessages = async cb => { const listenForMessages = async cb => {
const relays = getRoomRelays() const relays = getEventRelays($room)
return network.listen( return network.listen(
relays, relays,
@ -43,7 +33,7 @@
} }
const loadMessages = async ({until, limit}) => { const loadMessages = async ({until, limit}) => {
const relays = getRoomRelays() const relays = getEventRelays($room)
const events = await network.load(relays, {kinds: [42], '#e': [roomId], until, limit}) const events = await network.load(relays, {kinds: [42], '#e': [roomId], until, limit})
if (events.length) { if (events.length) {
@ -58,7 +48,7 @@
} }
const sendMessage = content => const sendMessage = content =>
cmd.createChatMessage(getRoomRelays(), roomId, content) cmd.createChatMessage(getEventRelays($room), roomId, content)
</script> </script>
<Channel <Channel

View File

@ -4,7 +4,7 @@
import {personKinds} from 'src/util/nostr' import {personKinds} from 'src/util/nostr'
import {batch, now} from 'src/util/misc' import {batch, now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte' import Channel from 'src/partials/Channel.svelte'
import {getRelays, getWriteRelays, user} from 'src/agent/helpers' import {getUserRelays, getPubkeyRelays, user} from 'src/agent/helpers'
import database from 'src/agent/database' import database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
@ -20,6 +20,8 @@
messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()})) messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()}))
const getRelays = () => getUserRelays('write').concat(getPubkeyRelays(pubkey, 'write'))
const decryptMessages = async events => { const decryptMessages = async events => {
// Gotta do it in serial because of extension limitations // Gotta do it in serial because of extension limitations
for (const event of events) { for (const event of events) {
@ -32,7 +34,7 @@
} }
const listenForMessages = cb => network.listen( const listenForMessages = cb => network.listen(
getRelays().concat(getRelays(pubkey)), getRelays(),
[{kinds: personKinds, authors: [pubkey]}, [{kinds: personKinds, authors: [pubkey]},
{kinds: [4], authors: [$user.pubkey], '#p': [pubkey]}, {kinds: [4], authors: [$user.pubkey], '#p': [pubkey]},
{kinds: [4], authors: [pubkey], '#p': [$user.pubkey]}], {kinds: [4], authors: [pubkey], '#p': [$user.pubkey]}],
@ -55,9 +57,8 @@
} }
const sendMessage = async content => { const sendMessage = async content => {
const relays = getWriteRelays().concat(getRelays(pubkey))
const cyphertext = await crypt.encrypt(pubkey, content) const cyphertext = await crypt.encrypt(pubkey, content)
const event = await cmd.createDirectMessage(relays, pubkey, cyphertext) const event = await cmd.createDirectMessage(getRelays(), pubkey, cyphertext)
// Return unencrypted content so we can display it immediately // Return unencrypted content so we can display it immediately
return {...event, content} return {...event, content}

View File

@ -2,7 +2,6 @@
import {last, find, reject} from 'ramda' import {last, find, reject} from 'ramda'
import {onMount, onDestroy} from 'svelte' import {onMount, onDestroy} from 'svelte'
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
import {first} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing' import {navigate} from 'svelte-routing'
import {renderContent} from 'src/util/html' import {renderContent} from 'src/util/html'
@ -14,7 +13,7 @@
import Notes from "src/views/person/Notes.svelte" import Notes from "src/views/person/Notes.svelte"
import Likes from "src/views/person/Likes.svelte" import Likes from "src/views/person/Likes.svelte"
import Network from "src/views/person/Network.svelte" import Network from "src/views/person/Network.svelte"
import {getRelays, getWriteRelays, user} from "src/agent/helpers" import {getPubkeyRelays, getUserRelays, user} from "src/agent/helpers"
import network from "src/agent/network" import network from "src/agent/network"
import keys from "src/agent/keys" import keys from "src/agent/keys"
import database from "src/agent/database" import database from "src/agent/database"
@ -24,7 +23,7 @@
export let npub export let npub
export let activeTab export let activeTab
export let relays = null export let relays = []
let subs = [] let subs = []
let pubkey = nip19.decode(npub).data as string let pubkey = nip19.decode(npub).data as string
@ -37,22 +36,27 @@
$: following = find(t => t[1] === pubkey, $user?.petnames || []) $: following = find(t => t[1] === pubkey, $user?.petnames || [])
onMount(async () => { onMount(async () => {
// Add all the relays we know the person uses
relays = relays.concat(getPubkeyRelays(pubkey))
// Refresh our person if needed // Refresh our person if needed
network.loadPeople(relays || getRelays(pubkey), [pubkey]).then(() => { network.loadPeople(relays, [pubkey]).then(() => {
person = database.getPersonWithFallback(pubkey) person = database.getPersonWithFallback(pubkey)
loading = false loading = false
}) })
// Get our followers count // Get our followers count
subs.push(await network.listen( subs.push(
relays || getRelays(pubkey), await network.listen(
[{kinds: [3], '#p': [pubkey]}], relays,
e => { [{kinds: [3], '#p': [pubkey]}],
followers.add(e.pubkey) e => {
followersCount = followers.size followers.add(e.pubkey)
}, followersCount = followers.size
{shouldProcess: false}, },
)) {shouldProcess: false},
)
)
}) })
onDestroy(() => { onDestroy(() => {
@ -74,17 +78,16 @@
} }
const follow = async () => { const follow = async () => {
const relay = first(relays || getRelays(pubkey)) const tag = ["p", pubkey, relays[0].url, person.name || ""]
const tag = ["p", pubkey, relay.url, person.name || ""]
const petnames = reject(t => t[1] === pubkey, $user.petnames).concat([tag]) const petnames = reject(t => t[1] === pubkey, $user.petnames).concat([tag])
cmd.setPetnames(getWriteRelays(), petnames) cmd.setPetnames(getUserRelays('write'), petnames)
} }
const unfollow = async () => { const unfollow = async () => {
const petnames = reject(t => t[1] === pubkey, $user.petnames) const petnames = reject(t => t[1] === pubkey, $user.petnames)
cmd.setPetnames(getWriteRelays(), petnames) cmd.setPetnames(getUserRelays('write'), petnames)
} }
const openAdvanced = () => { const openAdvanced = () => {

View File

@ -10,7 +10,7 @@
import Button from "src/partials/Button.svelte" import Button from "src/partials/Button.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte" import Heading from "src/partials/Heading.svelte"
import {user, getWriteRelays} from "src/agent/helpers" import {user, getUserRelays} from "src/agent/helpers"
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import {toast} from "src/app" import {toast} from "src/app"
import {routes} from "src/app/ui" import {routes} from "src/app/ui"
@ -45,7 +45,7 @@
const submit = async event => { const submit = async event => {
event.preventDefault() event.preventDefault()
await cmd.updateUser(getWriteRelays(), values) await cmd.updateUser(getUserRelays('write'), values)
navigate(routes.person($user.pubkey, 'profile')) navigate(routes.person($user.pubkey, 'profile'))

View File

@ -1,5 +1,5 @@
import {debounce} from 'throttle-debounce' import {debounce} from 'throttle-debounce'
import {pluck, identity, sortBy} from "ramda" import {allPass, prop, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda"
import Fuse from "fuse.js/dist/fuse.min.js" import Fuse from "fuse.js/dist/fuse.min.js"
import {writable} from 'svelte/store' import {writable} from 'svelte/store'
import {isObject} from 'hurdak/lib/hurdak' import {isObject} from 'hurdak/lib/hurdak'
@ -145,7 +145,7 @@ export const getLastSync = (k, fallback = 0) => {
export class Cursor { export class Cursor {
until: number until: number
limit: number limit: number
constructor(limit = 10) { constructor(limit = 50) {
this.until = now() this.until = now()
this.limit = limit this.limit = limit
} }
@ -197,3 +197,40 @@ export const asyncIterableToArray = async (it, f = identity) => {
return result return result
} }
export const avg = xs => sum(xs) / xs.length
export const where = filters =>
allPass(
Object.entries(filters)
.map(([key, value]) => {
/* eslint prefer-const: 0 */
let [field, operator = 'eq'] = key.split(':')
let test, modifier = identity
if (operator.startsWith('!')) {
operator = operator.slice(1)
modifier = complement
}
if (operator === 'eq' && is(Array, value)) {
test = v => (value as Array<any>).includes(v)
} else if (operator === 'eq') {
test = equals(value)
} else if (operator === 'lt') {
test = v => (v || 0) < value
} else if (operator === 'lte') {
test = v => (v || 0) <= value
} else if (operator === 'gt') {
test = v => (v || 0) > value
} else if (operator === 'gte') {
test = v => (v || 0) >= value
} else if (operator === 'nil') {
test = isNil
} else {
throw new Error(`Invalid operator ${operator}`)
}
return pipe(prop(field), modifier(test))
})
)

View File

@ -6,7 +6,7 @@
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Textarea from "src/partials/Textarea.svelte" import Textarea from "src/partials/Textarea.svelte"
import Button from "src/partials/Button.svelte" import Button from "src/partials/Button.svelte"
import {getWriteRelays} from 'src/agent/helpers' import {getUserRelays} from 'src/agent/helpers'
import database from 'src/agent/database' import database from 'src/agent/database'
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import {toast, modal} from "src/app" import {toast, modal} from "src/app"
@ -36,8 +36,8 @@
toast.show("error", "Please enter a name for your room.") toast.show("error", "Please enter a name for your room.")
} else { } else {
const event = room.id const event = room.id
? await cmd.updateRoom(getWriteRelays(), room) ? await cmd.updateRoom(getUserRelays('write'), room)
: await cmd.createRoom(getWriteRelays(), room) : await cmd.createRoom(getUserRelays('write'), room)
await database.rooms.patch({id: room.id || event.id, joined: true}) await database.rooms.patch({id: room.id || event.id, joined: true})

View File

@ -13,13 +13,13 @@
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte" import Modal from "src/partials/Modal.svelte"
import Heading from 'src/partials/Heading.svelte' import Heading from 'src/partials/Heading.svelte'
import {user, getWriteRelays} from "src/agent/helpers" import {user, getUserRelays} from "src/agent/helpers"
import database from 'src/agent/database' import database from 'src/agent/database'
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import {toast, modal} from "src/app" import {toast, modal} from "src/app"
let input = null let input = null
let relays = getWriteRelays() let relays = getUserRelays('write')
let showSettings = false let showSettings = false
let q = '' let q = ''
let search let search

View File

@ -2,7 +2,7 @@
import {onMount} from 'svelte' import {onMount} from 'svelte'
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {getRelays} from 'src/agent/helpers' import {getEventRelays, getUserRelays} from 'src/agent/helpers'
import network from 'src/agent/network' import network from 'src/agent/network'
import {annotate} from 'src/app' import {annotate} from 'src/app'
import Note from 'src/partials/Note.svelte' import Note from 'src/partials/Note.svelte'
@ -10,7 +10,7 @@
import Spinner from 'src/partials/Spinner.svelte' import Spinner from 'src/partials/Spinner.svelte'
export let note export let note
export let relays = getRelays() export let relays = getUserRelays().concat(getEventRelays(note))
let loading = true let loading = true
@ -21,6 +21,7 @@
// Show the main note without waiting for context // Show the main note without waiting for context
if (!note.pubkey) { if (!note.pubkey) {
note = annotate(found, []) note = annotate(found, [])
relays = getEventRelays(note)
} }
const context = await network.loadContext(relays, found, { const context = await network.loadContext(relays, found, {

View File

@ -1,15 +1,16 @@
<script type="ts"> <script type="ts">
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import PersonInfo from 'src/partials/PersonInfo.svelte' import PersonInfo from 'src/partials/PersonInfo.svelte'
import {getRelays} from 'src/agent/helpers' import {getUserRelays, getTopRelays} from 'src/agent/helpers'
import database from 'src/agent/database' import database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
export let pubkeys export let pubkeys
const relays = getUserRelays('read').concat(getTopRelays(pubkeys, 'write'))
const people = database.watch('people', people => people.all({pubkey: pubkeys})) const people = database.watch('people', people => people.all({pubkey: pubkeys}))
network.loadPeople(getRelays(), pubkeys) network.loadPeople(relays, pubkeys)
</script> </script>
<Content gap={2}> <Content gap={2}>

View File

@ -5,7 +5,7 @@
import Button from "src/partials/Button.svelte" import Button from "src/partials/Button.svelte"
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import SelectButton from "src/partials/SelectButton.svelte" import SelectButton from "src/partials/SelectButton.svelte"
import {user, getWriteRelays} from 'src/agent/helpers' import {user, getUserRelays} from 'src/agent/helpers'
import cmd from 'src/agent/cmd' import cmd from 'src/agent/cmd'
import {modal} from 'src/app' import {modal} from 'src/app'
@ -28,7 +28,7 @@
.concat([["p", $modal.person.pubkey, muffleValue.toString()]]) .concat([["p", $modal.person.pubkey, muffleValue.toString()]])
.filter(t => last(t) !== "1") .filter(t => last(t) !== "1")
cmd.muffle(getWriteRelays(), muffleTags) cmd.muffle(getUserRelays('write'), muffleTags)
history.back() history.back()
} }

View File

@ -8,7 +8,7 @@
let search let search
database.people.all({'name:!nil': null}).then(people => { database.people.iter({'name:!nil': null}).then(people => {
search = fuzzy(people, {keys: ["name", "about", "pubkey"]}) search = fuzzy(people, {keys: ["name", "about", "pubkey"]})
}) })
</script> </script>

View File

@ -1,11 +1,11 @@
<script> <script>
import Notes from "src/partials/Notes.svelte" import Notes from "src/partials/Notes.svelte"
import {Cursor, now, batch} from 'src/util/misc' import {Cursor, now, batch} from 'src/util/misc'
import {getRelays, getMuffle} from 'src/agent/helpers' import {getUserRelays, getMuffle} from 'src/agent/helpers'
import network from 'src/agent/network' import network from 'src/agent/network'
import {threadify} from 'src/app' import {threadify} from 'src/app'
const relays = getRelays() const relays = getUserRelays('read')
const filter = {kinds: [1, 5, 7]} const filter = {kinds: [1, 5, 7]}
const cursor = new Cursor() const cursor = new Cursor()

View File

@ -1,8 +1,8 @@
<script> <script>
import {uniqBy, prop} from 'ramda' import {uniq} from 'ramda'
import Notes from "src/partials/Notes.svelte" import Notes from "src/partials/Notes.svelte"
import {now, Cursor, shuffle, batch} from 'src/util/misc' import {now, Cursor, shuffle, batch} from 'src/util/misc'
import {user, getRelays, getFollows, getMuffle} from 'src/agent/helpers' import {user, getTopRelays, getFollows, getMuffle} from 'src/agent/helpers'
import network from 'src/agent/network' import network from 'src/agent/network'
import {threadify} from 'src/app' import {threadify} from 'src/app'
@ -10,20 +10,23 @@
// sending too many pubkeys. This will also result in some variety. // sending too many pubkeys. This will also result in some variety.
const follows = shuffle(getFollows($user?.pubkey)) const follows = shuffle(getFollows($user?.pubkey))
const others = shuffle(follows.flatMap(getFollows)).slice(0, 50) const others = shuffle(follows.flatMap(getFollows)).slice(0, 50)
const authors = follows.concat(others).slice(0, 100) const authors = uniq(follows.concat(others)).slice(0, 100)
const filter = {kinds: [1, 7], authors} const filter = {kinds: [1, 7], authors}
const cursor = new Cursor() const cursor = new Cursor()
const relays = uniqBy(prop('url'), follows.flatMap(getRelays))
const listenForNotes = onNotes => const listenForNotes = async onNotes => {
network.listen(relays, {...filter, since: now()}, batch(300, async notes => { const relays = getTopRelays(authors, 'write')
return network.listen(relays, {...filter, since: now()}, batch(300, async notes => {
const context = await network.loadContext(relays, notes) const context = await network.loadContext(relays, notes)
onNotes(threadify(notes, context, {muffle: getMuffle(), showReplies: false})) onNotes(threadify(notes, context, {muffle: getMuffle(), showReplies: false}))
})) }))
}
const loadNotes = async () => { const loadNotes = async () => {
const {limit, until} = cursor const {limit, until} = cursor
const relays = getTopRelays(authors, 'write')
const notes = await network.load(relays, {...filter, limit, until}) const notes = await network.load(relays, {...filter, limit, until})
const context = await network.loadContext(relays, notes) const context = await network.loadContext(relays, notes)

View File

@ -1,13 +1,13 @@
<script> <script>
import Notes from "src/partials/Notes.svelte" import Notes from "src/partials/Notes.svelte"
import {now, batch, Cursor} from 'src/util/misc' import {now, batch, Cursor} from 'src/util/misc'
import {getRelays, getMuffle} from 'src/agent/helpers' import {getPubkeyRelays, getMuffle} from 'src/agent/helpers'
import network from 'src/agent/network' import network from 'src/agent/network'
import {threadify} from 'src/app' import {threadify} from 'src/app'
export let pubkey export let pubkey
const relays = getRelays(pubkey) const relays = getPubkeyRelays(pubkey, 'write')
const filter = {kinds: [7], authors: [pubkey]} const filter = {kinds: [7], authors: [pubkey]}
const cursor = new Cursor() const cursor = new Cursor()

View File

@ -1,13 +1,13 @@
<script> <script>
import Notes from "src/partials/Notes.svelte" import Notes from "src/partials/Notes.svelte"
import {now, batch, Cursor} from 'src/util/misc' import {now, batch, Cursor} from 'src/util/misc'
import {getRelays, getMuffle} from 'src/agent/helpers' import {getPubkeyRelays, getMuffle} from 'src/agent/helpers'
import network from 'src/agent/network' import network from 'src/agent/network'
import {threadify} from 'src/app' import {threadify} from 'src/app'
export let pubkey export let pubkey
const relays = getRelays(pubkey) const relays = getPubkeyRelays(pubkey, 'write')
const filter = {kinds: [1], authors: [pubkey]} const filter = {kinds: [1], authors: [pubkey]}
const cursor = new Cursor() const cursor = new Cursor()