mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 00:10:52 +00:00
Add list save popover
This commit is contained in:
parent
723de29c08
commit
b327ed4497
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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}
|
||||
|
52
src/app/shared/FeedFormSaveAsList.svelte
Normal file
52
src/app/shared/FeedFormSaveAsList.svelte
Normal 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>
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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})
|
||||
|
@ -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"),
|
||||
|
@ -34,6 +34,8 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
adjustHeight()
|
||||
|
||||
const interval = setInterval(adjustHeight, 300)
|
||||
|
||||
return () => {
|
||||
|
Loading…
Reference in New Issue
Block a user