Add list save popover

This commit is contained in:
Jon Staab 2024-05-15 10:05:51 -07:00
parent 723de29c08
commit b327ed4497
12 changed files with 154 additions and 140 deletions

View File

@ -26,7 +26,6 @@
import FeedFormFilters from "src/app/shared/FeedFormFilters.svelte"
export let feed
export let saveAsList
enum FormType {
Advanced = "advanced",
@ -185,7 +184,7 @@
{#if formType === FormType.Advanced}
<FeedFormAdvanced {feed} onChange={onFeedChange} />
{:else}
<FeedFormFilters {feed} onChange={onFeedChange} bind:saveAsList />
<FeedFormFilters {feed} onChange={onFeedChange} />
{/if}
</FlexColumn>
</FlexColumn>

View File

@ -12,7 +12,7 @@
import FlexColumn from "src/partials/FlexColumn.svelte"
import Anchor from "src/partials/Anchor.svelte"
import FeedField from "src/app/shared/FeedField.svelte"
import {makeFeed, createFeed, editFeed, displayFeed} from "src/domain"
import {makeFeed, createFeed, editFeed, displayFeed, isTopicFeed, isMentionFeed} from "src/domain"
import {publishDeletionForEvent, createAndPublish, mention, hints} from "src/engine"
export let feed
@ -21,10 +21,6 @@
export let showSave = false
export let showDelete = false
const isTopicFeed = f => isTagFeed(f) && f[1] === "#t"
const isMentionFeed = f => isTagFeed(f) && f[1] === "#p"
const openSave = () => {
saveIsOpen = true
}
@ -67,42 +63,6 @@
const saveFeed = async () => {
const relays = hints.WriteRelays().getUrls()
// Create our lists
const addresses: string[] = []
await Promise.all(
saveAsList.map(async i => {
const subFeed = draft.definition[i]
let template
if (isAuthorFeed(subFeed)) {
template = {kind: NAMED_PEOPLE, tags: subFeed.slice(1).map(mention)}
} else if (isMentionFeed(subFeed)) {
template = {kind: NAMED_PEOPLE, tags: subFeed.slice(2).map(mention)}
} else if (isRelayFeed(subFeed)) {
template = {kind: NAMED_RELAYS, tags: subFeed.slice(1).map(url => ["r", url])}
} else if (isTopicFeed(subFeed)) {
template = {
kind: NAMED_TOPICS,
tags: subFeed.slice(1).map(topic => ["t", topic]),
}
}
if (template) {
const pub = await createAndPublish({...template, relays})
addresses.push(getAddress(pub.request.event))
}
}),
)
// Swap out various filters and use the new lists instead
if (addresses.length > 0) {
draft.definition = draft.definition
.filter((f, i) => !saveAsList.includes(i))
.concat([makeListFeed({addresses})])
}
const template = draft.event ? editFeed(draft) : createFeed(draft)
const pub = await createAndPublish({...template, relays})
@ -119,12 +79,11 @@
let deleteIsOpen = false
let saveIsOpen = showSave
let listDeleteIsOpen = false
let saveAsList = []
$: draft = shouldClone ? makeFeed({definition: feed.definition}) : feed
</script>
<FeedField bind:feed={feed.definition} bind:saveAsList />
<FeedField bind:feed={feed.definition} />
{#if !saveIsOpen}
<Card class="flex flex-col justify-between sm:flex-row">
@ -156,11 +115,6 @@
<Field label="Feed Description">
<Textarea bind:value={draft.description} />
</Field>
{#if saveAsList.length > 0}
<p class="text-neutral-500">
{quantify(saveAsList.length, "new list")} will be created based on the filters you've selected.
</p>
{/if}
</FlexColumn>
{#if !showSave}
<div class="absolute right-2 top-2 h-4 w-4 cursor-pointer" on:click={closeSave}>

View File

@ -35,29 +35,17 @@
import FeedFormSectionCreatedAt from "src/app/shared/FeedFormSectionCreatedAt.svelte"
import FeedFormSectionList from "src/app/shared/FeedFormSectionList.svelte"
import FeedFormSectionDVM from "src/app/shared/FeedFormSectionDVM.svelte"
import FeedFormSaveAsList from "src/app/shared/FeedFormSaveAsList.svelte"
import {isTopicFeed, isPeopleFeed, isMentionFeed} from "src/domain"
export let feed
export let onChange
export let saveAsList: number[]
const addFeed = newFeed => onChange([...feed, newFeed])
const onSubFeedChange = (i, newFeed) => onChange(feed.toSpliced(i, 1, newFeed))
const onSubFeedRemove = i => {
onChange(feed.toSpliced(i, 1))
saveAsList = without([i], saveAsList).map(j => (j > i ? j - 1 : j))
}
const toggleSaveAsList = i => {
saveAsList = saveAsList.includes(i) ? without([i], saveAsList) : saveAsList.concat(i)
}
const isTopicFeed = f => isTagFeed(f) && f[1] === "#t"
const isMentionFeed = f => isTagFeed(f) && f[1] === "#p"
const isPeopleFeed = f => isAuthorFeed(f) || isScopeFeed(f)
const onSubFeedRemove = i => onChange(feed.toSpliced(i, 1))
const openMenu = () => {
menuIsOpen = true
@ -108,13 +96,7 @@
{/if}
</FlexColumn>
{#if isAuthorFeed(subFeed) || isRelayFeed(subFeed) || isTopicFeed(subFeed) || isMentionFeed(subFeed)}
<div class="flex items-center gap-8">
<div class="flex items-center gap-2">
<Toggle value={saveAsList.includes(idx)} on:change={() => toggleSaveAsList(idx)} />
<p class="staatliches">Save as list</p>
</div>
<Input class="flex-grow" />
</div>
<FeedFormSaveAsList feed={subFeed} />
{/if}
</FlexColumn>
{#if i > 0}

View File

@ -0,0 +1,52 @@
<script lang="ts">
import {NAMED_PEOPLE, NAMED_RELAYS, NAMED_TOPICS} from '@welshman/util'
import {isAuthorFeed, isRelayFeed} from '@welshman/feeds'
import Card from 'src/partials/Card.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Popover2 from 'src/partials/Popover2.svelte'
import ListForm from 'src/app/shared/ListForm.svelte'
import {makeList, isTopicFeed, isPeopleFeed, isMentionFeed} from 'src/domain'
import {mention} from 'src/engine'
export let feed
const list = (() => {
if (isAuthorFeed(feed)) {
return makeList({kind: NAMED_PEOPLE, tags: feed.slice(1).map(mention)})
} else if (isMentionFeed(feed)) {
return makeList({kind: NAMED_PEOPLE, tags: feed.slice(2).map(mention)})
} else if (isRelayFeed(feed)) {
return makeList({kind: NAMED_RELAYS, tags: feed.slice(1).map(url => ["r", url])})
} else if (isTopicFeed(feed)) {
return makeList({
kind: NAMED_TOPICS,
tags: feed.slice(1).map(topic => ["t", topic]),
})
} else {
throw new Error(`Invalid feed type ${feed[0]} passed to FeedFormSaveAsList`)
}
})()
const openForm = () => {
formIsOpen = true
}
const closeForm = () => {
formIsOpen = false
}
let formIsOpen = false
</script>
<div class="flex flex-col items-end">
<Anchor underline class="text-neutral-500" on:click={openForm}>Save selection as list</Anchor>
{#if formIsOpen}
<div class="relative w-full">
<Popover2 onClose={closeForm}>
<Card class="shadow-xl mt-2">
<ListForm list={list} exit={closeForm} hide={["type", "tags"]} />
</Card>
</Popover2>
</div>
{/if}
</div>

View File

@ -2,7 +2,6 @@
import {identity} from "@welshman/lib"
import {Tags, NAMED_PEOPLE, NAMED_RELAYS, NAMED_TOPICS} from "@welshman/util"
import {showInfo} from "src/partials/Toast.svelte"
import Subheading from "src/partials/Subheading.svelte"
import Field from "src/partials/Field.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
@ -24,6 +23,7 @@
export let list
export let exit
export let hide = []
const submit = async () => {
const relays = hints.WriteRelays().getUrls()
@ -60,7 +60,6 @@
<form on:submit|preventDefault={submit}>
<FlexColumn>
<Subheading class="text-center">Manage list</Subheading>
<Field label="Name">
<Input bind:value={list.title} placeholder="My list" />
<p slot="info">Lists are identified by their name, so this has to be unique.</p>
@ -69,49 +68,53 @@
<Input bind:value={list.description} placeholder="About my list" />
<p slot="info">A brief description of what is in this list.</p>
</Field>
<Field label="List type">
<SearchSelect search={kindsHelper.search} value={list.kind} onChange={onKindChange}>
<div slot="item" let:item>{kindsHelper.display(item)}</div>
</SearchSelect>
</Field>
<Field label="List contents">
{#if list.kind === NAMED_PEOPLE}
<SearchSelect
multiple
value={Tags.wrap(list.tags).whereKey("p").values().valueOf()}
search={$searchPubkeys}
onChange={onPubkeysChange}>
<span slot="item" let:item let:context>
{#if context === "value"}
<Anchor modal href={router.at("people").of(item).toString()}>
{displayPubkey(item)}
</Anchor>
{:else}
<PersonBadge inert pubkey={item} />
{/if}
</span>
{#if !hide.includes("type")}
<Field label="List type">
<SearchSelect search={kindsHelper.search} value={list.kind} onChange={onKindChange}>
<div slot="item" let:item>{kindsHelper.display(item)}</div>
</SearchSelect>
{:else if list.kind === NAMED_RELAYS}
<SearchSelect
multiple
value={Tags.wrap(list.tags).whereKey("r").values().valueOf()}
search={$searchRelayUrls}
termToItem={identity}
onChange={onRelaysChange}>
<span slot="item" let:item>{displayRelayUrl(item)}</span>
</SearchSelect>
{:else if list.kind === NAMED_TOPICS}
<SearchSelect
multiple
value={Tags.wrap(list.tags).whereKey("t").values().valueOf()}
search={$searchTopicNames}
termToItem={identity}
onChange={onTopicsChange}>
<span slot="item" let:item>#{item}</span>
</SearchSelect>
{:else}
<p>Sorry, editing kind ${list.kind} lists isn't currently supported.</p>{/if}
</Field>
</Field>
{/if}
{#if !hide.includes("tags")}
<Field label="List contents">
{#if list.kind === NAMED_PEOPLE}
<SearchSelect
multiple
value={Tags.wrap(list.tags).whereKey("p").values().valueOf()}
search={$searchPubkeys}
onChange={onPubkeysChange}>
<span slot="item" let:item let:context>
{#if context === "value"}
<Anchor modal href={router.at("people").of(item).toString()}>
{displayPubkey(item)}
</Anchor>
{:else}
<PersonBadge inert pubkey={item} />
{/if}
</span>
</SearchSelect>
{:else if list.kind === NAMED_RELAYS}
<SearchSelect
multiple
value={Tags.wrap(list.tags).whereKey("r").values().valueOf()}
search={$searchRelayUrls}
termToItem={identity}
onChange={onRelaysChange}>
<span slot="item" let:item>{displayRelayUrl(item)}</span>
</SearchSelect>
{:else if list.kind === NAMED_TOPICS}
<SearchSelect
multiple
value={Tags.wrap(list.tags).whereKey("t").values().valueOf()}
search={$searchTopicNames}
termToItem={identity}
onChange={onTopicsChange}>
<span slot="item" let:item>#{item}</span>
</SearchSelect>
{:else}
<p>Sorry, editing kind ${list.kind} lists isn't currently supported.</p>{/if}
</Field>
{/if}
<div class="flex justify-between">
<Anchor button on:click={exit}>Discard</Anchor>
<Anchor button accent tag="button" type="submit">Save</Anchor>

View File

@ -58,22 +58,6 @@ export class FeedLoader {
isEventMuted = isEventMuted.get()
constructor(readonly opts: FeedOpts) {
const onEvent = cb => batch(300, async events => {
if (this.controller.signal.aborted) {
return
}
const keep = this.discardEvents(events)
if (this.opts.shouldLoadParents) {
this.loadParents(keep)
}
const ok = this.deferOrphans(keep)
cb(ok)
})
function* getRequestItems({relays, filters}) {
// Default to note kinds
filters = filters?.map(filter => ({kinds: noteKinds, ...filter})) || []
@ -107,26 +91,28 @@ export class FeedLoader {
// Use a custom feed loader so we can intercept the filters and infer relays
this.feedLoader = new CoreFeedLoader({
...baseFeedLoader.options,
request: async ({relays, filters}) => {
request: async ({relays, filters, onEvent}) => {
const tracker = new Tracker()
const signal = this.controller.signal
await Promise.all(
Array.from(getRequestItems({relays, filters})).map(opts =>
load({...opts, onEvent: onEvent(this.appendToFeed), tracker, signal}),
load({...opts, onEvent, tracker, signal}),
),
)
},
})
if (opts.shouldListen && this.feedLoader.compiler.canCompile(opts.feed)) {
console.log("listen")
this.feedLoader.compiler.compile(opts.feed).then(requests => {
const tracker = new Tracker()
const signal = this.controller.signal
const onEvent = this.onEvent(this.prependToFeed)
for (const {relays, filters} of requests) {
for (const opts of Array.from(getRequestItems({relays, filters}))) {
subscribe({...opts, onEvent: onEvent(this.prependToFeed), tracker, signal})
subscribe({...opts, onEvent, tracker, signal})
}
}
})
@ -137,6 +123,7 @@ export class FeedLoader {
start = () => {
this.loader = this.feedLoader.getLoader(this.opts.feed, {
onEvent: this.onEvent(this.appendToFeed),
onExhausted: () => {
this.done.set(true)
},
@ -153,6 +140,23 @@ export class FeedLoader {
// Event selection, deferral, and parent loading
onEvent = cb =>
batch(300, async events => {
if (this.controller.signal.aborted) {
return
}
const keep = this.discardEvents(events)
if (this.opts.shouldLoadParents) {
this.loadParents(keep)
}
const ok = this.deferOrphans(keep)
cb(ok)
})
discardEvents = events => {
let strict = true

View File

@ -1,4 +1,5 @@
<script lang="ts">
import Subheading from 'src/partials/Subheading.svelte'
import ListForm from "src/app/shared/ListForm.svelte"
import {router} from "src/app/util"
import {makeList} from "src/domain"
@ -8,4 +9,5 @@
const exit = () => router.clearModals()
</script>
<Subheading class="text-center">Create list</Subheading>
<ListForm {list} {exit} />

View File

@ -1,4 +1,5 @@
<script lang="ts">
import Subheading from 'src/partials/Subheading.svelte'
import ListForm from "src/app/shared/ListForm.svelte"
import {router} from "src/app/util"
import {readList} from "src/domain"
@ -12,6 +13,7 @@
</script>
{#if event}
<Subheading class="text-center">Edit list</Subheading>
<ListForm list={readList(event)} {exit} />
{:else}
<p class="text-center">Sorry, we weren't able to find that list.</p>

View File

@ -1,7 +1,13 @@
import {fromPairs, randomId} from "@welshman/lib"
import {FEED, Tags, getAddress} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {makeIntersectionFeed, hasSubFeeds} from "@welshman/feeds"
import {
makeIntersectionFeed,
hasSubFeeds,
isTagFeed,
isAuthorFeed,
isScopeFeed,
} from "@welshman/feeds"
import type {Feed as IFeed} from "@welshman/feeds"
import {SearchHelper} from "src/util/misc"
import {tryJson} from "src/util/misc"
@ -61,3 +67,9 @@ export class FeedSearch extends SearchHelper<Feed, string> {
display = (address: string) =>
displayFeed(this.options.find(feed => this.getValue(feed) === address))
}
export const isTopicFeed = f => isTagFeed(f) && f[1] === "#t"
export const isMentionFeed = f => isTagFeed(f) && f[1] === "#p"
export const isPeopleFeed = f => isAuthorFeed(f) || isScopeFeed(f)

View File

@ -90,7 +90,7 @@ export const getStaleAddrs = (addrs: string[]) => {
return Array.from(stale)
}
export const loadGroups = async (rawAddrs: string[], relays: string[] = []) => {
export const loadGroups = async (rawAddrs: string[], explicitRelays: string[] = []) => {
const addrs = getStaleAddrs(rawAddrs)
const authors = addrs.map(a => decodeAddress(a).pubkey)
const identifiers = addrs.map(a => decodeAddress(a).identifier)
@ -98,7 +98,9 @@ export const loadGroups = async (rawAddrs: string[], relays: string[] = []) => {
if (addrs.length > 0) {
const filters = [{kinds: [34550, 35834], authors, "#d": identifiers}]
const relays = forcePlatformRelays(
hints.merge([hints.product(addrs, relays), hints.WithinMultipleContexts(addrs)]).getUrls(),
hints
.merge([hints.product(addrs, explicitRelays), hints.WithinMultipleContexts(addrs)])
.getUrls(),
)
return load({relays, filters, skipCache: true})

View File

@ -64,7 +64,7 @@ import {
FEED,
decodeAddress,
Repository,
Relay,
Relay as LocalRelay,
Router,
Tags,
createEvent,
@ -173,7 +173,7 @@ export const channels = new Collection<Channel>("id")
export const repository = new Repository({throttle: 300})
export const relay = new Relay(repository)
export const relay = new LocalRelay(repository)
export const projections = new Worker<TrustedEvent>({
getKey: prop("kind"),

View File

@ -34,6 +34,8 @@
}
onMount(() => {
adjustHeight()
const interval = setInterval(adjustHeight, 300)
return () => {