Add relay symbol to notes, publish optimistically, reduce how many relays replies are published to, add more logging, re-work thread layout, optimize note loading by switching from debounce to throttle

This commit is contained in:
Jonathan Staab 2023-02-14 17:12:44 -06:00
parent 233d754258
commit f7e6d46fcf
24 changed files with 248 additions and 147 deletions

View File

@ -21,6 +21,7 @@ If you like Coracle and want to support its development, you can donate sats via
# Snacks
- [ ] Pinned posts ala snort
- [ ] Add nip05 verification on feed
- [ ] Linkify follow/followers numbers
- [ ] Support key delegation
@ -30,10 +31,10 @@ If you like Coracle and want to support its development, you can donate sats via
- [ ] Attachments (a tag w/content type and url)
- [ ] 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
- [ ] QR code generation/scanner to share nprofile https://cdn.jb55.com/s/d966a729777c2021.MP4
# Missions
- [ ] Topics/hashtag views
- [ ] Support paid relays
- atlas.nostr.land
- eden.nostr.land
@ -74,17 +75,16 @@ If you like Coracle and want to support its development, you can donate sats via
# Current
- [ ] Implement gossip model https://bountsr.org/code/2023/02/03/gossip-model.html
- [_] Add nip 05 to calculation
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP
- nevent1qqspjcqw2hu5gfcpkrjhs0aqvxuzjgtp50l375mcqjfpmk48cg5hevgpr3mhxue69uhkummnw3ez6un9d3shjtnhd3m8xtnnwpskxegpzamhxue69uhkummnw3ezuendwsh8w6t69e3xj7spramhxue69uhkummnw3ez6un9d3shjtnwdahxxefwv93kzer9d4usz9rhwden5te0wfjkccte9ejxzmt4wvhxjmcpr9mhxue69uhkummnw3ezuer9d3hjuum0ve68wctjv5n8hwfg
- [ ] 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.
- [ ] Custom views: slider between fast/complete with a warning at either extreme
- [ ] Deterministically calculate color for relays, show it on notes. User popper?
- [ ] Likes list
- [ ] Fix anon/new user experience
- [ ] Likes are slow
- [ ] Show loading on replies/notes
- [ ] Show loading on replies/new notes
- [ ] Initial user load doesn't have any relays, cache user or wait for people db to be loaded
# Changelog

View File

@ -26,6 +26,7 @@
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import Modal from 'src/partials/Modal.svelte'
import RelayCard from "src/partials/RelayCard.svelte"
import SignUp from "src/views/SignUp.svelte"
import PersonList from "src/views/PersonList.svelte"
import PrivKeyLogin from "src/views/PrivKeyLogin.svelte"
@ -361,6 +362,12 @@
<NoteCreate />
{:else if $modal.type === 'relay/add'}
<AddRelay />
{:else if $modal.type === 'relay/list'}
<Content>
{#each $modal.relays as relay}
<RelayCard showControls {relay} />
{/each}
</Content>
{:else if $modal.type === 'signUp'}
<SignUp />
{:else if $modal.type === 'room/edit'}

View File

@ -1,3 +1,4 @@
import type {MyEvent} from 'src/util/types'
import {prop, pick, join, uniqBy, last} from 'ramda'
import {get} from 'svelte/store'
import {first} from "hurdak/lib/hurdak"
@ -80,18 +81,19 @@ const deleteEvent = (relays, ids) =>
// Utils
const publishEvent = (relays, kind, {content = '', tags = []} = {}) => {
const publishEvent = (relays, kind, {content = '', tags = []} = {}): [MyEvent, Promise<MyEvent>] => {
if (relays.length === 0) {
throw new Error("Unable to publish, no relays specified")
}
const pubkey = get(keys.pubkey)
const createdAt = Math.round(new Date().valueOf() / 1000)
const event = {kind, content, tags, pubkey, created_at: createdAt}
const event = {kind, content, tags, pubkey, created_at: createdAt} as MyEvent
log("Publishing", event, relays)
return network.publish(relays, event)
// Return the event synchronously, separate from the promise
return [event, network.publish(relays, event)]
}
export default {

View File

@ -1,7 +1,7 @@
import type {Person, MyEvent} from 'src/util/types'
import type {Readable} from 'svelte/store'
import {isEmpty, pick, identity, sortBy, uniq, reject, groupBy, last, propEq, uniqBy, prop} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {first, ensurePlural} from 'hurdak/lib/hurdak'
import {derived, get} from 'svelte/store'
import {Tags} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc'
@ -61,17 +61,25 @@ export const getTopRelays = (pubkeys, mode = 'all') => {
export const getBestRelay = (pubkey, mode = 'all') =>
first(getTopRelays([pubkey], mode).concat(getPubkeyRelays(pubkey, mode)))
export const getEventRelays = event => {
export const getAllEventRelays = events => {
return uniqBy(
prop('url'),
getPubkeyRelays(event.pubkey, 'write')
.concat(Tags.from(event).relays())
.concat({url: event.seen_on})
ensurePlural(events)
.flatMap(event =>
getPubkeyRelays(event.pubkey, 'write')
.concat(Tags.from(event).relays())
.concat({url: event.seen_on})
)
)
}
export const getTopRelaysFromEvents = (events: Array<MyEvent>) =>
uniqBy(prop('url'), events.map(e => getBestRelay(e.pubkey) || {url: e.seen_on}))
export const getTopEventRelays = (events: Array<MyEvent>, mode = 'all') =>
uniqBy(
prop('url'),
ensurePlural(events)
.flatMap(e => [getBestRelay(e.pubkey, mode), {url: e.seen_on}])
.filter(identity)
)
export const getStalePubkeys = pubkeys => {
// If we're not reloading, only get pubkeys we don't already know about

View File

@ -2,7 +2,7 @@ import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda'
import {findReply, personKinds, findReplyId, Tags} from 'src/util/nostr'
import {chunk} from 'hurdak/lib/hurdak'
import {batch} from 'src/util/misc'
import {getFollows, getStalePubkeys, getTopRelaysFromEvents} from 'src/agent/helpers'
import {getFollows, getStalePubkeys, getTopEventRelays} from 'src/agent/helpers'
import pool from 'src/agent/pool'
import keys from 'src/agent/keys'
import sync from 'src/agent/sync'
@ -85,7 +85,7 @@ const loadParents = (relays, notes) => {
}
return load(
relays.concat(getTopRelaysFromEvents(notes)),
relays.concat(getTopEventRelays(notes, 'read')),
{kinds: [1], ids: Array.from(parentIds)}
)
}

View File

@ -1,9 +1,9 @@
import type {Relay} from 'nostr-tools'
import type {MyEvent} from 'src/util/types'
import {relayInit} from 'nostr-tools'
import {uniqBy, prop, find, is} from 'ramda'
import {uniqBy, without, prop, find, is} from 'ramda'
import {ensurePlural} from 'hurdak/lib/hurdak'
import {warn} from 'src/util/logger'
import {warn, log} from 'src/util/logger'
import {isRelay} from 'src/util/nostr'
import {sleep} from 'src/util/misc'
import database from 'src/agent/database'
@ -127,6 +127,8 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
const seen = new Set()
const eose = new Set()
log(`Starting subscription ${id} with ${relays.length} relays`, filters)
// Don't await before returning so we're not blocking on slow connects
const promises = relays.map(async relay => {
const conn = await connect(relay.url)
@ -175,6 +177,8 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
return {
unsub: () => {
log(`Closing subscription ${id}`)
promises.forEach(async promise => {
const sub = await promise
@ -206,7 +210,16 @@ const subscribeUntilEose = async (
const eose = new Set()
const attemptToComplete = () => {
if (eose.size === relays.length || Date.now() - now >= timeout) {
const isComplete = eose.size === relays.length
const isTimeout = Date.now() - now >= timeout
if (isTimeout) {
const timedOutRelays = without(Array.from(eose), relays)
log(`Timing out ${timedOutRelays.length} relays after ${timeout}ms`, timedOutRelays)
}
if (isComplete || isTimeout) {
onClose?.()
agg.unsub()
}

View File

@ -1,5 +1,6 @@
<script>
import {Link} from 'svelte-routing'
import ImageCircle from 'src/partials/ImageCircle.svelte'
import {killEvent} from 'src/util/html'
import {displayPerson} from 'src/util/nostr'
import {routes} from 'src/app/ui'
@ -10,9 +11,7 @@
{#if inert}
<span class="flex gap-2 items-center relative z-10">
<div
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({person.picture})" />
<ImageCircle src={person.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</span>
{:else}
@ -20,9 +19,7 @@
to={routes.person(person.pubkey)}
class="flex gap-2 items-center relative z-10"
on:click={killEvent}>
<div
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({person.picture})" />
<ImageCircle src={person.picture} />
<span class="text-lg font-bold">{displayPerson(person)}</span>
</Link>
{/if}

View File

@ -9,7 +9,7 @@
<div
on:click
in:fly={{y: 20}}
class={cx("py-2 px-3 flex flex-col gap-2 text-white", {
class={cx($$props.class, "card py-2 px-3 text-white", {
"cursor-pointer transition-all": interactive,
"hover:bg-dark": interactive && !invertColors,
"hover:bg-medium": interactive && invertColors,

View File

@ -0,0 +1,10 @@
<script lang="ts">
import cx from 'classnames'
export let src
export let size = 4
</script>
<div
class={cx($$props.class, `overflow-hidden w-${size} h-${size} rounded-full bg-cover bg-center shrink-0 border border-solid border-white`)}
style="background-image: url({src})" />

View File

@ -1,23 +1,25 @@
<script lang="ts">
import cx from 'classnames'
import {nip19} from 'nostr-tools'
import {always, whereEq, without, uniq, pluck, reject, propEq, find} from 'ramda'
import {last, always, whereEq, without, uniq, pluck, reject, propEq, find} from 'ramda'
import {onMount} from 'svelte'
import {tweened} from 'svelte/motion'
import {slide} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {quantify} from 'hurdak/lib/hurdak'
import {Tags, findReply, findRoot, findReplyId, displayPerson, isLike} from "src/util/nostr"
import {extractUrls} from "src/util/html"
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, renderNote} from "src/app"
import {formatTimestamp} from 'src/util/misc'
import Badge from "src/partials/Badge.svelte"
import Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte"
import {user, getEventRelays} from 'src/agent/helpers'
import {user, getTopEventRelays, getAllEventRelays} from 'src/agent/helpers'
import database from 'src/agent/database'
import cmd from 'src/agent/cmd'
import {routes} from 'src/app/ui'
export let note
export let anchorId = null
@ -37,7 +39,7 @@
const interactive = !anchorId || !showEntire
const person = database.watch('people', () => database.getPersonWithFallback(note.pubkey))
let likes, flags, like, flag
let likes, flags, like, flag, border, childrenContainer, noteContainer
const interpolate = (a, b) => t => a + Math.round((b - a) * t)
const likesCount = tweened(0, {interpolate})
@ -60,30 +62,35 @@
const target = e.target as HTMLElement
if (interactive && !['I'].includes(target.tagName) && !target.closest('a')) {
modal.set({type: 'note/detail', note, relays: getEventRelays(note)})
modal.set({type: 'note/detail', note, relays: getTopEventRelays(note)})
}
}
const goToParent = async () => {
const [id, url] = findReply(note).slice(1)
const relays = getEventRelays(note).concat({url})
const relays = getTopEventRelays(note).concat({url})
modal.set({type: 'note/detail', note: {id}, relays})
}
const goToRoot = async () => {
const [id, url] = findRoot(note).slice(1)
const relays = getEventRelays(note).concat({url})
const relays = getTopEventRelays(note).concat({url})
modal.set({type: 'note/detail', note: {id}, relays})
}
const showActiveRelays = () => {
modal.set({type: 'relay/list', relays: [{url: note.seen_on}]})
}
const react = async content => {
if (!$user) {
return navigate('/login')
}
const event = await cmd.createReaction(getEventRelays(note), note, content)
const relays = getTopEventRelays(note)
const [event] = cmd.createReaction(relays, note, content)
if (content === '+') {
likes = likes.concat(event)
@ -95,7 +102,7 @@
}
const deleteReaction = e => {
cmd.deleteEvent(getEventRelays(note), [e.id])
cmd.deleteEvent(getAllEventRelays(note), [e.id])
if (e.content === '+') {
likes = reject(propEq('pubkey', $user.pubkey), likes)
@ -123,14 +130,14 @@
replyMentions = getDefaultReplyMentions()
}
const sendReply = async () => {
const sendReply = () => {
let {content, mentions, topics} = reply.parse()
if (content) {
mentions = uniq(mentions.concat(replyMentions))
const relays = getEventRelays(note)
const event = await cmd.createReply(relays, note, content, mentions, topics)
const relays = getTopEventRelays(note)
const [event] = cmd.createReply(relays, note, content, mentions, topics)
toast.show("info", {
text: `Your note has been created!`,
@ -154,6 +161,33 @@
resetReply()
}
}
const setBorderHeight = () => {
const getHeight = e => e?.getBoundingClientRect().height || 0
if (childrenContainer && noteContainer) {
const lastChild = last(
[].slice.apply(childrenContainer.children)
.filter(e => e.matches('.note'))
)
const height = (
getHeight(noteContainer)
+ getHeight(replyContainer)
+ getHeight(childrenContainer)
- getHeight(lastChild)
- getHeight(lastChild.nextElementSibling)
)
border.style = `height: ${height - 21}px`
}
}
onMount(() => {
const interval = setInterval(setBorderHeight, 300)
return () => clearInterval(interval)
})
</script>
<svelte:body
@ -166,64 +200,85 @@
/>
{#if $person && shouldDisplay(note)}
<Card on:click={onClick} {interactive} {invertColors}>
<div class="flex gap-4 items-center justify-between">
<Badge person={$person} />
<Anchor
href={"/" + nip19.neventEncode({
id: note.id,
relays: pluck('url', getEventRelays(note).slice(0, 5)),
})}
class="text-sm text-light"
type="unstyled">
{formatTimestamp(note.created_at)}
<div bind:this={noteContainer} class="note relative">
<div class="absolute w-px bg-light z-10 ml-8 mt-12 transition-all h-0" bind:this={border} />
<Card class="flex gap-4 relative" on:click={onClick} {interactive} {invertColors}>
{#if !showParent}
<div class="absolute h-px w-3 bg-light z-10" style="left: 0px; top: 27px;" />
{/if}
<Anchor class="text-lg font-bold" href={routes.person($person.pubkey)}>
<ImageCircle size={10} src={$person.picture} />
</Anchor>
</div>
<div class="ml-6 flex flex-col gap-2">
{#if findReply(note) && showParent}
<small class="text-light">
Reply to <Anchor on:click={goToParent}>{findReplyId(note).slice(0, 8)}</Anchor>
</small>
{/if}
{#if findRoot(note) && findRoot(note) !== findReply(note) && showParent}
<small class="text-light">
Go to <Anchor on:click={goToRoot}>root</Anchor>
</small>
{/if}
{#if flag}
<p class="text-light border-l-2 border-solid border-medium pl-4">
You have flagged this content as offensive.
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
</p>
{:else}
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
<p>{@html renderNote(note, {showEntire})}</p>
{#each links.slice(-2) as link}
<button class="inline-block" on:click={e => e.stopPropagation()}>
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
</button>
{/each}
</div>
<div class="flex gap-6 text-light" on:click={e => e.stopPropagation()}>
<div>
<button class="fa fa-reply cursor-pointer" on:click={startReply} />
{$repliesCount}
<div class="flex flex-col gap-2 flex-grow min-w-0">
<div class="flex items-center justify-between">
<Anchor type="unstyled" class="text-lg font-bold flex gap-2 items-center" href={routes.person($person.pubkey)}>
<span>{displayPerson($person)}</span>
{#if $person.verified_as}
<i class="fa fa-circle-check text-accent text-sm" />
{/if}
</Anchor>
<Anchor
href={"/" + nip19.neventEncode({
id: note.id,
relays: pluck('url', getTopEventRelays(note).slice(0, 5)),
})}
class="text-sm text-light"
type="unstyled">
{formatTimestamp(note.created_at)}
</Anchor>
</div>
<div class={cx({'text-accent': like})}>
<button class="fa fa-heart cursor-pointer" on:click={() => like ? deleteReaction(like) : react("+")} />
{$likesCount}
</div>
<div>
<button class="fa fa-flag cursor-pointer" on:click={() => react("-")} />
{$flagsCount}
<div class="flex flex-col gap-2">
{#if findReply(note) && showParent}
<small class="text-light">
Reply to <Anchor on:click={goToParent}>{findReplyId(note).slice(0, 8)}</Anchor>
</small>
{/if}
{#if findRoot(note) && findRoot(note) !== findReply(note) && showParent}
<small class="text-light">
Go to <Anchor on:click={goToRoot}>root</Anchor>
</small>
{/if}
{#if flag}
<p class="text-light border-l-2 border-solid border-medium pl-4">
You have flagged this content as offensive.
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
</p>
{:else}
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
<p>{@html renderNote(note, {showEntire})}</p>
{#each links.slice(-2) as link}
<button class="inline-block" on:click={e => e.stopPropagation()}>
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
</button>
{/each}
</div>
<div class="flex justify-between text-light" on:click={e => e.stopPropagation()}>
<div class="flex gap-6">
<div>
<button class="fa fa-reply cursor-pointer" on:click={startReply} />
{$repliesCount}
</div>
<div class={cx({'text-accent': like})}>
<button class="fa fa-heart cursor-pointer" on:click={() => like ? deleteReaction(like) : react("+")} />
{$likesCount}
</div>
<div>
<button class="fa fa-flag cursor-pointer" on:click={() => react("-")} />
{$flagsCount}
</div>
</div>
<div class="cursor-pointer text-light" on:click={showActiveRelays}>
<i class="fa fa-server" />
</div>
</div>
{/if}
</div>
</div>
{/if}
</div>
</Card>
</Card>
</div>
{#if reply}
<div transition:slide class="note-reply" bind:this={replyContainer}>
<div transition:slide class="note-reply relative z-10" bind:this={replyContainer}>
<div class="bg-medium border-medium border border-solid">
<Compose bind:this={reply} onSubmit={sendReply}>
<button
@ -236,7 +291,7 @@
</Compose>
</div>
{#if replyMentions.length > 0}
<div class="text-white text-sm p-2 rounded-b border-t-0 border border-solid border-medium">
<div class="text-white text-sm p-2 rounded-b border-t-0 border border-solid border-medium bg-black">
<div class="inline-block border-r border-solid border-medium pl-1 pr-3 mr-2">
<i class="fa fa-at" />
</div>
@ -252,7 +307,8 @@
</div>
{/if}
<div class="ml-5 border-l border-solid border-medium">
{#if note.children.length > 0}
<div class="ml-8 note-children" bind:this={childrenContainer}>
{#if !showEntire && note.children.length > 3}
<button class="ml-5 py-2 text-light cursor-pointer" on:click={onClick}>
<i class="fa fa-up-down text-sm pr-2" />
@ -264,3 +320,5 @@
{/each}
</div>
{/if}
{/if}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {onMount} from 'svelte'
import {propEq, always, mergeRight, uniqBy, sortBy, prop} from 'ramda'
import {partition, propEq, always, mergeRight, uniqBy, sortBy, prop} from 'ramda'
import {slide} from 'svelte/transition'
import {quantify} from 'hurdak/lib/hurdak'
import {createScroller, now, Cursor} from 'src/util/misc'
@ -18,6 +18,7 @@
let notes = []
let notesBuffer = []
const since = now()
const maxNotes = 300
const muffle = getMuffle()
const cursor = new Cursor()
@ -52,47 +53,40 @@
// Drop notes at the end if there are a lot
notes = uniqBy(
prop('id'),
notesBuffer.splice(0).filter(shouldDisplay).concat(notes).slice(0, maxNotes)
notesBuffer.filter(shouldDisplay).concat(notes).slice(0, maxNotes)
)
notesBuffer = []
}
const onChunk = async newNotes => {
const chunk = sortBy(e => -e.created_at, await processNewNotes(newNotes))
const [bottom, top] = partition(e => e.created_at < since, chunk)
// Slice new notes in case someone leaves the tab open for a long time
notes = uniqBy(prop('id'), notes.concat(bottom))
notesBuffer = top.concat(notesBuffer).slice(0, maxNotes)
// Check all notes every time to stay very conservative with moving the window
cursor.onChunk(notes)
}
onMount(() => {
const sub = network.listen(
relays,
{...filter, since: now()},
async newNotes => {
const chunk = await processNewNotes(newNotes)
const sub = network.listen(relays, {...filter, since}, onChunk)
// Slice new notes in case someone leaves the tab open for a long time
notesBuffer = chunk.concat(notesBuffer).slice(0, maxNotes)
}
)
const scroller = createScroller(async () => {
const scroller = createScroller(() => {
if ($modal) {
return
}
const {limit, until} = cursor
return network.listenUntilEose(
relays,
{...filter, limit, until},
async newNotes => {
cursor.onChunk(newNotes)
const chunk = await processNewNotes(newNotes)
notes = sortBy(e => -e.created_at, uniqBy(prop('id'), notes.concat(chunk)))
}
)
return network.listenUntilEose(relays, {...filter, until, limit}, onChunk)
})
return async () => {
const {unsub} = await sub
return () => {
scroller.stop()
unsub()
sub.then(s => s?.unsub())
}
})
</script>

View File

@ -10,7 +10,6 @@
import {addRelay, removeRelay, setRelayWriteCondition} from "src/app"
export let relay
export let i = 0
export let showControls = false
let status = null
@ -35,7 +34,7 @@
<div
class="rounded border border-solid border-medium bg-dark shadow flex flex-col justify-between gap-3 py-3 px-6"
in:fly={{y: 20, delay: i * 100}}>
in:fly={{y: 20}}>
<div class="flex gap-2 items-center justify-between">
<div class="flex gap-2 items-center text-xl">
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />

View File

@ -9,14 +9,14 @@
<div class="inline-block">
<div class="rounded flex border border-solid border-light cursor-pointer">
{#each options as option, i}
<button
class={cx("px-4 py-2", {
<div
class={cx("px-4 py-2 transition-all", {
"border-l border-solid border-light": i > 0,
"bg-accent": value === option,
})}
on:click={() => { value = option }}>
{option}
</button>
</div>
{/each}
</div>
</div>

View File

@ -5,6 +5,7 @@
import Content from 'src/partials/Content.svelte'
import NoteDetail from 'src/views/NoteDetail.svelte'
import Person from 'src/routes/Person.svelte'
import {getUserRelays} from 'src/agent/helpers'
export let entity
@ -13,7 +14,7 @@
onMount(() => {
try {
({type, data} = nip19.decode(entity) as {type: string, data: any})
relays = (data.relays || []).map(objOf('url'))
relays = (data.relays || []).map(objOf('url')).concat(getUserRelays())
} catch (e) {
// pass
}

View File

@ -3,7 +3,7 @@
import {nip19} from 'nostr-tools'
import {now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte'
import {getEventRelays, user} from 'src/agent/helpers'
import {getTopEventRelays, user} from 'src/agent/helpers'
import database from 'src/agent/database'
import network from 'src/agent/network'
import {modal} from 'src/app'
@ -15,7 +15,7 @@
const room = database.watch('rooms', rooms => rooms.get(roomId))
const listenForMessages = async cb => {
const relays = getEventRelays($room)
const relays = getTopEventRelays($room)
return network.listen(
relays,
@ -33,7 +33,7 @@
}
const loadMessages = async ({until, limit}) => {
const relays = getEventRelays($room)
const relays = getTopEventRelays($room)
const events = await network.load(relays, {kinds: [42], '#e': [roomId], until, limit})
if (events.length) {
@ -48,7 +48,7 @@
}
const sendMessage = content =>
cmd.createChatMessage(getEventRelays($room), roomId, content)
cmd.createChatMessage(getTopEventRelays($room), roomId, content)
</script>
<Channel

View File

@ -58,7 +58,7 @@
const sendMessage = async content => {
const cyphertext = await crypt.encrypt(pubkey, content)
const event = await cmd.createDirectMessage(getRelays(), pubkey, cyphertext)
const [event] = cmd.createDirectMessage(getRelays(), pubkey, cyphertext)
// Return unencrypted content so we can display it immediately
return {...event, content}

View File

@ -46,7 +46,7 @@
const submit = async event => {
event.preventDefault()
await cmd.updateUser(getUserRelays('write'), values)
cmd.updateUser(getUserRelays('write'), values)
navigate(routes.person($user.pubkey, 'profile'))

View File

@ -78,8 +78,8 @@
<div class="text-center">No relays connected</div>
{/if}
<div class="grid grid-cols-1 gap-4">
{#each relays as relay, i (relay.url)}
<RelayCard showControls {relay} {i} />
{#each relays as relay (relay.url)}
<RelayCard showControls {relay} />
{/each}
</div>
<div class="flex flex-col gap-6" in:fly={{y: 20, delay: 1000}}>

View File

@ -107,3 +107,4 @@ export const renderContent = content => {
return content.trim()
}

View File

@ -1,4 +1,4 @@
import {debounce} from 'throttle-debounce'
import {debounce, throttle} from 'throttle-debounce'
import {allPass, prop, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda"
import Fuse from "fuse.js/dist/fuse.min.js"
import {writable} from 'svelte/store'
@ -151,7 +151,16 @@ export class Cursor {
this.limit = limit
}
onChunk(events) {
this.until = events.reduce((t, e) => Math.min(t, e.created_at), this.until)
if (events.length > 0) {
// Don't go straight to the earliest event, since relays often spit
// very old stuff at us. Use an overlapping window instead.
const sortedEvents = sortBy(prop('created_at'), events)
const midpointEvent = sortedEvents[Math.floor(events.length / 2)]
this.until = Math.min(this.until, midpointEvent.created_at)
}
return this
}
}
@ -172,7 +181,7 @@ export const shuffle = sortBy(() => Math.random() > 0.5)
export const batch = (t, f) => {
const xs = []
const cb = debounce(t, () => f(xs.splice(0)))
const cb = throttle(t, () => f(xs.splice(0)))
return x => {
xs.push(x)

View File

@ -36,9 +36,9 @@
if (!room.name) {
toast.show("error", "Please enter a name for your room.")
} else {
const event = room.id
? await cmd.updateRoom(getUserRelays('write'), room)
: await cmd.createRoom(getUserRelays('write'), room)
const [event] = room.id
? cmd.updateRoom(getUserRelays('write'), room)
: cmd.createRoom(getUserRelays('write'), room)
await database.rooms.patch({id: room.id || event.id, joined: true})

View File

@ -36,7 +36,7 @@
const {content, mentions, topics} = input.parse()
if (content) {
const event = await cmd.createNote(relays, content, mentions, topics)
const [event] = cmd.createNote(relays, content, mentions, topics)
toast.show("info", {
text: `Your note has been created!`,

View File

@ -4,7 +4,7 @@
import {fly} from 'svelte/transition'
import {first} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger'
import {getEventRelays, getUserRelays} from 'src/agent/helpers'
import {getAllEventRelays} from 'src/agent/helpers'
import network from 'src/agent/network'
import Note from 'src/partials/Note.svelte'
import Content from 'src/partials/Content.svelte'
@ -12,11 +12,13 @@
import {asDisplayEvent} from 'src/app'
export let note
export let relays = getEventRelays(note)
export let relays = []
let loading = true
onMount(async () => {
relays = relays.concat(getAllEventRelays(note))
if (!note.pubkey) {
note = first(await network.load(relays, {ids: [note.id]}))
}
@ -25,9 +27,9 @@
log('NoteDetail', nip19.noteEncode(note.id), note)
network.streamContext({
relays,
depth: 10,
notes: [note],
relays: getUserRelays().concat(relays),
updateNotes: cb => {
note = first(cb([note]))
},

View File

@ -15,9 +15,9 @@
{#if (person.relays || []).length === 0}
<div class="pt-8 text-center">No relays found</div>
{:else}
{#each person.relays as relay, i (relay.url)}
{#each person.relays as relay (relay.url)}
{#if relay.write !== '!'}
<RelayCard {relay} {i} />
<RelayCard {relay} />
{/if}
{/each}
{/if}