mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Re-work feeds based on new spec
This commit is contained in:
parent
9f2e59e679
commit
05eecb42b3
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
package-lock.json -diff
|
||||
yarn.lock -diff
|
@ -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>
|
||||
|
@ -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: []}]
|
||||
}
|
||||
|
@ -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,36 +115,33 @@
|
||||
<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"
|
||||
@ -136,10 +149,10 @@
|
||||
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")}
|
||||
{#if currentScopeFeed && isScopeFeed(currentScopeFeed)}
|
||||
From {displayList(getFeedArgs(currentScopeFeed))}
|
||||
{:else if currentScopeFeed && isAuthorFeed(currentScopeFeed)}
|
||||
From {quantify(getFeedArgs(currentScopeFeed).length, "author")}
|
||||
{:else}
|
||||
From global
|
||||
{/if}
|
||||
@ -148,54 +161,59 @@
|
||||
</div>
|
||||
<div slot="tooltip">
|
||||
<Menu>
|
||||
<MenuItem on:click={() => setPart({scopes: [Scope.Follows]})}>
|
||||
<MenuItem on:click={() => setFeed(makeScopeFeed(Scope.Follows))}>
|
||||
<i class="fa fa-user-plus mr-2" /> Follows
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => setPart({scopes: [Scope.Network]})}>
|
||||
<MenuItem on:click={() => setFeed(makeScopeFeed(Scope.Network))}>
|
||||
<i class="fa fa-share-nodes mr-2" /> Network
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => removeParts(["scopes"])}>
|
||||
<MenuItem on:click={() => removeFeed(currentScopeFeed)}>
|
||||
<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)}
|
||||
{#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}
|
||||
{/each}
|
||||
{/if}
|
||||
</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}
|
||||
<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}
|
||||
|
@ -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
|
||||
}
|
||||
} else if (type === FeedType.Filter) {
|
||||
controller.setAtCursor([type, {}])
|
||||
} else if (type === FeedType.DVM) {
|
||||
controller.setAtCursor([type, {kind: 5300, tags: [], relays: []}])
|
||||
} else {
|
||||
controller.setAtCursor([type])
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
})
|
||||
return FormType.Advanced
|
||||
}
|
||||
|
||||
let cursor = []
|
||||
const onFormTypeChange = newFormType => {
|
||||
if (formType === newFormType) {
|
||||
return
|
||||
}
|
||||
|
||||
$: console.log(JSON.stringify(feed, null, 2))
|
||||
$: current = cursor.reduce((f, i) => f[i], feed)
|
||||
$: subFeeds = getSubFeeds(current)
|
||||
$: feedType = current[0]
|
||||
// If we can't deal with the feed, clear it out
|
||||
if (!isNormalized(feed)) {
|
||||
feed = makeIntersectionFeed()
|
||||
}
|
||||
|
||||
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}
|
||||
{#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 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}
|
||||
<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>
|
||||
|
49
src/app/shared/FeedFormAdvanced.svelte
Normal file
49
src/app/shared/FeedFormAdvanced.svelte
Normal 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}
|
16
src/app/shared/FeedFormDVMs.svelte
Normal file
16
src/app/shared/FeedFormDVMs.svelte
Normal 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} />
|
135
src/app/shared/FeedFormFilters.svelte
Normal file
135
src/app/shared/FeedFormFilters.svelte
Normal 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}
|
18
src/app/shared/FeedFormPeople.svelte
Normal file
18
src/app/shared/FeedFormPeople.svelte
Normal 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} />
|
@ -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>
|
17
src/app/shared/FeedFormRelays.svelte
Normal file
17
src/app/shared/FeedFormRelays.svelte
Normal 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} />
|
39
src/app/shared/FeedFormSectionCreatedAt.svelte
Normal file
39
src/app/shared/FeedFormSectionCreatedAt.svelte
Normal 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>
|
14
src/app/shared/FeedFormSectionDVM.svelte
Normal file
14
src/app/shared/FeedFormSectionDVM.svelte
Normal 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>
|
@ -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>
|
30
src/app/shared/FeedFormSectionMentions.svelte
Normal file
30
src/app/shared/FeedFormSectionMentions.svelte
Normal 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>
|
53
src/app/shared/FeedFormSectionPeople.svelte
Normal file
53
src/app/shared/FeedFormSectionPeople.svelte
Normal 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>
|
19
src/app/shared/FeedFormSectionRelays.svelte
Normal file
19
src/app/shared/FeedFormSectionRelays.svelte
Normal 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>
|
19
src/app/shared/FeedFormSectionTopics.svelte
Normal file
19
src/app/shared/FeedFormSectionTopics.svelte
Normal 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>
|
18
src/app/shared/FeedFormTopics.svelte
Normal file
18
src/app/shared/FeedFormTopics.svelte
Normal 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} />
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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) {
|
||||
|
@ -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])
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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)} />
|
||||
|
@ -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}),
|
||||
),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
@ -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 setDate = d => {
|
||||
try {
|
||||
value = formatDateAsLocalISODate(d).slice(0, 10)
|
||||
date = d
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
const toDate = v => (v ? createLocalDate(v) : null)
|
||||
|
||||
const setValue = newValue => {
|
||||
if (value === newValue) {
|
||||
return
|
||||
}
|
||||
|
||||
if (prev !== value) {
|
||||
value = newValue
|
||||
date = toDate(value)
|
||||
onChange?.(value)
|
||||
}
|
||||
|
||||
prev = value
|
||||
const setDate = d => {
|
||||
try {
|
||||
setValue(formatDateAsLocalISODate(d).slice(0, 10))
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
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")}>
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {without} from "ramda"
|
||||
|
||||
export let value = null
|
||||
|
@ -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 />
|
||||
|
Loading…
Reference in New Issue
Block a user