Abort feed when updating opts

This commit is contained in:
Jon Staab 2024-05-08 14:21:47 -07:00
parent 324045a1cb
commit 5a813e394e
18 changed files with 83 additions and 61 deletions

View File

@ -24,6 +24,7 @@
- [x] Introduce new in-memory relay - [x] Introduce new in-memory relay
- [x] Re-work feed controls - [x] Re-work feed controls
- [x] Re-work utility library - [x] Re-work utility library
- [x] Make buttons, chips, and inputs sleeker
# 0.4.4 # 0.4.4

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import {equals} from 'ramda'
import {now} from "@welshman/lib" import {now} from "@welshman/lib"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {quantify, seconds} from "hurdak" import {quantify, seconds} from "hurdak"
@ -98,7 +97,7 @@
<div class="relative"> <div class="relative">
<div class="flex"> <div class="flex">
<Input <Input
class="h-7 !rounded !border-tinted-700 !bg-neutral-800 !px-2 py-px text-tinted-200 outline-none" class="border-tinted-700 bg-neutral-800 py-px text-tinted-200 outline-none"
on:blur={onSearchBlur} on:blur={onSearchBlur}
on:keydown={onSearchKeydown} on:keydown={onSearchKeydown}
bind:element={searchInput} bind:element={searchInput}

View File

@ -52,9 +52,10 @@
} }
} }
const update = async opts => { const onChange = async opts => {
limit = 0 limit = 0
feed = opts.feed feed = opts.feed
Storage.setJson("hideReplies", opts.shouldHideReplies)
start(opts) start(opts)
if (feedLoader.compiler.canCompile(opts.feed)) { if (feedLoader.compiler.canCompile(opts.feed)) {
@ -66,10 +67,7 @@
} }
} }
$: { onChange(opts)
update(opts)
Storage.setJson("hideReplies", opts.shouldHideReplies)
}
onMount(() => { onMount(() => {
const scroller = createScroller(loadMore, {element}) const scroller = createScroller(loadMore, {element})
@ -79,7 +77,7 @@
</script> </script>
{#if showControls} {#if showControls}
<FeedControls {address} bind:opts /> <FeedControls {opts} {address} {onChange} />
{/if} {/if}
<FlexColumn xl bind:element> <FlexColumn xl bind:element>

View File

@ -20,6 +20,7 @@
} from "src/engine" } from "src/engine"
export let opts export let opts
export let onChange
export let address = null export let address = null
const openListMenu = () => { const openListMenu = () => {
@ -39,13 +40,13 @@
} }
const toggleReplies = () => { const toggleReplies = () => {
opts = {...opts, shouldHideReplies: !opts.shouldHideReplies} onChange({...opts, shouldHideReplies: !opts.shouldHideReplies})
} }
const getSearch = definition => (getFeedArgs(definition)?.find(isSearchFeed)?.[1] as string) || "" const getSearch = definition => (getFeedArgs(definition)?.find(isSearchFeed)?.[1] as string) || ""
const setFeedDefinition = definition => { const setFeedDefinition = definition => {
opts = {...opts, feed: definition} onChange({...opts, feed: definition})
search = getSearch(definition) search = getSearch(definition)
closeListMenu() closeListMenu()
closeForm() closeForm()
@ -103,8 +104,9 @@
<div class="flex justify-between"> <div class="flex justify-between">
<Select <Select
dark
value={subFeeds.find(isScopeFeed)?.[1] || null} value={subFeeds.find(isScopeFeed)?.[1] || null}
class="hidden h-7 bg-tinted-700 text-neutral-200 sm:block"> class="hidden bg-tinted-700 sm:block">
<option value={Scope.Follows}>Follows</option> <option value={Scope.Follows}>Follows</option>
<option value={Scope.Network}>Network</option> <option value={Scope.Network}>Network</option>
<option value={null}>Global</option> <option value={null}>Global</option>
@ -112,27 +114,28 @@
<div class="flex flex-grow items-center justify-end gap-2"> <div class="flex flex-grow items-center justify-end gap-2">
<div class="flex"> <div class="flex">
<Input <Input
class="hidden h-7 rounded-r-none bg-neutral-900 xs:block" dark
class="hidden rounded-r-none xs:block"
on:input={onSearchBlur} on:input={onSearchBlur}
bind:value={search}> bind:value={search}>
<div slot="after" class="hidden text-white xs:block"> <div slot="after" class="hidden text-white xs:block">
<i class="fa fa-search" /> <i class="fa fa-search" />
</div> </div>
</Input> </Input>
<Anchor button low class="h-7 border-none xs:rounded-l-none" on:click={openForm}> <Anchor button low class="border-none xs:rounded-l-none" on:click={openForm}>
Filters ({feed.definition.length - 1}) Filters ({feed.definition.length - 1})
</Anchor> </Anchor>
</div> </div>
<div class="float-right flex h-8 items-center justify-end gap-2"> <div class="float-right flex h-8 items-center justify-end gap-2">
{#if opts.shouldHideReplies} {#if opts.shouldHideReplies}
<Anchor button low class="h-7 border-none opacity-50" on:click={toggleReplies} <Anchor button low class="border-none opacity-50" on:click={toggleReplies}
>Replies</Anchor> >Replies</Anchor>
{:else} {:else}
<Anchor button accent class="h-7 border-none" on:click={toggleReplies}>Replies</Anchor> <Anchor button accent class="border-none" on:click={toggleReplies}>Replies</Anchor>
{/if} {/if}
<div class="relative lg:hidden"> <div class="relative lg:hidden">
<div <div
class="flex h-7 w-6 cursor-pointer items-center justify-center rounded bg-neutral-700 text-center text-neutral-50 transition-colors hover:bg-neutral-600" class="flex h-8 w-6 cursor-pointer items-center justify-center rounded bg-neutral-700 text-center text-neutral-50 transition-colors hover:bg-neutral-600"
on:click={openListMenu}> on:click={openListMenu}>
<i class="fa fa-sm fa-ellipsis-v" /> <i class="fa fa-sm fa-ellipsis-v" />
</div> </div>

View File

@ -33,7 +33,7 @@
</div> </div>
<QRCode code={zap.invoice} onClick={collect}> <QRCode code={zap.invoice} onClick={collect}>
<div slot="below" let:copy class="flex gap-1"> <div slot="below" let:copy class="flex gap-1">
<Input value={zap.invoice} wrapperClass="flex-grow"> <Input value={zap.invoice} class="flex-grow">
<button slot="after" class="fa fa-copy" on:click={copy} /> <button slot="after" class="fa fa-copy" on:click={copy} />
</Input> </Input>
{#await getLightningImplementation()} {#await getLightningImplementation()}

View File

@ -48,6 +48,7 @@ export class FeedLoader {
done = false done = false
loader: Promise<Loader> loader: Promise<Loader>
feedLoader: CoreFeedLoader<Rumor> feedLoader: CoreFeedLoader<Rumor>
controller = new AbortController()
notes = writable<DisplayEvent[]>([]) notes = writable<DisplayEvent[]>([])
parents = new Map<string, DisplayEvent>() parents = new Map<string, DisplayEvent>()
reposts = new Map<string, Event[]>() reposts = new Map<string, Event[]>()
@ -60,6 +61,8 @@ export class FeedLoader {
this.feedLoader = new CoreFeedLoader({ this.feedLoader = new CoreFeedLoader({
...baseFeedLoader.options, ...baseFeedLoader.options,
request: async ({relays, filters, onEvent}) => { request: async ({relays, filters, onEvent}) => {
const signal = this.controller.signal
// Default to note kinds // Default to note kinds
filters = filters?.map(filter => ({kinds: noteKinds, ...filter})) || [] filters = filters?.map(filter => ({kinds: noteKinds, ...filter})) || []
@ -73,10 +76,10 @@ export class FeedLoader {
// Use relays specified in feeds // Use relays specified in feeds
if (relays?.length > 0) { if (relays?.length > 0) {
promises.push(load({filters, relays, tracker, onEvent})) promises.push(load({filters, relays, tracker, onEvent, signal}))
} else { } else {
if (!this.opts.skipCache) { if (!this.opts.skipCache) {
promises.push(load({filters, relays: [LOCAL_RELAY_URL], tracker, onEvent})) promises.push(load({filters, relays: [LOCAL_RELAY_URL], tracker, onEvent, signal}))
} }
if (!this.opts.skipNetwork) { if (!this.opts.skipNetwork) {
@ -87,7 +90,7 @@ export class FeedLoader {
} }
for (const {relay, filters} of selections) { for (const {relay, filters} of selections) {
promises.push(load({filters, relays: [relay], tracker, onEvent})) promises.push(load({filters, relays: [relay], tracker, onEvent, signal}))
} }
} }
} }
@ -100,11 +103,17 @@ export class FeedLoader {
// Public api // Public api
start = (opts: Partial<FeedOpts>) => { start = (opts: Partial<FeedOpts>) => {
const controller = new AbortController()
Object.assign(this.opts, opts) Object.assign(this.opts, opts)
this.loader = this.feedLoader.getLoader(this.opts.feed, { this.loader = this.feedLoader.getLoader(this.opts.feed, {
onEvent: batch(300, async events => { onEvent: batch(300, async events => {
const keep = await this.discardEvents(events) if (controller.signal.aborted) {
return
}
const keep = this.discardEvents(events)
if (this.opts.shouldLoadParents) { if (this.opts.shouldLoadParents) {
this.loadParents(keep) this.loadParents(keep)
@ -121,6 +130,8 @@ export class FeedLoader {
// Clear everything out // Clear everything out
this.notes.set([]) this.notes.set([])
this.controller.abort()
this.controller = controller
} }
subscribe = f => this.notes.subscribe(f) subscribe = f => this.notes.subscribe(f)
@ -129,7 +140,7 @@ export class FeedLoader {
// Event selection, deferral, and parent loading // Event selection, deferral, and parent loading
discardEvents = async events => { discardEvents = events => {
let strict = true let strict = true
// Be more tolerant when looking at communities // Be more tolerant when looking at communities
@ -186,14 +197,20 @@ export class FeedLoader {
return true return true
}) })
const {signal} = this.controller
const selections = hints.merge(notesWithParent.map(hints.EventParents)).getSelections() const selections = hints.merge(notesWithParent.map(hints.EventParents)).getSelections()
for (const {relay, values} of selections) { for (const {relay, values} of selections) {
load({ load({
filters: getIdFilters(values), filters: getIdFilters(values),
signal: this.controller.signal,
relays: this.opts.skipPlatform ? [relay] : forcePlatformRelays([relay]), relays: this.opts.skipPlatform ? [relay] : forcePlatformRelays([relay]),
onEvent: batch(100, async events => { onEvent: batch(100, async events => {
for (const e of await this.discardEvents(events)) { if (signal.aborted) {
return
}
for (const e of this.discardEvents(events)) {
for (const k of getIdAndAddress(e)) { for (const k of getIdAndAddress(e)) {
this.parents.set(k, e) this.parents.set(k, e)
} }
@ -215,7 +232,13 @@ export class FeedLoader {
return parents.length === 0 || parents.some(k => this.parents.has(k)) return parents.length === 0 || parents.some(k => this.parents.has(k))
}, notes) }, notes)
setTimeout(() => this.addToFeed(defer), 3000) const {signal} = this.controller
setTimeout(() => {
if (!signal.aborted) {
this.addToFeed(defer)
}
}, 3000)
return ok return ok
} }

View File

@ -77,7 +77,7 @@
<p class="text-center py-8">You haven't yet joined any groups.</p> <p class="text-center py-8">You haven't yet joined any groups.</p>
{/each} {/each}
<div class="mb-2 border-b border-solid border-neutral-600 pt-2" /> <div class="mb-2 border-b border-solid border-neutral-600 pt-2" />
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Search groups"> <Input bind:value={q} type="text" class="flex-grow" placeholder="Search groups">
<i slot="before" class="fa-solid fa-search" /> <i slot="before" class="fa-solid fa-search" />
</Input> </Input>
{#each otherGroups as group (group.address)} {#each otherGroups as group (group.address)}

View File

@ -136,8 +136,7 @@
<div class="flex"> <div class="flex">
<Input <Input
bind:value={username} bind:value={username}
class="rounded-r-none" class="flex-grow rounded-r-none"
wrapperClass="flex-grow"
placeholder="Username"> placeholder="Username">
<i slot="before" class="fa fa-user-astronaut" /> <i slot="before" class="fa fa-user-astronaut" />
</Input> </Input>
@ -146,8 +145,7 @@
defaultOptions={handlers} defaultOptions={handlers}
getKey={prop("domain")} getKey={prop("domain")}
termToItem={objOf("domain")} termToItem={objOf("domain")}
inputClass="rounded-l-none border-l-0" inputClass="rounded-l-none border-l-0 flex-grow"
inputWrapperClass="flex-grow"
search={() => handlers}> search={() => handlers}>
<i slot="before" class="fa fa-at relative top-[2px]" /> <i slot="before" class="fa fa-at relative top-[2px]" />
<span slot="item" let:item>{item.domain}</span> <span slot="item" let:item>{item.domain}</span>

View File

@ -42,7 +42,7 @@
</div> </div>
<p>Help people recognize you by setting up your profile.</p> <p>Help people recognize you by setting up your profile.</p>
<Field label="Your Name"> <Field label="Your Name">
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={profile.name}> <Input type="text" name="name" class="flex-grow" bind:value={profile.name}>
<i slot="before" class="fa-solid fa-user-astronaut" /> <i slot="before" class="fa-solid fa-user-astronaut" />
</Input> </Input>
</Field> </Field>

View File

@ -202,7 +202,7 @@
<Input <Input
bind:value={q} bind:value={q}
type="text" type="text"
wrapperClass="flex-grow" class="flex-grow"
placeholder="Search relays or add a custom url"> placeholder="Search relays or add a custom url">
<i slot="before" class="fa-solid fa-search" /> <i slot="before" class="fa-solid fa-search" />
</Input> </Input>

View File

@ -35,13 +35,13 @@
</div> </div>
<div class="flex w-full flex-col gap-8"> <div class="flex w-full flex-col gap-8">
<Field label="Username"> <Field label="Username">
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={values.name}> <Input type="text" name="name" class="flex-grow" bind:value={values.name}>
<i slot="before" class="fa-solid fa-user-astronaut" /> <i slot="before" class="fa-solid fa-user-astronaut" />
</Input> </Input>
<div slot="info">In most clients, this image will be shown on your profile page.</div> <div slot="info">In most clients, this image will be shown on your profile page.</div>
</Field> </Field>
<Field label="NIP-05 Identifier"> <Field label="NIP-05 Identifier">
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={values.nip05}> <Input type="text" name="name" class="flex-grow" bind:value={values.nip05}>
<i slot="before" class="fa-solid fa-user-check" /> <i slot="before" class="fa-solid fa-user-check" />
</Input> </Input>
<div slot="info"> <div slot="info">
@ -50,7 +50,7 @@
</div> </div>
</Field> </Field>
<Field label="Lightning address"> <Field label="Lightning address">
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={values.lud16}> <Input type="text" name="name" class="flex-grow" bind:value={values.lud16}>
<i slot="before" class="fa-solid fa-bolt" /> <i slot="before" class="fa-solid fa-bolt" />
</Input> </Input>
<div slot="info"> <div slot="info">
@ -59,7 +59,7 @@
</div> </div>
</Field> </Field>
<Field label="Website"> <Field label="Website">
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={values.website}> <Input type="text" name="name" class="flex-grow" bind:value={values.website}>
<i slot="before" class="fa-solid fa-link" /> <i slot="before" class="fa-solid fa-link" />
</Input> </Input>
<div slot="info">Enter any url where people can find out more about you.</div> <div slot="info">Enter any url where people can find out more about you.</div>

View File

@ -38,7 +38,9 @@
</div> </div>
<div class="flex w-full flex-col gap-8"> <div class="flex w-full flex-col gap-8">
<Field label="Default zap amount"> <Field label="Default zap amount">
<Input bind:value={settings.default_zap} /> <Input bind:value={settings.default_zap}>
<i slot="before" class="fa fa-bolt" />
</Input>
<p slot="info">The default amount of sats to use when sending a lightning tip.</p> <p slot="info">The default amount of sats to use when sending a lightning tip.</p>
</Field> </Field>
<Field label="Platform zap split"> <Field label="Platform zap split">
@ -58,7 +60,7 @@
<strong>Max relays per request</strong> <strong>Max relays per request</strong>
<div>{settings.relay_limit} relays</div> <div>{settings.relay_limit} relays</div>
</div> </div>
<Input type="range" bind:value={settings.relay_limit} min={1} max={30} parse={parseInt} /> <Input type="range" class="bg-transparent" bind:value={settings.relay_limit} min={1} max={30} parse={parseInt} />
<p slot="info"> <p slot="info">
This controls how many relays to max out at when loading feeds and event context. More is This controls how many relays to max out at when loading feeds and event context. More is
faster, but will require more bandwidth and processing power. faster, but will require more bandwidth and processing power.

View File

@ -33,11 +33,7 @@ export const projections = new Worker<Event>({
}) })
projections.addGlobalHandler(event => { projections.addGlobalHandler(event => {
const kinds = [ const kinds = [Kind.Delete, Kind.Feed, Kind.ListBookmarks]
Kind.Delete,
Kind.Feed,
Kind.ListBookmarks,
]
if (kinds.includes(event.kind)) { if (kinds.includes(event.kind)) {
repository.publish(event) repository.publish(event)
@ -221,7 +217,7 @@ export const createAndPublish = async ({
const template = createEvent(kind, {content, tags}) const template = createEvent(kind, {content, tags})
const event = await sign(template, {anonymous, sk}) const event = await sign(template, {anonymous, sk})
return publish({event, relays, timeout, verb}) return publish({event, relays, verb, signal: AbortSignal.timeout(timeout)})
} }
setInterval(() => { setInterval(() => {

View File

@ -18,7 +18,6 @@
export let danger = false export let danger = false
export let circle = false export let circle = false
export let underline = false export let underline = false
export let short = false
export let tall = false export let tall = false
export let grow = false export let grow = false
export let style = null export let style = null
@ -44,10 +43,10 @@
button, button,
"aspect-square flex justify-center items-center rounded-full !p-0": circle, "aspect-square flex justify-center items-center rounded-full !p-0": circle,
"aspect-square flex justify-center items-center": square, "aspect-square flex justify-center items-center": square,
"h-7": short, "h-7": button && !tall,
"w-7": short && circle, "w-7": button && !tall && circle,
"h-10": tall, "h-10": button && tall,
"w-10": tall && circle, "w-10": button && tall && circle,
"flex-grow": grow, "flex-grow": grow,
}) })

View File

@ -55,7 +55,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
{#if icon} {#if icon}
<Input type="text" wrapperClass="flex-grow" bind:value placeholder="https://"> <Input type="text" class="flex-grow" bind:value placeholder="https://">
<i slot="before" class={`fa fa-${icon}`} /> <i slot="before" class={`fa fa-${icon}`} />
</Input> </Input>
{/if} {/if}

View File

@ -3,19 +3,18 @@
import {identity} from "ramda" import {identity} from "ramda"
export let initialValue: string | number = "" export let initialValue: string | number = ""
export let wrapperClass = ""
export let value = initialValue export let value = initialValue
export let element = null export let element = null
export let hideBefore = false export let hideBefore = false
export let hideAfter = false export let hideAfter = false
export let format: (x: any) => string = identity export let format: (x: any) => string = identity
export let parse: (x: string) => any = identity export let parse: (x: string) => any = identity
export let dark = false
const showBefore = $$slots.before && !hideBefore const showBefore = $$slots.before && !hideBefore
const showAfter = $$slots.after && !hideAfter const showAfter = $$slots.after && !hideAfter
const className = cx( const className = cx(
$$props.class, "outline-none px-3 w-full placeholder:text-neutral-400 h-7 bg-transparent",
"outline-none rounded shadow-inset py-2 px-4 w-full placeholder:text-neutral-400 text-black",
{"pl-10": showBefore, "pr-10": showAfter}, {"pl-10": showBefore, "pr-10": showAfter},
) )
@ -26,7 +25,11 @@
$: inputValue = format(value) $: inputValue = format(value)
</script> </script>
<div class={cx(wrapperClass, "relative")}> <div
class={cx($$props.class, "shadow-inset relative rounded h-7 overflow-hidden", {
"bg-neutral-900 text-neutral-100": dark,
"bg-white dark:text-neutral-900": !dark,
})}>
<input <input
{...$$props} {...$$props}
class={className} class={className}
@ -39,15 +42,14 @@
on:input on:input
on:keydown /> on:keydown />
{#if showBefore} {#if showBefore}
<div class="absolute left-0 top-2 flex items-center gap-2 px-4 text-black opacity-75"> <div class="absolute left-0 top-0 flex items-center gap-2 px-3 opacity-75 h-7">
<div> <div>
<slot name="before" /> <slot name="before" />
</div> </div>
</div> </div>
{/if} {/if}
{#if showAfter} {#if showAfter}
<div <div class="absolute right-0 top-0 m-px flex items-center gap-2 rounded-full px-3 opacity-75 h-7">
class="absolute right-0 top-2 m-px flex items-center gap-2 rounded-full px-4 text-black opacity-75">
<div> <div>
<slot name="after" /> <slot name="after" />
</div> </div>

View File

@ -9,7 +9,6 @@
export let value = null export let value = null
export let onChange = null export let onChange = null
export let inputClass = "" export let inputClass = ""
export let inputWrapperClass = ""
export let placeholder = "" export let placeholder = ""
export let delimiters = [] export let delimiters = []
export let search = null export let search = null
@ -134,7 +133,6 @@
<div> <div>
<Input <Input
class={cx(inputClass, "cursor-text text-black outline-0")} class={cx(inputClass, "cursor-text text-black outline-0")}
wrapperClass={inputWrapperClass}
{autofocus} {autofocus}
{placeholder} {placeholder}
bind:value={term} bind:value={term}

View File

@ -3,15 +3,18 @@
export let value export let value
export let onChange = null export let onChange = null
export let wrapperClass = "" export let dark = false
const className = cx($$props.class, "rounded shadow-inset px-4 w-full cursor-pointer", { const className = cx("px-3 w-full bg-transparent h-7", {
"pl-10": $$slots.before, "pl-10": $$slots.before,
"pr-10": $$slots.after, "pr-10": $$slots.after,
}) })
</script> </script>
<div class={cx(wrapperClass, "relative")}> <div class={cx($$props.class, "rounded relative shadow-inset cursor-pointer h-7", {
"bg-neutral-900 text-neutral-100": dark,
"bg-neutral-100 text-neutral-900": !dark,
})}>
<select {...$$props} class={className} bind:value on:change={() => onChange(value)}> <select {...$$props} class={className} bind:value on:change={() => onChange(value)}>
<slot /> <slot />
</select> </select>