Opt for fast rather than complete loading

This commit is contained in:
Jonathan Staab 2023-01-16 13:21:29 -08:00
parent c781e88574
commit 383fb6e85d
22 changed files with 136 additions and 156 deletions

View File

@ -76,13 +76,11 @@ If you like Coracle and want to support its development, you can donate sats via
- [x] Load feeds from network rather than user relays?
- [x] Still use "my" relays for global, this could make global feed more useful
- [x] If we use my relays for global, we don't have to wait for network to load initially
- [ ] Figure out fast vs complete tradeoff. Skipping loadContext speeds things up a ton.
- [ ] Make loadParents false by default. Maybe make fast the default
- [ ] Figure out how to make threads fast and complete. Load only the note, then preload entire thread in background?
- [x] Figure out fast vs complete tradeoff. Skipping loadContext speeds things up a ton.
- [ ] Add relays/mentions to note and reply composition
- [ ] Add layout component with max-w, padding, etc. Test on mobile size
- [ ] Figure out migrations from previous version
- [ ] Fix search
- [ ] Move add note to modal
## 0.2.7

View File

@ -23,6 +23,7 @@
import PubKeyLogin from "src/views/PubKeyLogin.svelte"
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
import NoteCreate from "src/views/NoteCreate.svelte"
import NotFound from "src/routes/NotFound.svelte"
import Search from "src/routes/Search.svelte"
import Alerts from "src/routes/Alerts.svelte"
@ -35,7 +36,6 @@
import RelayList from "src/routes/RelayList.svelte"
import AddRelay from "src/routes/AddRelay.svelte"
import Person from "src/routes/Person.svelte"
import NoteCreate from "src/routes/NoteCreate.svelte"
import Bech32Entity from "src/routes/Bech32Entity.svelte"
export let url = ""
@ -122,7 +122,6 @@
<Route path="/alerts" component={Alerts} />
<Route path="/search/:activeTab" component={Search} />
<Route path="/notes/:activeTab" component={Notes} />
<Route path="/notes/new" component={NoteCreate} />
<Route path="/people/:npub/:activeTab" let:params>
{#key params.npub}
<Person {...params} />
@ -171,7 +170,7 @@
</li>
{/if}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/latest">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/global">
<i class="fa-solid fa-tag mr-2" /> Notes
</a>
</li>
@ -228,9 +227,9 @@
{#if $canSign}
<div class="fixed bottom-0 right-0 m-8">
<a
href="/notes/new"
class="rounded-full bg-accent color-white w-16 h-16 flex justify-center
items-center border border-dark shadow-2xl cursor-pointer">
items-center border border-dark shadow-2xl cursor-pointer"
on:click={() => modal.set({form: 'note/create'})}>
<span class="fa-sold fa-plus fa-2xl" />
</a>
</div>
@ -242,6 +241,8 @@
{#key $modal.note.id}
<NoteDetail {...$modal} />
{/key}
{:else if $modal.form === 'note/create'}
<NoteCreate />
{:else if $modal.form === 'relay'}
<AddRelay />
{:else if $modal.form === 'signUp'}

View File

@ -52,11 +52,11 @@ export const getRelays = pubkey => {
}
export const getEventRelays = event => {
if (event.seen_on) {
return [{url: event.seen_on}]
}
return uniq(getRelays(event.pubkey).concat(Tags.from(event).relays())).map(objOf('url'))
return uniq(
getRelays(event.pubkey)
.concat(Tags.from(event).relays())
.concat(event.seen_on)
).map(objOf('url'))
}
export const publish = async (relays, event) => {

View File

@ -167,30 +167,27 @@ const subscribe = async (relays, filters) => {
}
}
const request = (relays, filters, {mode = "most"} = {}) => {
const request = (relays, filters, {threshold = 1} = {}) => {
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
return new Promise(async resolve => {
const agg = await subscribe(relays, filters)
const now = Date.now()
const relaysWithEvents = new Set()
const events = []
const eose = []
const attemptToComplete = () => {
// If we have all relays, most after a short timeout, or all after
// a long timeout, go ahead and unsubscribe.
// If we have all relays, more than `threshold` reporting events, most after
// a short timeout, or all after a long timeout, go ahead and unsubscribe.
const done = (
eose.length === agg.subs.length
|| Date.now() - now >= 5000
|| eose.filter(url => relaysWithEvents.has(url)).length > threshold
|| (
mode === "fast"
&& events.length
)
|| (
mode === "most"
&& Date.now() - now >= 1000
Date.now() - now >= 1000
&& eose.length > agg.subs.length - Math.round(agg.subs.length / 10)
)
|| Date.now() - now >= 5000
)
if (done) {
@ -212,7 +209,10 @@ const request = (relays, filters, {mode = "most"} = {}) => {
}
}
agg.onEvent(e => events.push(e))
agg.onEvent(e => {
relaysWithEvents.add(e.seen_on)
events.push(e)
})
agg.onEose(async url => {
if (!eose.includes(url)) {

View File

@ -1,10 +1,10 @@
import {whereEq, sortBy, identity, when, assoc, reject} from 'ramda'
import {pluck, whereEq, sortBy, identity, when, assoc, reject} from 'ramda'
import {navigate} from 'svelte-routing'
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {get} from 'svelte/store'
import {renderContent} from 'src/util/html'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {user, people, getPerson, getRelays, load, keys} from 'src/agent'
import {user, people, getPerson, getRelays, keys} from 'src/agent'
import defaults from 'src/agent/defaults'
import {toast, routes, modal, settings} from 'src/app/ui'
import cmd from 'src/app/cmd'
@ -29,7 +29,7 @@ export const login = async ({privkey, pubkey}, usingExtension = false) => {
alerts.load(getRelays(), pubkey),
alerts.listen(getRelays(), pubkey),
navigate('/notes/latest')
navigate('/notes/global')
}
export const addRelay = async relay => {
@ -77,16 +77,6 @@ export const setRelayWriteCondition = async (url, write) => {
}
}
export const loadNote = async (relays, id) => {
const [found] = await load(relays, {ids: [id]})
if (!found) {
return null
}
return annotate(found, await loaders.loadContext(relays, found, {mode: 'fast', loadChildren: true}))
}
export const render = (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire
const $people = get(people)
@ -133,8 +123,7 @@ export const annotate = (note, context) => {
}
export const threadify = (events, context, {muffle = []} = {}) => {
const contextById = createMap('id', context)
const contextById = createMap('id', events.concat(context))
// Show parents when possible. For reactions, if there's no parent,
// throw it away. Sort by created date descending
const notes = sortBy(
@ -144,14 +133,19 @@ export const threadify = (events, context, {muffle = []} = {}) => {
.filter(e => e && !muffle.includes(e.pubkey))
)
// Annotate our feed with parents, reactions, replies
return notes.map(note => {
let parent = contextById[findReplyId(note)]
// Don't show notes that will also show up as children
const noteIds = new Set(pluck('id', notes))
if (parent) {
parent = annotate(parent, context)
}
// Annotate our feed with parents, reactions, replies.
return notes
.filter(note => !noteIds.has(findReplyId(note)))
.map(note => {
let parent = contextById[findReplyId(note)]
return annotate({...note, parent}, context)
})
if (parent) {
parent = annotate(parent, context)
}
return annotate({...note, parent}, context)
})
}

View File

@ -1,4 +1,4 @@
import {propEq, uniqBy, prop, uniq, flatten, pluck, identity} from 'ramda'
import {uniqBy, prop, uniq, flatten, pluck, identity} from 'ramda'
import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak'
import {findReply, personKinds, Tags} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc'
@ -41,10 +41,10 @@ const loadNetwork = async (relays, pubkey) => {
const tags = Tags.wrap(petnames)
// Use nip-2 recommended relays to load our user's second-order follows
await loadPeople(tags.relays(), tags.values().all(), {mode: 'fast'})
await loadPeople(tags.relays(), tags.values().all())
}
const loadContext = async (relays, notes, {loadParents = true, loadChildren = false, ...opts} = {}) => {
const loadContext = async (relays, notes, {loadParents = false, depth = 0, ...opts} = {}) => {
notes = ensurePlural(notes)
if (notes.length === 0) {
@ -53,34 +53,39 @@ const loadContext = async (relays, notes, {loadParents = true, loadChildren = fa
return flatten(await Promise.all(
chunk(256, notes).map(async chunk => {
const authors = getStalePubkeys(pluck('pubkey', notes))
const parentTags = uniq(notes.map(findReply).filter(identity))
const chunkIds = pluck('id', chunk)
const authors = getStalePubkeys(pluck('pubkey', chunk))
const parentTags = uniq(chunk.map(findReply).filter(identity))
const parentIds = Tags.wrap(parentTags).values().all()
const combinedRelays = uniq(relays.concat(Tags.wrap(parentTags).relays()))
const filter = [{kinds: [1, 7], '#e': pluck('id', notes)}]
const filter = [{kinds: [1, 7], '#e': chunkIds}]
if (authors.length > 0) {
filter.push({kinds: personKinds, authors})
}
if (loadParents && parentTags.length > 0) {
filter.push({kinds: [1], ids: Tags.wrap(parentTags).values().all()})
filter.push({kinds: [1], ids: parentIds})
}
let events = await load(combinedRelays, filter, opts)
const children = events.filter(propEq('kind', 1))
const childRelays = Tags.from(children).relays()
// Find children, but only if we didn't already get them
const children = events.filter(e => e.kind === 1)
const childRelays = relays.concat(Tags.from(children).relays())
if (loadChildren && children.length > 0) {
events = events.concat(await loadContext(childRelays, children, {loadParents: false, ...opts}))
if (depth > 0 && children.length > 0) {
events = events.concat(
await loadContext(childRelays, children, {depth: depth - 1, ...opts})
)
}
if (loadParents && parentTags.length > 0) {
if (loadParents && parentIds.length > 0) {
const eventsById = createMap('id', events)
const parents = Tags.wrap(parentTags).values().all().map(id => eventsById[id]).filter(identity)
const parentRelays = Tags.from(parents).relays()
const parents = parentIds.map(id => eventsById[id]).filter(identity)
const parentRelays = relays.concat(Tags.from(parents).relays())
events = events.concat(await loadContext(parentRelays, parents, {loadParents: false, ...opts}))
events = events.concat(await loadContext(parentRelays, parents, opts))
}
// We're recurring and so may end up with duplicates here

View File

@ -21,6 +21,6 @@
)
</script>
<a on:click {...$$props} {href} class={className} target={external ? '_blank noopener' : null}>
<a on:click {...$$props} {href} class={className} target={external ? '_blank' : null}>
<slot />
</a>

View File

@ -11,8 +11,8 @@
in:fly={{y: 20}}
class={cx("py-2 px-3 flex flex-col gap-2 text-white", {
"cursor-pointer transition-all": interactive,
"border border-solid border-black hover:border-medium hover:bg-dark": interactive && !invertColors,
"border border-solid border-dark hover:border-medium hover:bg-medium": interactive && invertColors,
"hover:bg-dark": interactive && !invertColors,
"hover:bg-medium": interactive && invertColors,
})}>
<slot />
</li>

View File

@ -3,7 +3,7 @@
export let size = "2xl"
const className = "flex flex-col m-auto text-white gap-8"
const className = "flex flex-col m-auto text-white gap-6"
if (!['lg', '2xl'].includes(size)) {
throw new Error(`Invalid size: ${size}`)

View File

@ -15,7 +15,7 @@
import Badge from "src/partials/Badge.svelte"
import Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte"
import {user, getPerson, getRelays, getEventRelays} from 'src/agent'
import {user, people, getRelays, getEventRelays} from 'src/agent'
import cmd from 'src/app/cmd'
export let note
@ -31,7 +31,9 @@
const interactive = !anchorId || !showEntire
const relays = getEventRelays(note)
let likes, flags, like, flag
let likes, flags, like, flag, person
$: person = $people[note.pubkey] || {pubkey: note.pubkey}
$: {
likes = note.reactions.filter(n => isLike(n.content))
@ -116,7 +118,7 @@
<Card on:click={onClick} {interactive} {invertColors}>
<div class="flex gap-4 items-center justify-between">
<Badge person={getPerson(note.pubkey, true)} />
<Badge person={person} />
<Anchor
href={"/" + nip19.neventEncode({id: note.id, relays: pluck('url', relays)})}
class="text-sm text-light"

View File

@ -53,14 +53,17 @@
{#if newNotes.length > 0}
<div
in:slide
class="mb-2 cursor-pointer text-center underline text-light"
class="cursor-pointer text-center underline text-light"
on:click={showNewNotes}>
Load {quantify(newNotes.length, 'new note')}
</div>
{/if}
{#each notes as note (note.id)}
<Note {note} {depth} />
{/each}
</Content>
<Spinner />
<div>
{#each notes as note (note.id)}
<Note {note} {depth} />
{/each}
</div>
<Spinner />
</Content>

View File

@ -1,6 +1,7 @@
<script>
import {objOf} from 'ramda'
import {nip19} from 'nostr-tools'
import Content from 'src/partials/Content.svelte'
import NoteDetail from 'src/views/NoteDetail.svelte'
import Person from 'src/routes/Person.svelte'
@ -11,9 +12,13 @@
</script>
{#if type === "nevent"}
<NoteDetail note={{id: data.id}} {relays} />
<Content>
<NoteDetail note={{id: data.id}} {relays} />
</Content>
{:else if type === "note"}
<NoteDetail note={{id: data}} />
<Content>
<NoteDetail note={{id: data}} />
</Content>
{:else if type === "nprofile"}
<Person npub={nip19.npubEncode(data.pubkey)} {relays} activeTab="notes" />
{:else if type === "npub"}

View File

@ -2,5 +2,5 @@
import {onMount} from 'svelte'
import {navigate} from 'svelte-routing'
onMount(() => navigate('/notes/latest'))
onMount(() => navigate('/notes/global'))
</script>

View File

@ -1,48 +0,0 @@
<script>
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import Button from "src/partials/Button.svelte"
import Compose from "src/partials/Compose.svelte"
import Content from "src/partials/Content.svelte"
import Heading from 'src/partials/Heading.svelte'
import {user, getRelays} from "src/agent"
import {toast} from "src/app"
import cmd from "src/app/cmd"
let input = null
const onSubmit = async e => {
const {content, mentions} = input.parse()
if (content) {
await cmd.createNote(getRelays(), content, mentions)
toast.show("info", `Your note has been created!`)
history.back()
}
}
onMount(() => {
if (!$user) {
navigate("/login")
}
})
</script>
<form on:submit|preventDefault={onSubmit} in:fly={{y: 20}}>
<Content size="lg">
<Heading class="text-center">Create a note</Heading>
<div class="flex flex-col gap-4 w-full">
<div class="flex flex-col gap-2">
<strong>What do you want to say?</strong>
<div class="border-l-2 border-solid border-medium pl-4">
<Compose bind:this={input} {onSubmit} />
</div>
</div>
<Button type="submit" class="text-center">Send</Button>
</div>
</Content>
</form>

View File

@ -4,7 +4,7 @@
import Content from "src/partials/Content.svelte"
import Tabs from "src/partials/Tabs.svelte"
import Network from "src/views/notes/Network.svelte"
import Latest from "src/views/notes/Latest.svelte"
import Global from "src/views/notes/Global.svelte"
import {user} from 'src/agent'
export let activeTab
@ -12,18 +12,21 @@
const setActiveTab = tab => navigate(`/notes/${tab}`)
</script>
{#if !$user}
<Content size="lg" class="text-center">
<p>
Don't have an account? Click <Anchor href="/login">here</Anchor> to join the nostr network.
</p>
<Content>
{#if !$user}
<Content size="lg" class="text-center">
<p>
Don't have an account? Click <Anchor href="/login">here</Anchor> to join the nostr network.
</p>
</Content>
{/if}
<div>
<Tabs tabs={['global', 'network']} {activeTab} {setActiveTab} />
{#if activeTab === 'network'}
<Network />
{:else}
<Global />
{/if}
</div>
</Content>
{/if}
<Tabs tabs={['latest', 'network']} {activeTab} {setActiveTab} />
{#if activeTab === 'network'}
<Network />
{:else}
<Latest />
{/if}

View File

@ -1,5 +1,5 @@
<script>
import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import Input from "src/partials/Input.svelte"
import Content from 'src/partials/Content.svelte'
import Tabs from 'src/partials/Tabs.svelte'

View File

@ -97,7 +97,7 @@ export const renderContent = content => {
const $a = document.createElement('a')
$a.href = url
$a.target = "_blank noopener"
$a.target = "_blank"
$a.className = "underline"
/* eslint no-useless-escape: 0 */
@ -107,5 +107,5 @@ export const renderContent = content => {
content = content.replace(url, $a.outerHTML)
}
return content
return content.trim()
}

View File

@ -49,6 +49,10 @@ export const findRoot = e =>
export const findRootId = e => Tags.wrap([findRoot(e)]).values().first()
export const displayPerson = p => {
if (p.display_name) {
return p.display_name
}
if (p.name) {
return p.name
}

View File

@ -1,7 +1,10 @@
<script>
import {onMount} from 'svelte'
import {nip19} from 'nostr-tools'
import {fly} from 'svelte/transition'
import {loadNote} from 'src/app'
import {load} from 'src/agent'
import {annotate} from 'src/app'
import loaders from 'src/app/loaders'
import Note from 'src/partials/Note.svelte'
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte'
@ -11,17 +14,28 @@
let loading = true
const logNote = () => {
if (note) {
onMount(async () => {
const [found] = await load(relays, {ids: [note.id]})
if (found) {
// Show the main note without waiting for context
if (!note.pubkey) {
note = annotate(found, [])
}
const context = await loaders.loadContext(relays, found, {
depth: 3,
loadParents: true,
})
note = annotate(found, context)
console.log('NoteDetail', nip19.noteEncode(note.id), note)
} else if (!note.pubkey) {
note = null
}
}
loadNote(relays, note.id).then(found => {
note = found
loading = false
logNote()
})
</script>

View File

@ -1,6 +1,5 @@
<script>
import {prop} from 'ramda'
import {fly} from 'svelte/transition'
import {ellipsize} from 'hurdak/lib/hurdak'
import {fuzzy} from "src/util/misc"
import {renderContent} from "src/util/html"

View File

@ -18,7 +18,7 @@
const loadNotes = async () => {
const {limit, until} = cursor
const notes = await load(relays, {...filter, limit, until}, {mode: 'fast'})
const notes = await load(relays, {...filter, limit, until})
const context = await loaders.loadContext(relays, notes)
cursor.onChunk(notes)

View File

@ -24,7 +24,7 @@
const loadNotes = async () => {
const {limit, until} = cursor
const notes = await load(relays, {...filter, limit, until}, {mode: 'fast'})
const notes = await load(relays, {...filter, limit, until})
const context = await loaders.loadContext(relays, notes)
cursor.onChunk(notes)