diff --git a/CHANGELOG.md b/CHANGELOG.md index 12765f7b..a68b4bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - [x] Improve anonymous and new user experience by prompting for relays and follows - [x] Fixed kind0 merging to avoid dropping properties coracle doesn't support - [x] Implement NIP-65 properly +- [x] Allow users to set max number of concurrent relays ## 0.2.11 diff --git a/ROADMAP.md b/ROADMAP.md index a76e2f6d..6fbe62d0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,15 +1,10 @@ # Current -- [ ] Fix anon/new user experience - - [ ] When logging in rather than generating a new keypair, ask for a relay to bootstrap from - - [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead. -- [ ] Test publishing events with zero relays - [ ] Try lumping tables into a single key each to reduce load/save contention and time -- [ ] Fix turning off link previews, or make sure it applies to images/videos too -- [ ] Allow user to set sample size to manage bandwidth/performance tradeoff # Snacks +- [ ] If a user has no write relays (or is not logged in), open a modal - [ ] open web+nostr links like snort - [ ] DM/chat read status in encrypted note - [ ] Relay recommendations based on follows/followers diff --git a/src/App.svelte b/src/App.svelte index 81f274ab..e6b4e882 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -24,7 +24,7 @@ import {loadAppData} from "src/app" import alerts from "src/app/alerts" import messages from "src/app/messages" - import {modal, toast, settings, routes, menuIsOpen, logUsage} from "src/app/ui" + import {modal, toast, routes, menuIsOpen, logUsage} from "src/app/ui" import RelayCard from "src/partials/RelayCard.svelte" import Anchor from 'src/partials/Anchor.svelte' import Content from 'src/partials/Content.svelte' @@ -119,7 +119,7 @@ const interval = setInterval( async () => { - const {dufflepudUrl} = $settings + const {dufflepudUrl} = user.getSettings() if (!dufflepudUrl) { return diff --git a/src/agent/network.ts b/src/agent/network.ts index 3abfc177..8807ac10 100644 --- a/src/agent/network.ts +++ b/src/agent/network.ts @@ -4,7 +4,7 @@ import {chunk} from 'hurdak/lib/hurdak' import {batch, timedelta, now} from 'src/util/misc' import { getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores, - getUserReadRelays, getRelaysForEventChildren, getUserRelays + getRelaysForEventChildren, sampleRelays, } from 'src/agent/relays' import database from 'src/agent/database' import pool from 'src/agent/pool' @@ -32,11 +32,7 @@ const publish = async (relays, event) => { } const load = async (relays, filter, opts?): Promise[]> => { - if (relays.length === 0) { - relays = getUserReadRelays() - } - - const events = await pool.request(relays, filter, opts) + const events = await pool.request(sampleRelays(relays), filter, opts) await sync.processEvents(events) @@ -44,11 +40,7 @@ const load = async (relays, filter, opts?): Promise[]> = } const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => { - if (relays.length === 0) { - relays = getUserReadRelays() - } - - return pool.subscribe(relays, filter, { + return pool.subscribe(sampleRelays(relays), filter, { onEvent: batch(300, events => { if (shouldProcess) { sync.processEvents(events) @@ -62,12 +54,8 @@ const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => { } const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => { - if (relays.length === 0) { - relays = getUserReadRelays() - } - return new Promise(resolve => { - pool.subscribeUntilEose(relays, filter, { + pool.subscribeUntilEose(sampleRelays(relays), filter, { onClose: () => resolve(), onEvent: batch(300, events => { if (shouldProcess) { @@ -92,25 +80,22 @@ const loadPeople = async (pubkeys, {relays = null, kinds = personKinds, force = await Promise.all( chunk(256, pubkeys).map(async chunk => { - // Use the best relays we have, but fall back to user relays - const chunkRelays = relays || ( - getAllPubkeyWriteRelays(chunk) - .concat(getUserReadRelays()) - .slice(0, 10) + await load( + sampleRelays(relays || getAllPubkeyWriteRelays(chunk), 0.5), + {kinds, authors: chunk}, + opts ) - - await load(chunkRelays, {kinds, authors: chunk}, opts) }) ) } const loadParents = notes => { const notesWithParent = notes.filter(findReplyId) - const relays = aggregateScores(notesWithParent.map(getRelaysForEventParent)) - .concat(getUserRelays()) - .slice(0, 10) - return load(relays, {kinds: [1], ids: notesWithParent.map(findReplyId)}) + return load( + sampleRelays(aggregateScores(notesWithParent.map(getRelaysForEventParent)), 0.3), + {kinds: [1], ids: notesWithParent.map(findReplyId)} + ) } const streamContext = ({notes, updateNotes, depth = 0}) => { @@ -118,9 +103,7 @@ const streamContext = ({notes, updateNotes, depth = 0}) => { chunk(256, notes).forEach(chunk => { const authors = getStalePubkeys(pluck('pubkey', chunk)) const filter = [{kinds: [1, 7], '#e': pluck('id', chunk)}] as Array - const relays = aggregateScores(chunk.map(getRelaysForEventChildren)) - .concat(getUserRelays()) - .slice(0, 10) + const relays = sampleRelays(aggregateScores(chunk.map(getRelaysForEventChildren))) if (authors.length > 0) { filter.push({kinds: personKinds, authors}) diff --git a/src/agent/relays.ts b/src/agent/relays.ts index 9a3a375f..b831bc8f 100644 --- a/src/agent/relays.ts +++ b/src/agent/relays.ts @@ -125,7 +125,27 @@ export const uniqByUrl = uniqBy(prop('url')) export const sortByScore = sortBy(r => -r.score) -export const sampleRelays = relays => shuffle(relays).slice(0, 30) +export const sampleRelays = (relays, scale = 1) => { + let limit = user.getSetting('relayLimit') + + // Allow the caller to scale down how many relays we're bothering depending on + // the use case, but only if we have enough relays to handle it + if (limit > 10) { + limit *= scale + } + + // Shuffle and limit target relays + relays = shuffle(relays).slice(0, limit) + + // If we're still under the limit, add user relays for good measure + if (relays.length < limit) { + relays = relays.concat( + shuffle(getUserReadRelays()).slice(0, limit - relays.length) + ) + } + + return relays +} export const aggregateScores = relayGroups => { const scores = {} as Record { if (type === 'kind:3') return 0.8 if (type === 'kind:2') return 0.5 if (type === 'seen') return 0.2 - if (type === 'tag') return 0.1 } const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => { @@ -268,19 +267,6 @@ const processRoutes = async events => { }, default: noop, }) - - // Add tag hints - events.forEach(e => { - Tags.wrap(e.tags).type("p").all().forEach(([_, pubkey, url]) => { - updates.push( - calculateRoute(pubkey, url, 'tag', 'write', e.created_at) - ) - - updates.push( - calculateRoute(pubkey, url, 'tag', 'read', e.created_at) - ) - }) - }) } updates = updates.filter(identity) diff --git a/src/agent/user.ts b/src/agent/user.ts index 56c14d86..b0b08b0e 100644 --- a/src/agent/user.ts +++ b/src/agent/user.ts @@ -14,6 +14,7 @@ import cmd from 'src/agent/cmd' // sync this stuff to regular private variables so we don't have to constantly call // `get` on our stores. +let settingsCopy = null let profileCopy = null let petnamesCopy = [] let relaysCopy = [] @@ -21,6 +22,13 @@ let relaysCopy = [] const anonPetnames = synced('agent/user/anonPetnames', []) const anonRelays = synced('agent/user/anonRelays', []) +const settings = synced("agent/user/settings", { + relayLimit: 20, + showMedia: true, + reportAnalytics: true, + dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, +}) + const profile = derived( [keys.pubkey, database.people as Readable], ([pubkey, $people]) => { @@ -46,6 +54,10 @@ const relays = derived( // Keep our copies up to date +settings.subscribe($settings => { + settingsCopy = $settings +}) + profile.subscribe($profile => { profileCopy = $profile }) @@ -59,6 +71,13 @@ relays.subscribe($relays => { }) const user = { + // Settings + + settings, + getSettings: () => settingsCopy, + getSetting: k => settingsCopy[k], + dufflepud: path => `${settingsCopy.dufflepudUrl}${path}`, + // Profile profile, @@ -102,7 +121,7 @@ const user = { } }, async addRelay(url) { - this.updateRelays($relays => $relays.concat({url, write: false, read: true})) + this.updateRelays($relays => $relays.concat({url, write: true, read: true})) }, async removeRelay(url) { this.updateRelays(reject(whereEq({url}))) diff --git a/src/app/connection.js b/src/app/connection.js index 14806e42..939f8a4e 100644 --- a/src/app/connection.js +++ b/src/app/connection.js @@ -1,6 +1,5 @@ import {pluck} from 'ramda' import {first} from 'hurdak/lib/hurdak' -import {log} from 'src/util/logger' import {writable} from 'svelte/store' import pool from 'src/agent/pool' import {getUserRelays} from 'src/agent/relays' @@ -9,23 +8,16 @@ export const slowConnections = writable([]) setInterval(() => { // Only notify about relays the user is actually subscribed to - const relayUrls = pluck('url', getUserRelays()) + const relayUrls = new Set(pluck('url', getUserRelays())) // Prune connections we haven't used in a while pool.getConnections() .filter(conn => conn.lastRequest < Date.now() - 60_000) .forEach(conn => conn.disconnect()) - // Log stats for debugging purposes - log( - 'Connection stats', - pool.getConnections() - .map(c => `${c.nostr.url} ${c.getQuality().join(' ')}`) - ) - // Alert the user to any heinously slow connections slowConnections.set( pool.getConnections() - .filter(c => relayUrls.includes(c.nostr.url) && first(c.getQuality()) < 0.3) + .filter(c => relayUrls.has(c.nostr.url) && first(c.getQuality()) < 0.3) ) }, 30_000) diff --git a/src/app/index.ts b/src/app/index.ts index 10dac1ec..8960c3cf 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -1,4 +1,5 @@ import type {DisplayEvent} from 'src/util/types' +import {navigate} from 'svelte-routing' import {omit, sortBy, identity} from 'ramda' import {createMap, ellipsize} from 'hurdak/lib/hurdak' import {renderContent} from 'src/util/html' @@ -23,7 +24,7 @@ export const loadAppData = async pubkey => { } } -export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => { +export const login = ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => { if (privkey) { keys.setPrivateKey(privkey) } else { @@ -33,6 +34,12 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin modal.set({type: 'login/connect', noEscape: true}) } +export const signup = privkey => { + keys.setPrivateKey(privkey) + + navigate('/notes/network') +} + export const renderNote = (note, {showEntire = false}) => { const shouldEllipsize = note.content.length > 500 && !showEntire const peopleByPubkey = createMap( diff --git a/src/app/ui.ts b/src/app/ui.ts index cfa69d1c..6a0d5900 100644 --- a/src/app/ui.ts +++ b/src/app/ui.ts @@ -6,8 +6,9 @@ import {navigate} from "svelte-routing" import {nip19} from 'nostr-tools' import {writable, get} from "svelte/store" import {globalHistory} from "svelte-routing/src/history" -import {synced, sleep} from "src/util/misc" +import {sleep} from "src/util/misc" import {warn} from 'src/util/logger' +import user from 'src/agent/user' // Routing @@ -74,15 +75,6 @@ export const modal = { }, } -// Settings, alerts, etc - -export const settings = synced("coracle/settings", { - reportAnalytics: true, - showLinkPreviews: true, - relayLimit: 30, - dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, -}) - // Wait for bugsnag to be started in main setTimeout(() => { Bugsnag.addOnError(event => { @@ -90,7 +82,7 @@ setTimeout(() => { return false } - if (!get(settings).reportAnalytics) { + if (!user.getSetting('reportAnalytics')) { return false } @@ -103,7 +95,7 @@ setTimeout(() => { const session = Math.random().toString().slice(2) export const logUsage = async name => { - const {dufflepudUrl, reportAnalytics} = get(settings) + const {dufflepudUrl, reportAnalytics} = user.getSettings() if (reportAnalytics) { try { diff --git a/src/partials/Note.svelte b/src/partials/Note.svelte index e4686cea..95e59db5 100644 --- a/src/partials/Note.svelte +++ b/src/partials/Note.svelte @@ -12,7 +12,7 @@ import ImageCircle from 'src/partials/ImageCircle.svelte' import Preview from 'src/partials/Preview.svelte' import Anchor from 'src/partials/Anchor.svelte' - import {toast, settings, modal} from "src/app/ui" + import {toast, modal} from "src/app/ui" import {renderNote} from "src/app" import {formatTimestamp, stringToColor} from 'src/util/misc' import Compose from "src/partials/Compose.svelte" @@ -37,7 +37,7 @@ let replyContainer = null const {profile} = user - const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : [] + const links = extractUrls(note.content) const showEntire = anchorId === note.id const interactive = !anchorId || !showEntire const person = database.watch('people', () => database.getPersonWithFallback(note.pubkey)) @@ -247,11 +247,11 @@ {:else}

{@html renderNote(note, {showEntire})}

- {#each links.slice(-2) as link} + {#if user.getSetting('showMedia') && links.length > 0} - {/each} + {/if}
e.stopPropagation()}>
diff --git a/src/partials/Notes.svelte b/src/partials/Notes.svelte index 031c1cff..17fa0cde 100644 --- a/src/partials/Notes.svelte +++ b/src/partials/Notes.svelte @@ -10,7 +10,6 @@ import Note from "src/partials/Note.svelte" import user from 'src/agent/user' import network from 'src/agent/network' - import {getUserReadRelays, uniqByUrl} from 'src/agent/relays' import {modal} from "src/app/ui" import {mergeParents} from "src/app" @@ -22,18 +21,13 @@ let notesBuffer = [] const since = now() - const maxNotes = 300 + const maxNotes = 100 const cursor = new Cursor() const {profile} = user const muffle = Tags .wrap(($profile?.muffle || []).filter(t => Math.random() > parseFloat(last(t)))) .values().all() - // Sample relays in case we have a whole ton of them. Add in user relays in - // case we don't have any - const sampleRelays = () => - uniqByUrl(relays.concat(getUserReadRelays())).slice(0, 30) - const processNewNotes = async newNotes => { // Remove people we're not interested in hearing about, sort by created date newNotes = newNotes.filter(e => !muffle.includes(e.pubkey)) @@ -82,7 +76,7 @@ } onMount(() => { - const sub = network.listen(sampleRelays(), {...filter, since}, onChunk) + const sub = network.listen(relays, {...filter, since}, onChunk) const scroller = createScroller(() => { if ($modal) { @@ -91,7 +85,7 @@ const {limit, until} = cursor - return network.listenUntilEose(sampleRelays(), {...filter, until, limit}, onChunk) + return network.listenUntilEose(relays, {...filter, until, limit}, onChunk) }) return () => { diff --git a/src/partials/Preview.svelte b/src/partials/Preview.svelte index 7ef2f999..0fff642c 100644 --- a/src/partials/Preview.svelte +++ b/src/partials/Preview.svelte @@ -2,9 +2,9 @@ import {onMount} from 'svelte' import {slide} from 'svelte/transition' import Anchor from 'src/partials/Anchor.svelte' + import user from 'src/agent/user' export let url - export let endpoint let preview @@ -15,7 +15,7 @@ preview = {video: url} } else { try { - const res = await fetch(endpoint, { + const res = await fetch(user.dufflepud('/link/preview'), { method: 'POST', body: JSON.stringify({url}), headers: { diff --git a/src/partials/RelayCard.svelte b/src/partials/RelayCard.svelte index 1a73be91..e04f6130 100644 --- a/src/partials/RelayCard.svelte +++ b/src/partials/RelayCard.svelte @@ -64,11 +64,13 @@

{#if joined} + {#if $relays.length > 1} + {/if} {:else}