Make users with no relays or follows comply

This commit is contained in:
Jonathan Staab 2023-02-16 20:29:03 -06:00
parent e2baa5c0c9
commit 7eeeb63e82
19 changed files with 235 additions and 150 deletions

View File

@ -27,6 +27,22 @@
import messages from "src/app/messages" import messages from "src/app/messages"
import {modal, toast, settings, routes, menuIsOpen, logUsage} from "src/app/ui" import {modal, toast, settings, routes, menuIsOpen, logUsage} from "src/app/ui"
import RelayCard from "src/partials/RelayCard.svelte" import RelayCard from "src/partials/RelayCard.svelte"
import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte'
import EnsureData from 'src/partials/EnsureData.svelte'
import Modal from 'src/partials/Modal.svelte'
import SideNav from 'src/partials/SideNav.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import TopNav from 'src/partials/TopNav.svelte'
import ChatEdit from "src/views/ChatEdit.svelte"
import NoteCreate from "src/views/NoteCreate.svelte"
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonList from "src/views/PersonList.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
import PersonShare from "src/views/PersonShare.svelte"
import PrivKeyLogin from "src/views/PrivKeyLogin.svelte"
import PubKeyLogin from "src/views/PubKeyLogin.svelte"
import SignUp from "src/views/SignUp.svelte"
import AddRelay from "src/routes/AddRelay.svelte" import AddRelay from "src/routes/AddRelay.svelte"
import Alerts from "src/routes/Alerts.svelte" import Alerts from "src/routes/Alerts.svelte"
import Bech32Entity from "src/routes/Bech32Entity.svelte" import Bech32Entity from "src/routes/Bech32Entity.svelte"
@ -44,21 +60,6 @@
import RelayList from "src/routes/RelayList.svelte" import RelayList from "src/routes/RelayList.svelte"
import Search from "src/routes/Search.svelte" import Search from "src/routes/Search.svelte"
import Settings from "src/routes/Settings.svelte" import Settings from "src/routes/Settings.svelte"
import ChatEdit from "src/views/ChatEdit.svelte"
import NoteCreate from "src/views/NoteCreate.svelte"
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonList from "src/views/PersonList.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
import PersonShare from "src/views/PersonShare.svelte"
import PrivKeyLogin from "src/views/PrivKeyLogin.svelte"
import PubKeyLogin from "src/views/PubKeyLogin.svelte"
import SignUp from "src/views/SignUp.svelte"
import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte'
import Modal from 'src/partials/Modal.svelte'
import SideNav from 'src/partials/SideNav.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import TopNav from 'src/partials/TopNav.svelte'
Object.assign(window, {cmd, database, keys, network, pool, sync}) Object.assign(window, {cmd, database, keys, network, pool, sync})
@ -167,8 +168,16 @@
<div use:links class="h-full"> <div use:links class="h-full">
<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" let:params>
<Route path="/notes/:activeTab" component={Notes} /> <EnsureData enforcePeople={false}>
<Search activeTab={params.activeTab} />
</EnsureData>
</Route>
<Route path="/notes/:activeTab" let:params>
<EnsureData>
<Notes activeTab={params.activeTab} />
</EnsureData>
</Route>
<Route path="/people/:npub/:activeTab" let:params> <Route path="/people/:npub/:activeTab" let:params>
{#key params.npub} {#key params.npub}
<Person npub={params.npub} activeTab={params.activeTab} /> <Person npub={params.npub} activeTab={params.activeTab} />

View File

@ -1,11 +1,12 @@
import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda' import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda'
import {personKinds, findReplyId} from 'src/util/nostr' import {personKinds, findReplyId} from 'src/util/nostr'
import {chunk} from 'hurdak/lib/hurdak' import {chunk} from 'hurdak/lib/hurdak'
import {batch, timedelta, now} from 'src/util/misc' import {batch, shuffle, timedelta, now} from 'src/util/misc'
import { import {
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores, getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
getUserNetworkWriteRelays, getUserReadRelays, getUserReadRelays,
} from 'src/agent/relays' } from 'src/agent/relays'
import {getUserNetwork} from 'src/agent/social'
import database from 'src/agent/database' import database from 'src/agent/database'
import pool from 'src/agent/pool' import pool from 'src/agent/pool'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
@ -32,6 +33,10 @@ const publish = async (relays, event) => {
} }
const load = async (relays, filter, opts?): Promise<Record<string, unknown>[]> => { const load = async (relays, filter, opts?): Promise<Record<string, unknown>[]> => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
const events = await pool.request(relays, filter, opts) const events = await pool.request(relays, filter, opts)
await sync.processEvents(events) await sync.processEvents(events)
@ -40,6 +45,10 @@ const load = async (relays, filter, opts?): Promise<Record<string, unknown>[]> =
} }
const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => { const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
return pool.subscribe(relays, filter, { return pool.subscribe(relays, filter, {
onEvent: batch(300, events => { onEvent: batch(300, events => {
if (shouldProcess) { if (shouldProcess) {
@ -54,6 +63,10 @@ const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
} }
const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => { const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
return new Promise(resolve => { return new Promise(resolve => {
pool.subscribeUntilEose(relays, filter, { pool.subscribeUntilEose(relays, filter, {
onClose: () => resolve(), onClose: () => resolve(),
@ -79,7 +92,7 @@ const loadPeople = (pubkeys, {kinds = personKinds, force = false, ...opts} = {})
} }
return load( return load(
getUserReadRelays().concat(getAllPubkeyWriteRelays(pubkeys)).slice(0, 10), shuffle(getUserReadRelays().concat(getAllPubkeyWriteRelays(pubkeys))).slice(0, 3),
{kinds, authors: pubkeys}, {kinds, authors: pubkeys},
opts opts
) )
@ -97,15 +110,16 @@ const streamContext = ({notes, updateNotes, depth = 0}) => {
// but it's also more likely to include spam. Checking our user's social graph // but it's also more likely to include spam. Checking our user's social graph
// avoids this problem. TODO: review this, maybe add note authors's graphs to this // avoids this problem. TODO: review this, maybe add note authors's graphs to this
// as well. // as well.
const relays = getUserNetworkWriteRelays() const relays = getAllPubkeyWriteRelays(getUserNetwork()).slice(0, 3)
// Some relays reject very large filters, send multiple // Some relays reject very large filters, send multiple
chunk(256, notes).forEach(chunk => { chunk(256, notes).forEach(chunk => {
const authors = getStalePubkeys(pluck('pubkey', chunk)) const authors = getStalePubkeys(pluck('pubkey', chunk))
const filter = [ const filter = [{kinds: [1, 7], '#e': pluck('id', chunk)}] as Array<object>
{kinds: [1, 7], '#e': pluck('id', chunk)},
{kinds: personKinds, authors}, if (authors.length > 0) {
] filter.push({kinds: personKinds, authors})
}
// Load authors and reactions in one subscription // Load authors and reactions in one subscription
listenUntilEose(relays, filter, events => { listenUntilEose(relays, filter, events => {

View File

@ -1,10 +1,9 @@
import type {Relay} from 'src/util/types' import type {Relay} from 'src/util/types'
import {get} from 'svelte/store' import {get} from 'svelte/store'
import {pick, map, assoc, sortBy, uniq, uniqBy, prop} from 'ramda' import {pick, map, assoc, sortBy, uniqBy, prop} from 'ramda'
import {first} from 'hurdak/lib/hurdak' import {first} from 'hurdak/lib/hurdak'
import {Tags} from 'src/util/nostr' import {Tags} from 'src/util/nostr'
import {synced} from 'src/util/misc' import {synced} from 'src/util/misc'
import {getFollows} from 'src/agent/social'
import database from 'src/agent/database' import database from 'src/agent/database'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
@ -51,19 +50,6 @@ export const getUserRelays = (): Array<Relay> => get(relays).map(assoc('score',
export const getUserReadRelays = () => getUserRelays().filter(prop('read')) export const getUserReadRelays = () => getUserRelays().filter(prop('read'))
export const getUserWriteRelays = () => getUserRelays().filter(prop('write')) export const getUserWriteRelays = () => getUserRelays().filter(prop('write'))
// Network relays
export const getNetworkWriteRelays = pubkey => {
const follows = Tags.wrap(getFollows(pubkey)).values().all()
const others = Tags.wrap(follows.flatMap(getFollows)).values().all()
return getAllPubkeyWriteRelays(uniq(follows.concat(others)))
}
// User's network relays
export const getUserNetworkWriteRelays = () => getNetworkWriteRelays(get(keys.pubkey))
// Event-related special cases // Event-related special cases
// If we're looking for an event's parent, tags are the most reliable hint, // If we're looking for an event's parent, tags are the most reliable hint,
@ -74,7 +60,7 @@ export const getRelaysForEventParent = event => {
const pubkeys = tags.type("p").values().all() const pubkeys = tags.type("p").values().all()
const pubkeyRelays = pubkeys.flatMap(getPubkeyWriteRelays) const pubkeyRelays = pubkeys.flatMap(getPubkeyWriteRelays)
return uniqByUrl(relays.concat(pubkeyRelays).concat(event.seen_on)) return uniqByUrl(relays.concat(pubkeyRelays).concat({url: event.seen_on}))
} }
// If we're looking for an event's children, the read relays the author has // If we're looking for an event's children, the read relays the author has
@ -82,10 +68,10 @@ export const getRelaysForEventParent = event => {
// will write replies there. However, this may include spam, so we may want // will write replies there. However, this may include spam, so we may want
// to read from the current user's network's read relays instead. // to read from the current user's network's read relays instead.
export const getRelaysForEventChildren = event => { export const getRelaysForEventChildren = event => {
return uniqByUrl(getPubkeyReadRelays(event.pubkey).concat(event.seen_on)) return uniqByUrl(getPubkeyReadRelays(event.pubkey).concat({url: event.seen_on}))
} }
export const getRelayForEventHint = event => event.seen_on export const getRelayForEventHint = event => ({url: event.seen_on})
export const getRelayForPersonHint = (pubkey, event) => export const getRelayForPersonHint = (pubkey, event) =>
first(getPubkeyWriteRelays(pubkey)) || getRelayForEventHint(event) first(getPubkeyWriteRelays(pubkey)) || getRelayForEventHint(event)

View File

@ -1,9 +1,22 @@
import {uniq} from 'ramda' import {uniq} from 'ramda'
import {get} from 'svelte/store'
import {Tags} from 'src/util/nostr' import {Tags} from 'src/util/nostr'
import database from 'src/agent/database' import database from 'src/agent/database'
import {follows} from 'src/agent/user'
export const getFollows = pubkey => export const getFollows = pubkey =>
Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all() Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all()
export const getNetwork = pubkey => export const getNetwork = pubkey => {
uniq(getFollows(pubkey).flatMap(getFollows)) const follows = getFollows(pubkey)
return uniq(follows.concat(follows.flatMap(getFollows)))
}
export const getUserFollows = (): Array<string> => get(follows.pubkeys)
export const getUserNetwork = () => {
const follows = getUserFollows()
return uniq(follows.concat(follows.flatMap(getFollows)))
}

View File

@ -1,9 +1,9 @@
import {pick, objOf, identity, isEmpty} from 'ramda' import {pick, identity, isEmpty} from 'ramda'
import {nip05} from 'nostr-tools' import {nip05} from 'nostr-tools'
import {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak' import {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {log, warn} from 'src/util/logger' import {warn} from 'src/util/logger'
import {now, timedelta, shuffle, hash} from 'src/util/misc' import {now, timedelta, shuffle, hash} from 'src/util/misc'
import {personKinds, Tags, roomAttrs, isRelay} from 'src/util/nostr' import {Tags, roomAttrs, isRelay, normalizeRelayUrl} from 'src/util/nostr'
import database from 'src/agent/database' import database from 'src/agent/database'
const processEvents = async events => { const processEvents = async events => {
@ -17,7 +17,7 @@ const processEvents = async events => {
const processProfileEvents = async events => { const processProfileEvents = async events => {
const profileEvents = ensurePlural(events) const profileEvents = ensurePlural(events)
.filter(e => personKinds.includes(e.kind)) .filter(e => [0, 3, 12165].includes(e.kind))
const updates = {} const updates = {}
for (const e of profileEvents) { for (const e of profileEvents) {
@ -43,9 +43,6 @@ const processProfileEvents = async events => {
}, },
3: () => ({petnames: e.tags}), 3: () => ({petnames: e.tags}),
12165: () => ({muffle: e.tags}), 12165: () => ({muffle: e.tags}),
default: () => {
log(`Received unsupported event type ${e.kind}`)
},
}), }),
updated_at: now(), updated_at: now(),
} }
@ -119,11 +116,12 @@ const getWeight = type => {
if (type === 'tag') return 0.1 if (type === 'tag') return 0.1
} }
const calculateRoute = (pubkey, url, type, mode, created_at) => { const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
if (!isRelay(url)) { if (!isRelay(rawUrl)) {
return return
} }
const url = normalizeRelayUrl(rawUrl)
const id = hash([pubkey, url, mode].join('')).toString() const id = hash([pubkey, url, mode].join('')).toString()
const score = getWeight(type) * (1 - (now() - created_at) / timedelta(30, 'days')) const score = getWeight(type) * (1 - (now() - created_at) / timedelta(30, 'days'))
const route = database.routes.get(id) || {id, pubkey, url, mode, score: 0, count: 0} const route = database.routes.get(id) || {id, pubkey, url, mode, score: 0, count: 0}
@ -229,7 +227,9 @@ const verifyNip05 = (pubkey, as) =>
if (result.relays?.length > 0) { if (result.relays?.length > 0) {
const urls = result.relays.filter(isRelay) const urls = result.relays.filter(isRelay)
database.relays.bulkPatch(createMap('url', urls.map(objOf('url')))) database.relays.bulkPatch(
createMap('url', urls.map(url => ({url: normalizeRelayUrl(url)})))
)
database.routes.bulkPut( database.routes.bulkPut(
createMap('id', urls.flatMap(url =>[ createMap('id', urls.flatMap(url =>[

View File

@ -1,7 +1,8 @@
import {pipe, concat, reject, nth, map} from 'ramda'
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 {derived, get, writable} from 'svelte/store' import {pipe, concat, reject, nth, map} from 'ramda'
import {synced} from 'src/util/misc'
import {derived, get} from 'svelte/store'
import database from 'src/agent/database' import database from 'src/agent/database'
import {getUserWriteRelays} from 'src/agent/relays' import {getUserWriteRelays} from 'src/agent/relays'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
@ -22,7 +23,7 @@ export const user = derived(
// the user is logged in or not // the user is logged in or not
export const follows = (() => { export const follows = (() => {
const anonPetnames = writable([]) const anonPetnames = synced('agent/user/anonPetnames', [])
const petnames = derived( const petnames = derived(
[user, anonPetnames], [user, anonPetnames],
@ -32,7 +33,7 @@ export const follows = (() => {
return { return {
petnames, petnames,
pubkeys: derived(petnames, pipe(nth(0), map(nth(1)))), pubkeys: derived(petnames, map(nth(1))) as Readable<Array<string>>,
update(f) { update(f) {
const $petnames = f(get(petnames)) const $petnames = f(get(petnames))

View File

@ -0,0 +1,83 @@
<script>
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte"
import RelaySearch from "src/partials/RelaySearch.svelte"
import SearchPeople from "src/views/SearchPeople.svelte"
import {relays} from 'src/agent/relays'
import {follows} from 'src/agent/user'
export let enforceRelays = true
export let enforcePeople = true
const {petnames} = follows
let modalIsOpen = true
const closeModal = () => {
modalIsOpen = false
}
</script>
{#if $relays.length === 0 && enforceRelays}
{#if modalIsOpen}
<Modal onEscape={closeModal}>
<Content>
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>
Search below to find one to join.
</div>
</div>
<RelaySearch />
</Content>
</Modal>
{:else}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>
Click <Anchor href="/relays">here</Anchor> to find one to join.
</div>
</div>
</Content>
{/if}
{:else if $petnames.length === 0 && enforcePeople}
{#if modalIsOpen}
<Modal onEscape={closeModal}>
<Content>
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>
Search below to find some interesting people.
</div>
</div>
<SearchPeople />
</Content>
</Modal>
{:else}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>
Click <Anchor href="/search/people">here</Anchor> to find some
interesting people.
</div>
</div>
</Content>
{/if}
{:else}
<slot />
{/if}

View File

@ -26,7 +26,7 @@
class="absolute inset-0 mt-20 sm:mt-28 modal-content" class="absolute inset-0 mt-20 sm:mt-28 modal-content"
transition:fly={{y: 1000, opacity: 1}} transition:fly={{y: 1000, opacity: 1}}
style={nested && `padding-top: 1rem`}> style={nested && `padding-top: 1rem`}>
<div class="bg-dark border-t border-solid border-medium h-full w-full overflow-auto"> <div class="bg-dark border-t border-solid border-medium h-full w-full overflow-auto pb-10">
<slot /> <slot />
</div> </div>
</div> </div>

View File

@ -145,7 +145,7 @@
text: 'View', text: 'View',
href: "/" + nip19.neventEncode({ href: "/" + nip19.neventEncode({
id: event.id, id: event.id,
relays: pluck('url', relays.slice(0, 5)), relays: pluck('url', relays.slice(0, 3)),
}), }),
}, },
}) })

View File

@ -9,7 +9,6 @@
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Note from "src/partials/Note.svelte" import Note from "src/partials/Note.svelte"
import {user} from 'src/agent/user' import {user} from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays'
import network from 'src/agent/network' import network from 'src/agent/network'
import {modal} from "src/app/ui" import {modal} from "src/app/ui"
import {mergeParents} from "src/app" import {mergeParents} from "src/app"
@ -76,10 +75,6 @@
} }
onMount(() => { onMount(() => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
const sub = network.listen(relays, {...filter, since}, onChunk) const sub = network.listen(relays, {...filter, since}, onChunk)
const scroller = createScroller(() => { const scroller = createScroller(() => {
@ -89,6 +84,8 @@
const {limit, until} = cursor const {limit, until} = cursor
console.log('here')
return network.listenUntilEose(relays, {...filter, until, limit}, onChunk) return network.listenUntilEose(relays, {...filter, until, limit}, onChunk)
}) })

View File

@ -0,0 +1,33 @@
<script>
import {pluck} from 'ramda'
import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import database from 'src/agent/database'
import {relays} from "src/agent/relays"
let q = ""
let search
let knownRelays = database.watch('relays', t => t.all())
$: {
const joined = new Set(pluck('url', $relays))
search = fuzzy(
$knownRelays.filter(r => isRelay(r.url) && !joined.has(r.url)),
{keys: ["name", "description", "url"]}
)
}
</script>
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
<i slot="before" class="fa-solid fa-search" />
</Input>
{#each (search(q) || []).slice(0, 50) as relay (relay.url)}
<RelayCard {relay} />
{/each}
<small class="text-center">
Showing {Math.min(($knownRelays || []).length - $relays.length, 50)}
of {($knownRelays || []).length - $relays.length} known relays
</small>

View File

@ -4,33 +4,22 @@
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import NewNoteButton from "src/partials/NewNoteButton.svelte" import NewNoteButton from "src/partials/NewNoteButton.svelte"
import Tabs from "src/partials/Tabs.svelte" import Tabs from "src/partials/Tabs.svelte"
import Modal from 'src/partials/Modal.svelte'
import RelayList from "src/routes/RelayList.svelte"
import Network from "src/views/notes/Network.svelte" import Network from "src/views/notes/Network.svelte"
import Popular from "src/views/notes/Popular.svelte" import Popular from "src/views/notes/Popular.svelte"
import {relays} from 'src/agent/relays' import {user} from 'src/agent/user'
import {user, follows} from 'src/agent/user'
export let activeTab export let activeTab
const {petnames} = follows
const setActiveTab = tab => navigate(`/notes/${tab}`) const setActiveTab = tab => navigate(`/notes/${tab}`)
// If they're not following anyone, skip network tab
if ($petnames.length === 0) {
setActiveTab('popular')
}
</script> </script>
<Content> <Content>
{#if !$user} {#if !$user}
<Content size="lg" class="text-center"> <Content size="lg" class="text-center">
<p> <p class="text-xl">Don't have an account?</p>
Don't have an account? Click <Anchor href="/login">here</Anchor> to join the nostr network. <p>Click <Anchor href="/login">here</Anchor> to join the nostr network.</p>
</p>
</Content> </Content>
{/if} {/if}
<div> <div>
<Tabs tabs={['network', 'popular']} {activeTab} {setActiveTab} /> <Tabs tabs={['network', 'popular']} {activeTab} {setActiveTab} />
{#if activeTab === 'network'} {#if activeTab === 'network'}
@ -40,11 +29,4 @@
{/if} {/if}
</div> </div>
</Content> </Content>
<NewNoteButton /> <NewNoteButton />
{#if $relays.length === 0}
<Modal>
<RelayList />
</Modal>
{/if}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {last, find} from 'ramda' import {last} from 'ramda'
import {onMount} from 'svelte' import {onMount} from 'svelte'
import {tweened} from 'svelte/motion' import {tweened} from 'svelte/motion'
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
@ -17,7 +17,7 @@
import Likes from "src/views/person/Likes.svelte" import Likes from "src/views/person/Likes.svelte"
import Relays from "src/views/person/Relays.svelte" import Relays from "src/views/person/Relays.svelte"
import {user, follows} from "src/agent/user" import {user, follows} from "src/agent/user"
import {getPubkeyWriteRelays} from "src/agent/relays" import {getUserReadRelays, getPubkeyWriteRelays} from "src/agent/relays"
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"
@ -28,6 +28,7 @@
export let relays = [] export let relays = []
const interpolate = (a, b) => t => a + Math.round((b - a) * t) const interpolate = (a, b) => t => a + Math.round((b - a) * t)
const {pubkeys: userFollows} = follows
let pubkey = nip19.decode(npub).data as string let pubkey = nip19.decode(npub).data as string
let following = false let following = false
@ -36,7 +37,7 @@
let person = database.getPersonWithFallback(pubkey) let person = database.getPersonWithFallback(pubkey)
let loading = true let loading = true
$: following = find(t => t[1] === pubkey, $user?.petnames || []) $: following = $userFollows.includes(pubkey)
onMount(async () => { onMount(async () => {
log('Person', npub, person) log('Person', npub, person)
@ -86,7 +87,9 @@
} }
const follow = async () => { const follow = async () => {
follows.addFollow(pubkey, relays[0].url, person.name) const [{url}] = relays.concat(getUserReadRelays())
follows.addFollow(pubkey, url, person.name)
} }
const unfollow = async () => { const unfollow = async () => {
@ -138,15 +141,15 @@
<Anchor type="button-circle" href={`/messages/${npub}`}> <Anchor type="button-circle" href={`/messages/${npub}`}>
<i class="fa fa-envelope" /> <i class="fa fa-envelope" />
</Anchor> </Anchor>
{#if following} {/if}
<Anchor type="button-circle" on:click={unfollow}> {#if following}
<i class="fa fa-user-minus" /> <Anchor type="button-circle" on:click={unfollow}>
</Anchor> <i class="fa fa-user-minus" />
{:else if $user} </Anchor>
<Anchor type="button-circle" on:click={follow}> {:else}
<i class="fa fa-user-plus" /> <Anchor type="button-circle" on:click={follow}>
</Anchor> <i class="fa fa-user-plus" />
{/if} </Anchor>
{/if} {/if}
<Anchor type="button-circle" on:click={share}> <Anchor type="button-circle" on:click={share}>
<i class="fa fa-share-nodes" /> <i class="fa fa-share-nodes" />

View File

@ -1,28 +1,11 @@
<script> <script>
import {pluck} from 'ramda'
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import RelaySearch from "src/partials/RelaySearch.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte" import RelayCard from "src/partials/RelayCard.svelte"
import {relays} from "src/agent/relays" import {relays} from "src/agent/relays"
import database from 'src/agent/database'
import {modal} from "src/app/ui" import {modal} from "src/app/ui"
let q = ""
let search
let knownRelays = database.watch('relays', t => t.all())
$: {
const joined = new Set(pluck('url', $relays))
search = fuzzy(
$knownRelays.filter(r => isRelay(r.url) && !joined.has(r.url)),
{keys: ["name", "description", "url"]}
)
}
</script> </script>
<div in:fly={{y: 20}}> <div in:fly={{y: 20}}>
@ -32,7 +15,7 @@
<i class="fa fa-server fa-lg" /> <i class="fa fa-server fa-lg" />
<h2 class="staatliches text-2xl">Your relays</h2> <h2 class="staatliches text-2xl">Your relays</h2>
</div> </div>
<Anchor type="button" on:click={() => modal.set({type: 'relay/add', url: q})}> <Anchor type="button" on:click={() => modal.set({type: 'relay/add'})}>
<i class="fa-solid fa-plus" /> Add Relay <i class="fa-solid fa-plus" /> Add Relay
</Anchor> </Anchor>
</div> </div>
@ -52,7 +35,6 @@
{/each} {/each}
</div> </div>
<div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}> <div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}>
{#if ($knownRelays || []).length > 0}
<div class="pt-2 mb-2 border-b border-solid border-medium" /> <div class="pt-2 mb-2 border-b border-solid border-medium" />
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<i class="fa fa-earth-asia fa-lg" /> <i class="fa fa-earth-asia fa-lg" />
@ -62,17 +44,7 @@
Coracle automatically discovers relays as you browse the network. Adding more relays Coracle automatically discovers relays as you browse the network. Adding more relays
will generally make things quicker to load, at the expense of higher data usage. will generally make things quicker to load, at the expense of higher data usage.
</p> </p>
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search"> <RelaySearch />
<i slot="before" class="fa-solid fa-search" />
</Input>
{/if}
{#each (search(q) || []).slice(0, 50) as relay (relay.url)}
<RelayCard {relay} />
{/each}
<small class="text-center">
Showing {Math.min(($knownRelays || []).length - $relays.length, 50)}
of {($knownRelays || []).length - $relays.length} known relays
</small>
</div> </div>
</Content> </Content>
</div> </div>

View File

@ -2,11 +2,8 @@
import {navigate} from 'svelte-routing' import {navigate} from 'svelte-routing'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Tabs from 'src/partials/Tabs.svelte' import Tabs from 'src/partials/Tabs.svelte'
import Modal from 'src/partials/Modal.svelte'
import SearchPeople from 'src/views/SearchPeople.svelte' import SearchPeople from 'src/views/SearchPeople.svelte'
import RelayList from "src/routes/RelayList.svelte"
import Scan from 'src/views/Scan.svelte' import Scan from 'src/views/Scan.svelte'
import {relays} from 'src/agent/relays'
export let activeTab export let activeTab
@ -21,9 +18,3 @@
<Scan /> <Scan />
{/if} {/if}
</Content> </Content>
{#if $relays.length === 0}
<Modal>
<RelayList />
</Modal>
{/if}

View File

@ -96,6 +96,8 @@ export const poll = (t, cb) => {
} }
export const createScroller = (loadMore, {reverse = false} = {}) => { export const createScroller = (loadMore, {reverse = false} = {}) => {
const THRESHOLD = 1200
// NOTE TO FUTURE SELF // NOTE TO FUTURE SELF
// If the scroller is saturating request channels on a slow relay, the // If the scroller is saturating request channels on a slow relay, the
// loadMore function is not properly awaiting all the work necessary. // loadMore function is not properly awaiting all the work necessary.
@ -106,8 +108,8 @@ export const createScroller = (loadMore, {reverse = false} = {}) => {
const {scrollY, innerHeight} = window const {scrollY, innerHeight} = window
const {scrollHeight} = document.body const {scrollHeight} = document.body
const shouldLoad = reverse const shouldLoad = reverse
? scrollY < 800 ? scrollY < THRESHOLD
: scrollY + innerHeight + 800 > scrollHeight : scrollY + innerHeight + THRESHOLD > scrollHeight
// Only trigger loading the first time we reach the threshold // Only trigger loading the first time we reach the threshold
if (shouldLoad) { if (shouldLoad) {

View File

@ -81,4 +81,6 @@ export const isAlert = (e, pubkey) => {
export const isRelay = url => typeof url === 'string' && url.match(/^wss?:\/\/.+/) export const isRelay = url => typeof url === 'string' && url.match(/^wss?:\/\/.+/)
export const normalizeRelayUrl = url => url.replace(/\/+$/, '')
export const roomAttrs = ['name', 'about', 'picture'] export const roomAttrs = ['name', 'about', 'picture']

View File

@ -51,7 +51,7 @@
text: 'View', text: 'View',
href: "/" + nip19.neventEncode({ href: "/" + nip19.neventEncode({
id: event.id, id: event.id,
relays: pluck('url', relays.slice(0, 5)), relays: pluck('url', relays.slice(0, 3)),
}), }),
}, },
}) })

View File

@ -1,16 +1,13 @@
<script> <script>
import {uniq} from 'ramda' import {shuffle} from 'src/util/misc'
import Notes from "src/partials/Notes.svelte" import Notes from "src/partials/Notes.svelte"
import {user} from 'src/agent/user' import {getUserNetwork} from 'src/agent/social'
import {getFollows, getNetwork} from 'src/agent/social'
import {getAllPubkeyWriteRelays} from 'src/agent/relays' import {getAllPubkeyWriteRelays} from 'src/agent/relays'
// Get first- and second-order follows. shuffle and slice network so we're not // 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. // sending too many pubkeys. This will also result in some variety.
const follows = getFollows($user?.pubkey) const authors = shuffle(getUserNetwork()).slice(0, 100)
const network = getNetwork($user?.pubkey) const relays = getAllPubkeyWriteRelays(authors).slice(0, 3)
const authors = uniq(follows.concat(network)).slice(0, 100)
const relays = getAllPubkeyWriteRelays(authors)
const filter = {kinds: [1, 7], authors} const filter = {kinds: [1, 7], authors}
</script> </script>