Re-work feeds based on new spec

This commit is contained in:
Jon Staab 2024-05-01 16:15:00 -07:00
parent 9f2e59e679
commit 05eecb42b3
40 changed files with 775 additions and 684 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
package-lock.json -diff
yarn.lock -diff

View File

@ -11,7 +11,6 @@
export let dvmItem
export let onChange
export let onRemove
const removeTag = i => {
onChange({...dvmItem, tags: dvmItem.tags.toSpliced(i, 1)})
@ -29,6 +28,7 @@
{label: "Content discovery", kind: 5300},
{label: "Person discovery", kind: 5301},
{label: "Content search", kind: 5302},
{label: "Person Search", kind: 5303},
]
const searchKindItems = fuzzy(kinds, {keys: ["kind", "label"]})
@ -46,14 +46,18 @@
<FlexColumn class="relative">
<Field label="Kind">
<SearchSelect search={searchKinds} value={dvmItem.kind} onChange={setKind} termToItem={identity}>
<SearchSelect
search={searchKinds}
value={dvmItem.kind}
onChange={setKind}
termToItem={identity}>
<div slot="item" let:item>{displayKind(item)}</div>
</SearchSelect>
</Field>
<Field label="Relays">
<SearchSelect
multiple
value={dvmItem.relays}
value={dvmItem.relays || []}
search={$searchRelayUrls}
termToItem={normalizeRelayUrl}
onChange={relays => onChange({...dvmItem, relays})}>
@ -61,17 +65,17 @@
</SearchSelect>
<p slot="info">Select which relays requests to this DVM should be sent to.</p>
</Field>
{#each dvmItem.tags as [type, value], i (i + key)}
<div class="flex gap-2 items-center justify-between">
{#each dvmItem.tags || [] as [type, value], i (i + key)}
<div class="flex items-center justify-between gap-2">
<i class="fa fa-trash cursor-pointer" on:click={() => removeTag(i)} />
<div class="flex gap-2 items-center justify-end">
<div class="flex gap-3 items-center">
<div class="flex items-center justify-end gap-2">
<div class="flex items-center gap-3">
<label>Type</label>
<Input bind:value={type} on:change={() => onChange(dvmItem)} />
</div>
<div class="flex gap-3 items-center">
<div class="flex items-center gap-3">
<label>Value</label>
<Input bind:value={value} on:change={() => onChange(dvmItem)} />
<Input bind:value on:change={() => onChange(dvmItem)} />
</div>
</div>
</div>
@ -79,7 +83,4 @@
<Anchor on:click={addTag} class="cursor-pointer">
<i class="fa fa-plus" /> Add tag
</Anchor>
<div class="absolute -right-4 -top-2 flex h-4 w-4 cursor-pointer items-center justify-center" on:click={onRemove}>
<i class="fa fa-times fa-lg" />
</div>
</FlexColumn>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {Storage} from "hurdak"
import {prop} from "ramda"
import type {Filter} from "@welshman/util"
import type {Feed} from "@welshman/feeds"
import {createScroller} from "src/util/misc"
@ -59,7 +58,7 @@
if (feedLoader.compiler.canCompile(opts.feed)) {
const requests = await feedLoader.compiler.compile(opts.feed)
filters = requests.flatMap(r => r.filters)
filters = requests.flatMap(r => r.filters || [])
} else {
filters = [{ids: []}]
}

View File

@ -1,30 +1,41 @@
<script lang="ts">
import {omit} from "ramda"
import {quantify, pluralize, displayList} from "hurdak"
import {isNil, clamp} from "@welshman/lib"
import type {DynamicFilter, Feed} from "@welshman/feeds"
import {FeedType, Scope, getSubFeeds} from "@welshman/feeds"
import {Tags} from "@welshman/util"
import {
FeedType,
isScopeFeed,
isSearchFeed,
isAuthorFeed,
isCreatedAtFeed,
makeSearchFeed,
makeScopeFeed,
makeIntersectionFeed,
Scope,
hasSubFeeds,
getFeedArgs,
feedsFromTags,
} from "@welshman/feeds"
import {slide} from "src/util/transition"
import {formatTimestampAsDate, getStringWidth} from "src/util/misc"
import Card from "src/partials/Card.svelte"
import Popover from "src/partials/Popover.svelte"
import Subheading from "src/partials/Subheading.svelte"
import Popover2 from "src/partials/Popover2.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Menu from "src/partials/Menu.svelte"
import MenuItem from "src/partials/MenuItem.svelte"
import Chip from "src/partials/Chip.svelte"
import Toggle from "src/partials/Toggle.svelte"
import Modal from "src/partials/Modal.svelte"
import FeedForm from "src/app/shared/FeedForm.svelte"
import {feedLoader, displayRelayUrl, displayPubkey} from "src/engine"
import {router} from "src/app/util"
import {displayRelayUrl, displayPubkey, displayList as displayList2, userLists} from "src/engine"
export let value
const openModal = () => {
isOpen = true
const openListMenu = () => {
listMenuIsOpen = true
}
const closeModal = () => {
isOpen = false
const closeListMenu = () => {
listMenuIsOpen = false
}
const showSearch = () => {
@ -40,12 +51,22 @@
value = {...value, shouldHideReplies: !value.shouldHideReplies}
}
const setPart = (filter: DynamicFilter) => {
saveFeed([feed[0], {...feed[1], ...filter}] as Feed)
const getSearch = feed => getFeedArgs(feed)?.find(isSearchFeed)?.[1] as string | null
const saveFeed = feed => {
value = {...value, feed}
search = getSearch(feed)
closeListMenu()
}
const removeParts = (keys: string[]) => {
saveFeed([feed[0], omit(keys, feed[1])] as Feed)
const setFeed = thisFeed => {
const idx = feed.findIndex(f => f[0] === thisFeed[0])
saveFeed(idx >= 0 ? feed.toSpliced(idx, 1, thisFeed) : [...feed, ...thisFeed])
}
const removeFeed = thisFeed => {
saveFeed(feed.filter(f => f !== thisFeed))
}
const onSearchFocus = () => {
@ -66,31 +87,26 @@
}
if (text) {
setPart({search: text})
setFeed(makeSearchFeed(text))
} else {
removeParts(["search"])
removeFeed(subFeeds.find(isSearchFeed))
}
}
const saveFeed = f => {
feed = f
value = {...value, feed}
search = feed[1]?.search
closeModal()
}
const loadList = list => saveFeed(makeIntersectionFeed(...feedsFromTags(Tags.fromEvent(list))))
const displayPeople = pubkeys =>
pubkeys.length === 1 ? displayPubkey(pubkeys[0]) : `${pubkeys.length} people`
const displayTopics = topics => (topics.length === 1 ? topics[0] : `${topics.length} topics`)
let isOpen = false
let search = value.feed[1]?.search
let listMenuIsOpen = false
let searchFocused = false
let search = getSearch(value.feed)
$: feed = value.feed
$: feedType = feed[0]
$: subFeeds = getSubFeeds(feed)
$: feed = hasSubFeeds(value.feed) ? value.feed : [FeedType.Intersection, value.feed]
$: subFeeds = getFeedArgs(feed)
$: currentScopeFeed = subFeeds.find(f => isScopeFeed(f) || isAuthorFeed(f))
</script>
<div class="-mb-2">
@ -99,103 +115,105 @@
<Toggle scale={0.6} value={!value.shouldHideReplies} on:change={toggleReplies} />
<small class="text-neutral-200">Show replies</small>
</div>
<i class="fa fa-sliders cursor-pointer p-2" on:click={openModal} />
<slot name="controls" />
<div class="relative lg:hidden">
<i class="fa fa-sliders cursor-pointer p-2" on:click={openListMenu} />
{#if listMenuIsOpen}
<Popover2 absolute hideOnClick onClose={closeListMenu} class="right-0 top-8 w-60">
<Menu>
<MenuItem inert class="flex items-center justify-between bg-neutral-800 shadow">
<span class="staatliches text-lg">Your Lists & Feeds</span>
<Anchor modal href={router.at("feeds").toString()}>
<i class="fa fa-cog" />
</Anchor>
</MenuItem>
<div class="max-h-96 overflow-auto">
{#each $userLists as list}
<MenuItem on:click={() => loadList(list)}>{displayList2(list)}</MenuItem>
{/each}
</div>
</Menu>
</Popover2>
{/if}
</div>
</div>
<div class="mb-2 mr-2 inline-block py-1">Showing notes:</div>
{#if subFeeds.length > 0}
{#if feed[0] !== FeedType.Intersection}
<Chip class="mb-2 mr-2 inline-block">
Custom feed ({quantify(subFeeds.length, "selection")})
</Chip>
{/if}
{#if feedType === FeedType.Relay}
<Chip class="mb-2 mr-2 inline-block">
On {feed[1].length === 1 ? displayRelayUrl(feed[1][0]) : `${feed[1].length} relays`}
</Chip>
{:else if feedType === FeedType.List}
<Chip class="mb-2 mr-2 inline-block">
From {quantify(feed.slice(1).length, "list")}
</Chip>
{:else if feedType === FeedType.DVM}
<Chip class="mb-2 mr-2 inline-block">
From {quantify(feed.slice(1).length, "DVM")}
</Chip>
{:else if feedType === FeedType.Filter}
{#if feed.length > 2}
<Chip class="mb-2 mr-2 inline-block">
From {quantify(feed.slice(1).length, "filter")}
</Chip>
{:else}
{#await feedLoader.compiler.compile(feed)}
<!-- pass -->
{:then [{ filters: [filter] }]}
<Popover
class="inline-block"
placement="bottom-end"
theme="transparent"
opts={{hideOnClick: true}}>
<div slot="trigger" class="cursor-pointer">
<Chip class="mb-2 mr-2 inline-block">
{#if feed[1].scopes}
From {displayList(feed[1].scopes)}
{:else if filter.authors}
From {quantify(filter.authors.length, "author")}
{:else}
From global
{:else}
<Popover
class="inline-block"
placement="bottom-end"
theme="transparent"
opts={{hideOnClick: true}}>
<div slot="trigger" class="cursor-pointer">
<Chip class="mb-2 mr-2 inline-block">
{#if currentScopeFeed && isScopeFeed(currentScopeFeed)}
From {displayList(getFeedArgs(currentScopeFeed))}
{:else if currentScopeFeed && isAuthorFeed(currentScopeFeed)}
From {quantify(getFeedArgs(currentScopeFeed).length, "author")}
{:else}
From global
{/if}
<i class="fa fa-caret-down p-1" />
</Chip>
</div>
<div slot="tooltip">
<Menu>
<MenuItem on:click={() => setFeed(makeScopeFeed(Scope.Follows))}>
<i class="fa fa-user-plus mr-2" /> Follows
</MenuItem>
<MenuItem on:click={() => setFeed(makeScopeFeed(Scope.Network))}>
<i class="fa fa-share-nodes mr-2" /> Network
</MenuItem>
<MenuItem on:click={() => removeFeed(currentScopeFeed)}>
<i class="fa fa-earth-americas mr-2" /> Global
</MenuItem>
</Menu>
</div>
</Popover>
{#each subFeeds as subFeed}
{@const feedType = subFeed[0]}
{#if ![FeedType.Search, FeedType.Scope, FeedType.Author].includes(feedType)}
<Chip class="mb-2 mr-2 inline-block" onRemove={() => removeFeed(subFeed)}>
{#if feedType === FeedType.Relay}
On {subFeed.length === 2 ? displayRelayUrl(subFeed[1]) : `${subFeed.length - 1} relays`}
{:else if feedType === FeedType.List}
From {quantify(getFeedArgs(subFeed).length, "list")}
{:else if feedType === FeedType.Address || feedType === FeedType.ID}
{quantify(getFeedArgs(subFeed).length, "event")}
{:else if feedType === FeedType.DVM}
From {quantify(getFeedArgs(subFeed).length, "DVM")}
{:else if feedType === FeedType.Kind}
{@const kinds = getFeedArgs(subFeed)}
{pluralize(kinds.length, "Kind")}
{displayList(kinds)}
{:else if feedType === FeedType.Tag}
{@const [key, values] = getFeedArgs(subFeed)}
{#if key === "#p"}
Mentioning {displayPeople(values)}
{:else if key === "#t"}
Related to {displayTopics(values)}
{:else if key === "#e" || key === "#a"}
Tagging {pluralize(values.length, "event")}
{:else}
{pluralize(values.length, "other tag")}
{/if}
{:else if isCreatedAtFeed(subFeed)}
{#each getFeedArgs(subFeed) as { since, until, relative }}
{#if since && until}
Between {formatTimestampAsDate(since)} and {formatTimestampAsDate(until)}
{:else if since}
From {formatTimestampAsDate(since)}
{:else if until}
Through {formatTimestampAsDate(until)}
{/if}
<i class="fa fa-caret-down p-1" />
</Chip>
</div>
<div slot="tooltip">
<Menu>
<MenuItem on:click={() => setPart({scopes: [Scope.Follows]})}>
<i class="fa fa-user-plus mr-2" /> Follows
</MenuItem>
<MenuItem on:click={() => setPart({scopes: [Scope.Network]})}>
<i class="fa fa-share-nodes mr-2" /> Network
</MenuItem>
<MenuItem on:click={() => removeParts(["scopes"])}>
<i class="fa fa-earth-americas mr-2" /> Global
</MenuItem>
<MenuItem on:click={openModal}>
<i class="fa fa-cog mr-2" /> Custom
</MenuItem>
</Menu>
</div>
</Popover>
{#if filter.kinds?.length > 0}
<Chip class="mb-2 mr-2 inline-block" onRemove={() => removeParts(["kinds"])}>
{pluralize(filter.kinds.length, "Kind")}
{displayList(filter.kinds)}
</Chip>
{/if}
{#if filter["#p"]?.length > 0}
<Chip class="mb-2 mr-2 inline-block" onRemove={() => removeParts(["#p"])}>
Mentioning {displayPeople(filter["#p"])}
</Chip>
{/if}
{#if filter["#t"]?.length > 0}
<Chip class="mb-2 mr-2 inline-block" onRemove={() => removeParts(["#t"])}>
Related to {displayTopics(filter["#t"])}
</Chip>
{/if}
{#if filter.since && filter.until}
{@const since = formatTimestampAsDate(filter.since)}
{@const until = formatTimestampAsDate(filter.until)}
<Chip class="mb-2 mr-2 inline-block" onRemove={() => removeParts(["since", "until"])}>
Between {since} and {until}
</Chip>
{:else if filter.since}
<Chip class="mb-2 mr-2 inline-block" onRemove={() => removeParts(["since"])}>
From {formatTimestampAsDate(filter.since)}
</Chip>
{:else if filter.until}
<Chip class="mb-2 mr-2 inline-block" onRemove={() => removeParts(["until"])}>
Through {formatTimestampAsDate(filter.until)}
</Chip>
{/if}
{/await}
{/if}
{/each}
{/if}
</Chip>
{/if}
{/each}
<Chip class="cursor-pointer" on:click={showSearch}>
<div class="flex h-6 items-center justify-center">
<i class="fa fa-search" />
@ -216,10 +234,3 @@
</Chip>
{/if}
</div>
{#if true || isOpen}
<Modal onEscape={closeModal}>
<Subheading class="ml-6">Create a custom Feed</Subheading>
<FeedForm {feed} onCancel={closeModal} onChange={saveFeed} />
</Modal>
{/if}

View File

@ -1,222 +1,121 @@
<script lang="ts">
import {assocPath} from "ramda"
import {quantify, switcherFn, updatePath} from "hurdak"
import {inc} from "@welshman/lib"
import {FeedType, hasSubFeeds, getSubFeeds} from "@welshman/feeds"
import {FeedType, makeIntersectionFeed, hasSubFeeds, getFeedArgs} from "@welshman/feeds"
import Icon from "src/partials/Icon.svelte"
import SelectTiles from "src/partials/SelectTiles.svelte"
import Card from "src/partials/Card.svelte"
import Popover from "src/partials/Popover.svelte"
import Menu from "src/partials/Menu.svelte"
import MenuItem from "src/partials/MenuItem.svelte"
import Anchor from "src/partials/Anchor.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import Field from "src/partials/Field.svelte"
import Select from "src/partials/Select.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import FilterField from "src/app/shared/FilterField.svelte"
import DVMField from "src/app/shared/DVMField.svelte"
import FeedFormRelay from "src/app/shared/FeedFormRelay.svelte"
import {searchRelayUrls, searchListAddrs, displayListByAddress, displayRelayUrl} from "src/engine"
import FeedFormPeople from "src/app/shared/FeedFormPeople.svelte"
import FeedFormTopics from "src/app/shared/FeedFormTopics.svelte"
import FeedFormRelays from "src/app/shared/FeedFormRelays.svelte"
import FeedFormDVMs from "src/app/shared/FeedFormDVMs.svelte"
import FeedFormAdvanced from "src/app/shared/FeedFormAdvanced.svelte"
export let feed
export let onChange
export let onCancel
const controller = {
pushCursor: i => {
cursor = [...cursor, i]
},
popCursor: i => {
cursor = cursor.slice(0, -1)
},
setAtCursor: (v, p = []) => {
feed = assocPath(cursor.concat(p), v, feed)
},
updateAtCursor: (f, p = []) => {
feed = updatePath(cursor.concat(p), f, feed)
},
addFeed: feedType => controller.setAtCursor([...current, [feedType]]),
removeFeed: i => controller.setAtCursor(current.toSpliced(i, 1)),
enum FormType {
Advanced = "advanced",
DVMs = "dvms",
People = "people",
Relays = "relays",
Topics = "topics",
}
const onTypeChange = type => {
if (hasSubFeeds([type])) {
if (hasSubFeeds(current)) {
controller.setAtCursor([type, ...current.slice(1)])
} else {
controller.setAtCursor([type, current])
const normalize = feed => (isNormalized(feed) ? feed : [FeedType.Intersection, feed])
const isNormalized = feed =>
feed[0] === FeedType.Intersection && getFeedArgs(feed).every(f => !hasSubFeeds(f))
const inferFormType = feed => {
for (const subFeed of getFeedArgs(normalize(feed))) {
if ([FeedType.Scope, FeedType.Author].includes(subFeed[0])) {
return FormType.People
}
if (subFeed[0] === FeedType.Tag && subFeed[1] === "#t") {
return FormType.Topics
}
if (subFeed[0] === FeedType.Relay) {
return FormType.Relays
}
if (subFeed[0] === FeedType.DVM) {
return FormType.DVMs
}
} else if (type === FeedType.Filter) {
controller.setAtCursor([type, {}])
} else if (type === FeedType.DVM) {
controller.setAtCursor([type, {kind: 5300, tags: [], relays: []}])
} else {
controller.setAtCursor([type])
}
return FormType.Advanced
}
const displayFeed = ([type, ...feed]) =>
switcherFn(type, {
[FeedType.Filter]: () => quantify(feed.length, "filter"),
[FeedType.List]: () => quantify(feed.length, "list"),
[FeedType.DVM]: () => quantify(feed.length, "DVM"),
[FeedType.Relay]: () =>
quantify(feed.slice(1).length, "feed") + " on " + quantify(feed[0].length, "relays"),
[FeedType.Union]: () => "union of " + quantify(feed.length, "feed"),
[FeedType.Intersection]: () => "union of " + quantify(feed.length, "feed"),
[FeedType.Difference]: () => "union of " + quantify(feed.length, "feed"),
[FeedType.SymmetricDifference]: () => "union of " + quantify(feed.length, "feed"),
})
const onFormTypeChange = newFormType => {
if (formType === newFormType) {
return
}
let cursor = []
// If we can't deal with the feed, clear it out
if (!isNormalized(feed)) {
feed = makeIntersectionFeed()
}
$: console.log(JSON.stringify(feed, null, 2))
$: current = cursor.reduce((f, i) => f[i], feed)
$: subFeeds = getSubFeeds(current)
$: feedType = current[0]
formType = newFormType
}
const onFeedChange = newFeed => {
feed = newFeed
}
let formType = inferFormType(feed)
$: console.log(JSON.stringify(normalize(feed), null, 2))
</script>
<FlexColumn class="pb-32">
<Card>
<Field label="Choose a feed type">
<SelectTiles
options={[FeedType.Filter, FeedType.Relay, FeedType.DVM, "advanced"]}
onChange={onTypeChange}
value={feedType}>
options={[FormType.People, FormType.Topics, FormType.Relays, FormType.DVMs]}
onChange={onFormTypeChange}
value={formType}>
<div slot="item" class="flex flex-col items-center" let:option let:active>
{#if option === FeedType.Filter}
{#if option === FormType.People}
<Icon icon="people-nearby" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
<span class="staatliches text-2xl">Standard</span>
{:else if option === FeedType.Relay}
<span class="staatliches text-2xl">People</span>
{:else if option === FormType.Topics}
<span class="flex h-12 w-12 items-center justify-center" class:text-accent={active}>
<i class="fa fa-2xl fa-tags" />
</span>
<span class="staatliches text-2xl">Topics</span>
{:else if option === FormType.Relays}
<Icon icon="server" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
<span class="staatliches text-2xl">Relays</span>
{:else if option === FeedType.DVM}
{:else if option === FormType.DVMs}
<Icon icon="network" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
<span class="staatliches text-2xl">DVMs</span>
{:else}
<span class="flex h-12 w-12 items-center justify-center">
<i class="fa fa-2xl fa-gears" />
</span>
<span class="staatliches text-2xl">Advanced</span>
{/if}
</div>
</SelectTiles>
</Field>
<div class="flex justify-end">
<Anchor underline on:click={() => onFormTypeChange(FormType.Advanced)}>Advanced mode</Anchor>
</div>
</Card>
{#if feedType === FeedType.Relay}
<FeedFormRelay feed={current} {controller} />
<Field label="Which relays would you like to use?">
<SearchSelect
multiple
value={current[1] || []}
search={$searchRelayUrls}
onChange={urls => controller.setAtCursor(urls, [1])}>
<span slot="item" let:item>{displayRelayUrl(item)}</span>
</SearchSelect>
<p slot="info">Select which relays you'd like to limit loading feeds from.</p>
</Field>
{:else if feedType === FeedType.Filter}
<FeedFormForRelayFeed feed={current} {controller} />
{#each current.slice(1) as filter, filterIdx ([current.length, filterIdx].join(":"))}
{@const feedIdx = inc(filterIdx)}
<Card>
<FilterField
{filter}
onChange={filter => controller.setAtCursor(filter, [feedIdx])}
onRemove={() => controller.updateAtCursor(feed => feed.toSpliced(feedIdx, 1))} />
</Card>
{#if feedIdx < current.length - 1}
<p class="staatliches text-center">— OR —</p>
{/if}
{/each}
<div class="flex">
<Anchor button on:click={() => controller.setAtCursor([...current, {}])}>
<i class="fa fa-plus" /> Add filter
</Anchor>
</div>
{:else if feedType === FeedType.List}
<Field label="List Selections">
<SearchSelect
multiple
value={current.slice(1)}
search={$searchListAddrs}
onChange={addrs => controller.setAtCursor([FeedType.List, ...addrs])}>
<span slot="item" let:item>{displayListByAddress(item)}</span>
</SearchSelect>
<p slot="info">Select which lists you'd like to view.</p>
</Field>
{:else if feedType === FeedType.DVM}
{#each current.slice(1) as item, itemIdx ([current.length, itemIdx].join(":"))}
{@const feedIdx = inc(itemIdx)}
<Card>
<DVMField
dvmItem={item}
onChange={item => controller.setAtCursor(item, [feedIdx])}
onRemove={() => controller.updateAtCursor(feed => feed.toSpliced(feedIdx, 1))} />
</Card>
{#if feedIdx < current.length - 1}
<p class="staatliches text-center">— OR —</p>
{/if}
{/each}
<div class="flex">
<Anchor
button
on:click={() => controller.setAtCursor([...current, {kind: 5300, tags: [], relays: []}])}>
<i class="fa fa-plus" /> Add DVM
</Anchor>
</div>
{#if formType === FormType.People}
<FeedFormPeople feed={normalize(feed)} onChange={onFeedChange} />
{:else if formType === FormType.Topics}
<FeedFormTopics feed={normalize(feed)} onChange={onFeedChange} />
{:else if formType === FormType.Relays}
<FeedFormRelays feed={normalize(feed)} onChange={onFeedChange} />
{:else if formType === FormType.DVMs}
<FeedFormDVMs feed={normalize(feed)} onChange={onFeedChange} />
{:else if formType === FormType.Advanced}
<FeedFormAdvanced {feed} onChange={onFeedChange} />
{/if}
{#each subFeeds as subFeed, i (displayFeed(subFeed) + i)}
<Card class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Anchor on:click={() => controller.removeFeed(current.indexOf(subFeed))}>
<i class="fa fa-trash fa-sm" />
</Anchor>
<span class="text-lg">{displayFeed(subFeed)}</span>
</div>
<Anchor
class="flex items-center gap-2"
on:click={() => controller.pushCursor(current.indexOf(subFeed))}>
<i class="fa fa-edit" /> Edit
</Anchor>
</Card>
{#if i < subFeeds.length - 1}
<p class="staatliches text-center">
{#if feedType === FeedType.Union}
— OR —
{:else if feedType === FeedType.Intersection}
— AND —
{:else if feedType === FeedType.Difference}
— WITHOUT —
{:else if feedType === FeedType.SymmetricDifference}
— XOR —
{/if}
</p>
{/if}
{/each}
{#if hasSubFeeds(current)}
<div class="inline-block">
<Popover theme="transparent" opts={{hideOnClick: true}} class="inline-block">
<div slot="trigger">
<Anchor button><i class="fa fa-plus" /> Add Feed</Anchor>
</div>
<div slot="tooltip">
<Menu>
<MenuItem on:click={() => controller.addFeed(FeedType.Filter)}>Standard Feed</MenuItem>
<MenuItem on:click={() => controller.addFeed(FeedType.List)}>List Feed</MenuItem>
<MenuItem on:click={() => controller.addFeed(FeedType.DVM)}>DVM Feed</MenuItem>
</Menu>
</div>
</Popover>
</div>
{/if}
<div class="flex items-center justify-end gap-3">
{#if current === feed}
<Anchor button on:click={onCancel}>Cancel</Anchor>
<Anchor button accent on:click={() => onChange(feed)}>Save</Anchor>
{:else}
<Anchor button on:click={controller.popCursor}>Done</Anchor>
{/if}
<div class="flex items-center justify-between gap-3">
<Anchor button on:click={onCancel}>Discard</Anchor>
<Anchor button accent on:click={() => onChange(feed)}>Save feed</Anchor>
</div>
</FlexColumn>

View File

@ -0,0 +1,49 @@
<script lang="ts">
import {tryCatch} from "@welshman/lib"
import Field from "src/partials/Field.svelte"
import Textarea from "src/partials/Textarea.svelte"
export let feed
export let onChange
const onInput = async e => {
const newFeed = await tryCatch(() => JSON.parse(e.target.value))
if (newFeed) {
onChange(newFeed)
}
isValid = Boolean(newFeed)
}
const onFocus = () => {
isFocused = true
}
const onBlur = () => {
if (isValid) {
json = JSON.stringify(feed, null, 2)
}
isFocused = false
}
let isValid = true
let isFocused = false
let json = JSON.stringify(feed, null, 2)
</script>
<Field label="Enter your custom feed below">
<Textarea
class="h-72 whitespace-pre-wrap"
value={json}
on:input={onInput}
on:focus={onFocus}
on:blur={onBlur} />
</Field>
{#if !isValid && !isFocused}
<p>
<i class="fa fa-triangle-exclamation" />
Your feed is currently invalid. Please double check that it is valid JSON.
</p>
{/if}

View File

@ -0,0 +1,16 @@
<script lang="ts">
import {flatten, partition} from "@welshman/lib"
import {FeedType, getFeedArgs, isDVMFeed} from "@welshman/feeds"
import FeedFormFilters from "src/app/shared/FeedFormFilters.svelte"
export let feed
export let onChange
if (!getFeedArgs(feed).some(isDVMFeed)) {
onChange([...feed, [FeedType.DVM, {kind: 5300}]])
}
$: sorted = [feed[0], ...flatten(partition(isDVMFeed, getFeedArgs(feed)))]
</script>
<FeedFormFilters feed={sorted} {onChange} />

View File

@ -0,0 +1,135 @@
<script lang="ts">
import {toTitle} from "hurdak"
import {
getFeedArgs,
isCreatedAtFeed,
isAuthorFeed,
isKindFeed,
isDVMFeed,
isTagFeed,
isScopeFeed,
isRelayFeed,
makeTagFeed,
makeAuthorFeed,
makeRelayFeed,
makeKindFeed,
makeCreatedAtFeed,
makeDVMFeed,
} from "@welshman/feeds"
import Card from "src/partials/Card.svelte"
import Menu from "src/partials/Menu.svelte"
import MenuItem from "src/partials/MenuItem.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Popover2 from "src/partials/Popover2.svelte"
import FeedFormSectionPeople from "src/app/shared/FeedFormSectionPeople.svelte"
import FeedFormSectionRelays from "src/app/shared/FeedFormSectionRelays.svelte"
import FeedFormSectionTopics from "src/app/shared/FeedFormSectionTopics.svelte"
import FeedFormSectionMentions from "src/app/shared/FeedFormSectionMentions.svelte"
import FeedFormSectionKinds from "src/app/shared/FeedFormSectionKinds.svelte"
import FeedFormSectionCreatedAt from "src/app/shared/FeedFormSectionCreatedAt.svelte"
import FeedFormSectionDVM from "src/app/shared/FeedFormSectionDVM.svelte"
export let feed
export let onChange
const addFeed = newFeed => onChange([...feed, newFeed])
const onSubFeedChange = (i, newFeed) => onChange(feed.toSpliced(i, 1, newFeed))
const onSubFeedRemove = i => onChange(feed.toSpliced(i, 1))
const isTopicFeed = f => isTagFeed(f) && f[1] === "#t"
const isMentionFeed = f => isTagFeed(f) && f[1] === "#p"
const isPeopleFeed = f => isAuthorFeed(f) || isScopeFeed(f)
const openMenu = () => {
menuIsOpen = true
}
const closeMenu = () => {
menuIsOpen = false
}
let menuIsOpen = false
$: subFeeds = getFeedArgs(feed)
$: hasTopics = subFeeds.some(isTopicFeed)
$: hasMentions = subFeeds.some(isMentionFeed)
$: hasPeople = subFeeds.some(isPeopleFeed)
$: hasRelays = subFeeds.some(isRelayFeed)
$: hasKinds = subFeeds.some(isKindFeed)
$: hasCreatedAt = subFeeds.some(isCreatedAtFeed)
$: hasDVM = subFeeds.some(isDVMFeed)
</script>
{#key feed.length}
{#each subFeeds as subFeed, i}
{@const change = f => onSubFeedChange(i + 1, f)}
<Card class="relative">
{#if isPeopleFeed(subFeed)}
<FeedFormSectionPeople feed={subFeed} onChange={change} />
{:else if isRelayFeed(subFeed)}
<FeedFormSectionRelays feed={subFeed} onChange={change} />
{:else if isTopicFeed(subFeed)}
<FeedFormSectionTopics feed={subFeed} onChange={change} />
{:else if isMentionFeed(subFeed)}
<FeedFormSectionMentions feed={subFeed} onChange={change} />
{:else if isKindFeed(subFeed)}
<FeedFormSectionKinds feed={subFeed} onChange={change} />
{:else if isCreatedAtFeed(subFeed)}
<FeedFormSectionCreatedAt feed={subFeed} onChange={change} />
{:else if isDVMFeed(subFeed)}
<FeedFormSectionDVM feed={subFeed} onChange={change} />
{:else}
No support for editing {toTitle(subFeed[0])} filters. Click "Advanced" to edit manually.
{/if}
{#if i > 0}
<div
class="absolute right-2 top-2 h-4 w-4 cursor-pointer"
on:click={() => onSubFeedRemove(i + 1)}>
<i class="fa fa-times" />
</div>
{/if}
</Card>
{/each}
{/key}
{#if !hasTopics || !hasMentions || !hasPeople || !hasRelays || !hasKinds || !hasCreatedAt || !hasDVM}
<div>
{#if menuIsOpen}
<Popover2 hideOnClick onClose={closeMenu} position="top">
<Menu class="relative top-2 m-auto w-48">
{#if !hasTopics}
<MenuItem on:click={() => addFeed(makeTagFeed("#t"))}>Topics</MenuItem>
{/if}
{#if !hasMentions}
<MenuItem on:click={() => addFeed(makeTagFeed("#p"))}>Mentions</MenuItem>
{/if}
{#if !hasPeople}
<MenuItem on:click={() => addFeed(makeAuthorFeed())}>Authors</MenuItem>
{/if}
{#if !hasRelays}
<MenuItem on:click={() => addFeed(makeRelayFeed())}>Relays</MenuItem>
{/if}
{#if !hasKinds}
<MenuItem on:click={() => addFeed(makeKindFeed())}>Kinds</MenuItem>
{/if}
{#if !hasCreatedAt}
<MenuItem on:click={() => addFeed(makeCreatedAtFeed())}>Date range</MenuItem>
{/if}
{#if !hasDVM}
<MenuItem on:click={() => addFeed(makeDVMFeed({kind: 5300}))}
>Data vending machine</MenuItem>
{/if}
</Menu>
</Popover2>
{/if}
<Anchor
class="flex items-center justify-center rounded-lg border border-dashed border-neutral-500 p-4 text-neutral-300"
on:click={openMenu}>
<span class="staatliches underline">Add a filter</span>
</Anchor>
</div>
{/if}

View File

@ -0,0 +1,18 @@
<script lang="ts">
import {partition, identity} from "ramda"
import {FeedType, Scope, getFeedArgs, isAuthorFeed, isScopeFeed} from "@welshman/feeds"
import FeedFormFilters from "src/app/shared/FeedFormFilters.svelte"
export let feed
export let onChange
const isPeopleFeed = f => isAuthorFeed(f) || isScopeFeed(f)
if (!getFeedArgs(feed).some(isPeopleFeed)) {
onChange([...feed, [FeedType.Scope, Scope.Follows]])
}
$: sorted = [feed[0], ...partition(isPeopleFeed, feed.slice(1)).flatMap(identity)]
</script>
<FeedFormFilters feed={sorted} {onChange} />

View File

@ -1,20 +0,0 @@
<script lang="ts">
import {FeedType} from "@welshman/feeds"
import Field from "src/partials/Field.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import {searchRelayUrls, displayRelayUrl} from "src/engine"
export let feed
export let controller
</script>
<Field label="Which relays would you like to use?">
<SearchSelect
multiple
value={feed.slice(1)}
search={$searchRelayUrls}
onChange={urls => controller.setAtCursor([FeedType.Relay, ...urls])}>
<span slot="item" let:item>{displayRelayUrl(item)}</span>
</SearchSelect>
<p slot="info">Select which relays you'd like to limit loading feeds from.</p>
</Field>

View File

@ -0,0 +1,17 @@
<script lang="ts">
import {flatten, partition} from "@welshman/lib"
import {makeRelayFeed, getFeedArgs, isRelayFeed} from "@welshman/feeds"
import type {IntersectionFeed} from "@welshman/feeds"
import FeedFormFilters from "src/app/shared/FeedFormFilters.svelte"
export let feed: IntersectionFeed
export let onChange
if (!getFeedArgs(feed).some(isRelayFeed)) {
onChange([...feed, makeRelayFeed()])
}
$: sorted = [feed[0], ...flatten(partition(isRelayFeed, getFeedArgs(feed)))]
</script>
<FeedFormFilters feed={sorted} {onChange} />

View File

@ -0,0 +1,39 @@
<script lang="ts">
import {omit} from "ramda"
import {FeedType} from "@welshman/feeds"
import {createLocalDate, dateToSeconds, formatTimestampAsDate} from "src/util/misc"
import Field from "src/partials/Field.svelte"
import DateInput from "src/partials/DateInput.svelte"
export let feed
export let onChange
const changeSince = since => {
const value = since
? {...feed[1], since: dateToSeconds(createLocalDate(since).setHours(0, 0, 0, 0))}
: omit(["since"], feed[1])
onChange([FeedType.CreatedAt, value])
}
const changeUntil = until => {
const value = until
? {...feed[1], until: dateToSeconds(createLocalDate(until).setHours(23, 59, 59, 0))}
: omit(["until"], feed[1])
onChange([FeedType.CreatedAt, value])
}
</script>
<Field label="What time range would you like to consider?">
<div class="grid grid-cols-2 gap-2">
<DateInput
placeholder="Since"
value={feed[1]?.since ? formatTimestampAsDate(feed[1]?.since) : null}
onChange={changeSince} />
<DateInput
placeholder="Until"
value={feed[1]?.until ? formatTimestampAsDate(feed[1]?.until) : null}
onChange={changeUntil} />
</div>
</Field>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import {getFeedArgs} from "@welshman/feeds"
import Field from "src/partials/Field.svelte"
import DVMField from "src/app/shared/DVMField.svelte"
export let feed
export let onChange
</script>
<Field label="What options should be sent to the DVM?">
{#each getFeedArgs(feed) as dvmItem, i}
<DVMField {dvmItem} onChange={it => onChange(feed.toSpliced(i + 1, 1, it))} />
{/each}
</Field>

View File

@ -1,14 +1,14 @@
<script lang="ts">
import {pluck, identity} from "ramda"
import {pluck} from "ramda"
import {FeedType} from "@welshman/feeds"
import {fuzzy} from "src/util/misc"
import Field from "src/partials/Field.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
export let filter
export let feed
export let onChange
export let onRemove
const change = kinds => onChange({...filter, kinds})
const onKindsChange = kinds => onChange([FeedType.Kind, ...kinds])
const kinds = [
{label: "Note", kind: 1},
@ -38,7 +38,7 @@
const searchKindItems = fuzzy(kinds, {keys: ["kind", "label"]})
const searchKinds = term => pluck("kind", searchKindItems(term))
const searchKinds = term => pluck("kind", searchKindItems(term)).filter(k => !feed.includes(k))
const displayKind = kind => {
const option = kinds.find(k => k.kind === kind)
@ -47,11 +47,8 @@
}
</script>
<Field>
<span slot="label" class="flex cursor-pointer items-center gap-2" on:click={onRemove}>
<i class="fa fa-trash fa-sm" /> Kinds
</span>
<SearchSelect multiple search={searchKinds} value={filter.kinds || []} onChange={change} termToItem={identity}>
<Field label="What kind of content do you want to see?">
<SearchSelect multiple search={searchKinds} value={feed.slice(1)} onChange={onKindsChange}>
<div slot="item" let:item>{displayKind(item)}</div>
</SearchSelect>
</Field>

View File

@ -0,0 +1,30 @@
<script lang="ts">
import {FeedType} from "@welshman/feeds"
import Field from "src/partials/Field.svelte"
import Anchor from "src/partials/Anchor.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {searchPubkeys, displayPubkey} from "src/engine"
import {router} from "src/app/util/router"
export let feed
export let onChange
</script>
<Field label="Which mentions would you like to see?">
<SearchSelect
multiple
value={feed.slice(2)}
search={$searchPubkeys}
onChange={pubkeys => onChange([FeedType.Tag, "#p", ...pubkeys])}>
<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>
</Field>

View File

@ -0,0 +1,53 @@
<script lang="ts">
import {toTitle} from "hurdak"
import {without} from "ramda"
import {FeedType, Scope, isScopeFeed, isAuthorFeed} from "@welshman/feeds"
import Field from "src/partials/Field.svelte"
import Anchor from "src/partials/Anchor.svelte"
import SelectButton from "src/partials/SelectButton.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {searchPubkeys, displayPubkey} from "src/engine"
import {router} from "src/app/util/router"
export let feed
export let onChange
const scopeOptions = (Object.values(Scope) as string[]).concat("custom")
const onScopeChange = scopes => {
if (isScopeFeed(feed) && scopes.includes("custom")) {
onChange([FeedType.Author])
} else {
onChange([FeedType.Scope, ...without(["custom"], scopes)])
}
}
$: scopes = isScopeFeed(feed) ? feed.slice(1) : ["custom"]
</script>
<Field label="Which authors would you like to see?">
<SelectButton
multiple
value={scopes}
displayOption={toTitle}
options={scopeOptions}
onChange={onScopeChange} />
{#if isAuthorFeed(feed)}
<SearchSelect
multiple
value={feed.slice(1)}
search={$searchPubkeys}
onChange={pubkeys => onChange([FeedType.Author, ...pubkeys])}>
<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>
{/if}
</Field>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import {FeedType} from '@welshman/feeds'
import Field from 'src/partials/Field.svelte'
import SearchSelect from 'src/partials/SearchSelect.svelte'
import {searchRelayUrls, displayRelayUrl} from 'src/engine'
export let feed
export let onChange
</script>
<Field label="Which relays would you like to use?">
<SearchSelect
multiple
value={feed.slice(1)}
search={$searchRelayUrls}
onChange={urls => onChange([FeedType.Relay, ...urls])}>
<span slot="item" let:item>{displayRelayUrl(item)}</span>
</SearchSelect>
</Field>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import {FeedType} from '@welshman/feeds'
import Field from 'src/partials/Field.svelte'
import SearchSelect from 'src/partials/SearchSelect.svelte'
import {searchTopicNames} from 'src/engine'
export let feed
export let onChange
</script>
<Field label="Which topics do you want to see?">
<SearchSelect
multiple
value={feed.slice(2)}
search={$searchTopicNames}
onChange={topics => onChange([FeedType.Tag, "#t", ...topics])}>
<span slot="item" let:item>#{item}</span>
</SearchSelect>
</Field>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import {partition, identity} from "ramda"
import {FeedType, getFeedArgs, isTagFeed} from "@welshman/feeds"
import FeedFormFilters from "src/app/shared/FeedFormFilters.svelte"
export let feed
export let onChange
const isTopicFeed = f => isTagFeed(f) && f[1] === "#t"
if (!getFeedArgs(feed).some(isTopicFeed)) {
onChange([...feed, [FeedType.Tag, "#t"]])
}
$: sorted = [feed[0], ...partition(isTopicFeed, feed.slice(1)).flatMap(identity)]
</script>
<FeedFormFilters feed={sorted} {onChange} />

View File

@ -1,43 +0,0 @@
<script lang="ts">
import {ucFirst} from 'hurdak'
import {omit, without, last} from "ramda"
import {Scope} from '@welshman/feeds'
import Field from "src/partials/Field.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import SelectButton from "src/partials/SelectButton.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {searchPubkeys, displayPubkey} from 'src/engine'
export let filter
export let onChange
export let onRemove
const scopeOptions = (Object.values(Scope) as string[]).concat(['custom'])
const changeAuthors = authors => onChange(omit(['scopes'], {...filter, authors}))
const changeScopes = scopes =>
last(scopes) === 'custom'
? changeAuthors([])
: onChange(omit(['authors'], {...filter, scopes: without(['custom'], scopes)}))
$: scopes = filter.scopes || ['custom']
</script>
<Field>
<span slot="label" class="flex gap-2 items-center cursor-pointer" on:click={onRemove}>
<i class="fa fa-trash fa-sm" /> Authors
</span>
<SelectButton multiple onChange={changeScopes} value={scopes} options={scopeOptions} displayOption={ucFirst} />
{#if !filter.scopes}
<SearchSelect multiple search={$searchPubkeys} value={filter.authors || []} onChange={changeAuthors}>
<div slot="item" let:item let:context>
{#if context === "value"}
{displayPubkey(item)}
{:else}
<PersonBadge inert pubkey={item} />
{/if}
</div>
</SearchSelect>
{/if}
</Field>

View File

@ -1,132 +0,0 @@
<script context="module" lang="ts">
import {LRUCache} from "@welshman/lib"
import type {Filter} from "@welshman/util"
// Keep track of filter part order, even when we re-render
const sectionsByFilter = new LRUCache<Filter, string[]>(30)
</script>
<script lang="ts">
import {omit, without} from "ramda"
import Popover from "src/partials/Popover.svelte"
import Menu from "src/partials/Menu.svelte"
import MenuItem from "src/partials/MenuItem.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import FilterKindsField from "src/app/shared/FilterKindsField.svelte"
import FilterSearchField from "src/app/shared/FilterSearchField.svelte"
import FilterTopicsField from "src/app/shared/FilterTopicsField.svelte"
import FilterAuthorsField from "src/app/shared/FilterAuthorsField.svelte"
import FilterMentionsField from "src/app/shared/FilterMentionsField.svelte"
import FilterTimeframeField from "src/app/shared/FilterTimeframeField.svelte"
export let filter
export let onChange
export let onRemove
const getSections = () => {
const sections: string[] = []
if (filter.kinds) sections.push("kinds")
if (filter.search) sections.push("search")
if (filter["#t"]) sections.push("topics")
if (filter.authors || filter.scopes) sections.push("authors")
if (filter["#p"]) sections.push("mentions")
if (filter.since || filter.until || filter.since_ago || filter.until_ago) {
sections.push("timeframe")
}
return sections
}
const removeSection = section => {
sections = without([section], sections)
if (section === "kinds") {
filter = omit(["kinds"], filter)
}
if (section === "search") {
filter = omit(["search"], filter)
}
if (section === "topics") {
filter = omit(["#t"], filter)
}
if (section === "authors") {
filter = omit(["authors", "scopes"], filter)
}
if (section === "mentions") {
filter = omit(["#p"], filter)
}
if (section === "timeframe") {
filter = omit(["since", "until", "since_ago", "until_ago"], filter)
}
onChange(filter)
}
const addSection = section => {
sections = [...sections, section]
}
let sections: string[] = sectionsByFilter.get(filter) || getSections()
$: sectionsByFilter.set(filter, sections)
</script>
<FlexColumn class="relative">
{#each sections as section}
<div class="relative">
{#if section === "kinds"}
<FilterKindsField {filter} {onChange} onRemove={() => removeSection("kinds")} />
{:else if section === "search"}
<FilterSearchField {filter} {onChange} onRemove={() => removeSection("search")} />
{:else if section === "topics"}
<FilterTopicsField {filter} {onChange} onRemove={() => removeSection("topics")} />
{:else if section === "authors"}
<FilterAuthorsField {filter} {onChange} onRemove={() => removeSection("authors")} />
{:else if section === "mentions"}
<FilterMentionsField {filter} {onChange} onRemove={() => removeSection("mentions")} />
{:else if section === "timeframe"}
<FilterTimeframeField {filter} {onChange} onRemove={() => removeSection("timeframe")} />
{/if}
</div>
{/each}
<div>
<Popover theme="transparent" opts={{hideOnClick: true}} class="inline">
<span slot="trigger" class="cursor-pointer">
<i class="fa fa-plus" /> Add selection
</span>
<div slot="tooltip">
<Menu>
{#if !sections.includes("topics")}
<MenuItem on:click={() => addSection("topics")}>Topics</MenuItem>
{/if}
{#if !sections.includes("authors")}
<MenuItem on:click={() => addSection("authors")}>Authors</MenuItem>
{/if}
{#if !sections.includes("mentions")}
<MenuItem on:click={() => addSection("mentions")}>Mentions</MenuItem>
{/if}
{#if !sections.includes("timeframe")}
<MenuItem on:click={() => addSection("timeframe")}>Timeframe</MenuItem>
{/if}
{#if !sections.includes("search")}
<MenuItem on:click={() => addSection("search")}>Search</MenuItem>
{/if}
{#if !sections.includes("kinds")}
<MenuItem on:click={() => addSection("kinds")}>Kinds</MenuItem>
{/if}
</Menu>
</div>
</Popover>
</div>
<div
class="absolute -right-4 -top-2 flex h-4 w-4 cursor-pointer items-center justify-center"
on:click={onRemove}>
<i class="fa fa-times fa-lg" />
</div>
</FlexColumn>

View File

@ -1,27 +0,0 @@
<script lang="ts">
import Field from "src/partials/Field.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {searchPubkeys, displayPubkey} from 'src/engine'
export let filter
export let onChange
export let onRemove
const change = pubkeys => onChange({...filter, '#p': pubkeys})
</script>
<Field>
<span slot="label" class="flex gap-2 items-center cursor-pointer" on:click={onRemove}>
<i class="fa fa-trash fa-sm" /> Mentions
</span>
<SearchSelect multiple search={$searchPubkeys} value={filter['#p'] || []} onChange={change}>
<div slot="item" let:item let:context>
{#if context === "value"}
{displayPubkey(item)}
{:else}
<PersonBadge inert pubkey={item} />
{/if}
</div>
</SearchSelect>
</Field>

View File

@ -1,19 +0,0 @@
<script lang="ts">
import Field from 'src/partials/Field.svelte'
import Input from 'src/partials/Input.svelte'
export let filter
export let onChange
export let onRemove
const change = e => onChange({...filter, search: e.target.value})
</script>
<Field>
<span slot="label" class="flex gap-2 items-center cursor-pointer" on:click={onRemove}>
<i class="fa fa-trash fa-sm" /> Search
</span>
<Input value={filter.search} on:change={change}>
<i slot="before" class="fa fa-search" />
</Input>
</Field>

View File

@ -1,31 +0,0 @@
<script lang="ts">
import {createLocalDate, dateToSeconds, formatTimestampAsDate} from "src/util/misc"
import Field from "src/partials/Field.svelte"
import DateInput from "src/partials/DateInput.svelte"
export let filter
export let onChange
export let onRemove
const changeSince = since =>
onChange({...filter, since: dateToSeconds(createLocalDate(since).setHours(0, 0, 0, 0))})
const changeUntil = until =>
onChange({...filter, until: dateToSeconds(createLocalDate(until).setHours(23, 59, 59, 0))})
</script>
<div class="grid grid-cols-2 gap-2">
<Field>
<span slot="label" class="flex cursor-pointer items-center gap-2" on:click={onRemove}>
<i class="fa fa-trash fa-sm" /> Since
</span>
<DateInput
value={filter.since ? formatTimestampAsDate(filter.since) : null}
onChange={changeSince} />
</Field>
<Field label="Until">
<DateInput
value={filter.until ? formatTimestampAsDate(filter.until) : null}
onChange={changeUntil} />
</Field>
</div>

View File

@ -1,18 +0,0 @@
<script lang="ts">
import Field from "src/partials/Field.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import {searchTopicNames} from "src/engine"
export let filter
export let onChange
export let onRemove
const change = topics => onChange({...filter, "#t": topics})
</script>
<Field>
<span slot="label" class="flex cursor-pointer items-center gap-2" on:click={onRemove}>
<i class="fa fa-trash fa-sm" /> Topics
</span>
<SearchSelect multiple search={$searchTopicNames} value={filter["#t"] || []} onChange={change} />
</Field>

View File

@ -56,9 +56,6 @@ export class FeedLoader {
isDeleted = isDeleted.get()
constructor(readonly opts: FeedOpts) {
// @ts-ignore
window.feed = this
// Use a custom feed loader so we can intercept the filters
this.feedLoader = new CoreFeedLoader({
...baseFeedLoader.options,
@ -171,7 +168,6 @@ export class FeedLoader {
}
}
const parentIds = new Set<string>()
const notesWithParent = notes.filter(e => {
if (repostKinds.includes(e.kind)) {
return false
@ -187,17 +183,9 @@ export class FeedLoader {
return false
}
for (const id of ids) {
parentIds.add(id)
}
return true
})
if (parentIds.size === 0) {
return
}
const selections = hints.merge(notesWithParent.map(hints.EventParents)).getSelections()
for (const {relay, values} of selections) {

View File

@ -1,11 +1,17 @@
<script lang="ts">
import {Scope, feedFromFilter, intersectionFeed, kindFeed, scopeFeed} from "@welshman/feeds"
import {
Scope,
feedFromFilter,
makeIntersectionFeed,
makeKindFeed,
makeScopeFeed,
} from "@welshman/feeds"
import Calendar from "src/app/shared/Calendar.svelte"
import {env, loadGroupMessages} from "src/engine"
const feed = $env.FORCE_GROUP
? feedFromFilter({kinds: [31923], "#a": [$env.FORCE_GROUP]})
: intersectionFeed(kindFeed(31923), scopeFeed(Scope.Self, Scope.Follows))
: makeIntersectionFeed(makeKindFeed(31923), makeScopeFeed(Scope.Self, Scope.Follows))
if ($env.FORCE_GROUP) {
loadGroupMessages([$env.FORCE_GROUP])

View File

@ -1,7 +1,14 @@
<script lang="ts">
import cx from "classnames"
import {Tags} from "@welshman/util"
import {Scope, authorFeed, tagFeed, scopeFeed, relayFeed, intersectionFeed} from "@welshman/feeds"
import {
Scope,
makeAuthorFeed,
makeTagFeed,
makeScopeFeed,
makeRelayFeed,
makeIntersectionFeed,
} from "@welshman/feeds"
import type {Feed as TFeed} from "@welshman/feeds"
import {theme} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
@ -11,10 +18,10 @@
import {session, canSign, lists, userLists} from "src/engine"
export let relays = []
export let feed: TFeed = scopeFeed(Scope.Follows)
export let feed: TFeed = makeScopeFeed(Scope.Follows)
if (relays.length > 0) {
feed = intersectionFeed(relayFeed(...relays), feed)
feed = makeIntersectionFeed(makeRelayFeed(...relays), feed)
}
let key = Math.random()
@ -31,15 +38,15 @@
const urls = tags.values("r").valueOf()
if (authors.length > 0) {
feed = authorFeed(...authors)
feed = makeAuthorFeed(...authors)
} else if (topics.length > 0) {
feed = tagFeed("#t", ...topics)
feed = makeTagFeed("#t", ...topics)
} else {
feed = scopeFeed(Scope.Follows)
feed = makeScopeFeed(Scope.Follows)
}
if (urls.length > 0) {
feed = intersectionFeed(relayFeed(...urls), feed)
feed = makeIntersectionFeed(makeRelayFeed(...urls), feed)
}
key = Math.random()

View File

@ -1,5 +1,11 @@
<script lang="ts">
import {Scope, feedFromFilter, intersectionFeed, kindFeed, scopeFeed} from "@welshman/feeds"
import {
Scope,
feedFromFilter,
makeIntersectionFeed,
makeKindFeed,
makeScopeFeed,
} from "@welshman/feeds"
import Card from "src/partials/Card.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Feed from "src/app/shared/Feed.svelte"
@ -8,7 +14,7 @@
const feed = $env.FORCE_GROUP
? feedFromFilter({kinds: [30402], "#a": [$env.FORCE_GROUP]})
: intersectionFeed(kindFeed(30402), scopeFeed(Scope.Self, Scope.Follows))
: makeIntersectionFeed(makeKindFeed(30402), makeScopeFeed(Scope.Self, Scope.Follows))
const createListing = () => router.at("notes/create").qp({type: "listing"}).open()

View File

@ -3,7 +3,6 @@
import {now, sortBy} from "@welshman/lib"
import {PublishStatus} from "@welshman/net"
import Tile from "src/partials/Tile.svelte"
import AltColor from "src/partials/AltColor.svelte"
import Subheading from "src/partials/Subheading.svelte"
import PublishCard from "src/app/shared/PublishCard.svelte"
import type {PublishInfo} from "src/engine"

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {batch} from "hurdak"
import {wotFeed, relayFeed, intersectionFeed, feedFromFilter} from "@welshman/feeds"
import {makeWOTFeed, makeRelayFeed, makeIntersectionFeed, feedFromFilter} from "@welshman/feeds"
import type {Feed as TFeed} from "@welshman/feeds"
import {getAvgRating} from "src/util/nostr"
import Feed from "src/app/shared/Feed.svelte"
@ -11,13 +11,13 @@
import {deriveRelay, normalizeRelayUrl, displayRelay, getMinWot} from "src/engine"
export let url
export let feed: TFeed = wotFeed({min: getMinWot()})
export let feed: TFeed = makeWOTFeed({min: getMinWot()})
let reviews = []
let activeTab = "notes"
$: url = normalizeRelayUrl(url)
$: feed = intersectionFeed(relayFeed(url), feed)
$: feed = makeIntersectionFeed(makeRelayFeed(url), feed)
$: rating = getAvgRating(reviews)
const relay = deriveRelay(url)

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {tagFeed} from "@welshman/feeds"
import {makeTagFeed} from "@welshman/feeds"
import Feed from "src/app/shared/Feed.svelte"
import Heading from "src/partials/Heading.svelte"
import TopicActions from "src/app/shared/TopicActions.svelte"
@ -13,4 +13,4 @@
<TopicActions {topic} />
</div>
</div>
<Feed feed={tagFeed("#t", topic)} />
<Feed feed={makeTagFeed("#t", topic)} />

View File

@ -1,7 +1,7 @@
import {assocPath, uniq} from "ramda"
import {seconds} from "hurdak"
import {now} from "@welshman/lib"
import {relayFeed, feedFromFilter, unionFeed, intersectionFeed} from "@welshman/feeds"
import {makeRelayFeed, feedFromFilter, makeUnionFeed, makeIntersectionFeed} from "@welshman/feeds"
import {sessions} from "src/engine/session/state"
import {session} from "src/engine/session/derived"
import {loadPubkeys, subscribe} from "src/engine/network/utils"
@ -21,9 +21,9 @@ export const loadAllMessages = ({reload = false} = {}) => {
})
const loader = loadAll(
intersectionFeed(
relayFeed(...hints.User().getUrls()),
unionFeed(
makeIntersectionFeed(
makeRelayFeed(...hints.User().getUrls()),
makeUnionFeed(
feedFromFilter({kinds: [4], authors: [pubkey], since}),
feedFromFilter({kinds: [4, 1059], "#p": [pubkey], since}),
),

View File

@ -5,9 +5,9 @@ import type {LoadOpts} from "@welshman/feeds"
import {
FeedLoader,
Scope,
relayFeed,
intersectionFeed,
unionFeed,
makeRelayFeed,
makeIntersectionFeed,
makeUnionFeed,
feedFromFilter,
} from "@welshman/feeds"
import {giftWrapKinds, generatePrivateKey} from "src/util/nostr"
@ -74,7 +74,7 @@ export const feedLoader = new FeedLoader<Event | Rumor>({
)
}
},
requestDvm: async ({kind, tags = [], relays = [], onEvent}) => {
requestDVM: async ({kind, tags = [], relays = [], onEvent}) => {
const sk = generatePrivateKey()
const event = await dvmRequest({kind, tags, relays, sk, timeout: 3000})
@ -94,7 +94,7 @@ export const feedLoader = new FeedLoader<Event | Rumor>({
return pubkeys.length === 0 ? env.get().DEFAULT_FOLLOWS : pubkeys
},
getPubkeysForWotRange: (min, max) => {
getPubkeysForWOTRange: (min, max) => {
const pubkeys = []
const $user = user.get()
const thresholdMin = maxWot.get() * min
@ -143,7 +143,10 @@ export const sync = (fromUrl, toUrl, filters) => {
worker.addGlobalHandler(event => publish({event, relays: [toUrl]}))
const feed = intersectionFeed(relayFeed(fromUrl), unionFeed(...filters.map(feedFromFilter)))
const feed = makeIntersectionFeed(
makeRelayFeed(fromUrl),
makeUnionFeed(...filters.map(feedFromFilter)),
)
return loadAll(feed, {
onEvent: e => worker.push(e as Event),

View File

@ -1,5 +1,5 @@
import {nip19} from "nostr-tools"
import {pushToMapKey} from "@welshman/lib"
import {pushToMapKey, clamp} from "@welshman/lib"
import {
Router,
normalizeRelayUrl as normalize,
@ -161,13 +161,13 @@ export const hints = new Router({
const oneHour = 60 * oneMinute
const oneDay = 24 * oneHour
const oneWeek = 7 * oneDay
const {count = 0, faults = []} = relays.key(url).get() || {}
const connection = NetworkContext.pool.get(url, {autoConnect: false})
// If we haven't connected, consult our relay record and see if there has
// been a recent fault. If there has been, penalize the relay. If there have been several,
// don't use the relay.
if (!connection) {
const faults = relays.key(url).get()?.faults || []
const lastFault = last(faults) || 0
if (faults.filter(n => n > Date.now() - oneHour).length > 10) {
@ -192,7 +192,7 @@ export const hints = new Router({
[ConnectionStatus.Closed]: 0.6,
[ConnectionStatus.Slow]: 0.5,
[ConnectionStatus.Ok]: 1,
default: 0.5,
default: clamp([0.5, 1], count / 1000),
})
},
})

View File

@ -8,28 +8,30 @@
export let initialValue = null
export let value = initialValue
let prev = value
let date = value ? createLocalDate(value) : new Date()
const className = cx(
$$props.class,
"rounded-full shadow-inset py-2 px-4 w-full placeholder:text-neutral-400",
"bg-white border border-solid border-neutral-200 text-black pl-10",
)
const toDate = v => (v ? createLocalDate(v) : null)
const setValue = newValue => {
if (value === newValue) {
return
}
value = newValue
date = toDate(value)
onChange?.(value)
}
const setDate = d => {
try {
value = formatDateAsLocalISODate(d).slice(0, 10)
date = d
setValue(formatDateAsLocalISODate(d).slice(0, 10))
} catch (e) {
logger.error(e)
}
if (prev !== value) {
onChange?.(value)
}
prev = value
}
const init = () => {
@ -38,11 +40,11 @@
}
}
const clear = () => {
value = null
}
const clear = () => setValue(null)
$: value && setDate(date)
let date = toDate(value)
$: date ? setDate(date) : setValue(null)
</script>
<div class={cx(className, "relative")}>

View File

@ -1,11 +1,15 @@
<script lang="ts">
import cx from "classnames"
import Anchor from "src/partials/Anchor.svelte"
export let inert = false
</script>
<Anchor
{...$$props}
class={cx($$props.class, "block p-3 px-4 transition-all hover:bg-accent hover:text-white")}
class={cx($$props.class, "block p-3 px-4", {
"transition-all hover:bg-accent hover:text-white": !inert,
})}
on:click>
<slot />
</Anchor>

View File

@ -1,7 +1,14 @@
<script lang="ts">
import cx from "classnames"
import {onMount} from "svelte"
import {fly} from "src/util/transition"
export let onClose = null
export let hideOnClick = false
export let position = "bottom"
export let absolute = false
export let fixed = false
let popover
const removePadding = () => {
@ -36,9 +43,23 @@
})
</script>
<div class="relative">
<svelte:window
on:mouseup={e => {
if (hideOnClick || !popover.contains(e.target)) {
setTimeout(onClose)
}
}}
on:keydown={e => {
if (e.key === "Escape") {
setTimeout(onClose)
}
}} />
<div class={cx($$props.class, {absolute, fixed})}>
<div
class="absolute left-0 right-0 top-0 z-popover"
class:top-0={position === "bottom"}
class:bottom-0={position === "top"}
bind:this={popover}
transition:fly|local={{y: 20, duration: 200}}>
<slot />

View File

@ -1,5 +1,4 @@
<script lang="ts">
import cx from "classnames"
import {without} from "ramda"
export let value = null

View File

@ -13,4 +13,14 @@
)
</script>
<textarea {...$$props} class={className} bind:this={element} bind:value on:keydown on:keypress />
<textarea
{...$$props}
class={className}
bind:this={element}
bind:value
on:keydown
on:keypress
on:change
on:input
on:blur
on:focus />