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 dvmItem
|
||||||
export let onChange
|
export let onChange
|
||||||
export let onRemove
|
|
||||||
|
|
||||||
const removeTag = i => {
|
const removeTag = i => {
|
||||||
onChange({...dvmItem, tags: dvmItem.tags.toSpliced(i, 1)})
|
onChange({...dvmItem, tags: dvmItem.tags.toSpliced(i, 1)})
|
||||||
@ -29,6 +28,7 @@
|
|||||||
{label: "Content discovery", kind: 5300},
|
{label: "Content discovery", kind: 5300},
|
||||||
{label: "Person discovery", kind: 5301},
|
{label: "Person discovery", kind: 5301},
|
||||||
{label: "Content search", kind: 5302},
|
{label: "Content search", kind: 5302},
|
||||||
|
{label: "Person Search", kind: 5303},
|
||||||
]
|
]
|
||||||
|
|
||||||
const searchKindItems = fuzzy(kinds, {keys: ["kind", "label"]})
|
const searchKindItems = fuzzy(kinds, {keys: ["kind", "label"]})
|
||||||
@ -46,14 +46,18 @@
|
|||||||
|
|
||||||
<FlexColumn class="relative">
|
<FlexColumn class="relative">
|
||||||
<Field label="Kind">
|
<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>
|
<div slot="item" let:item>{displayKind(item)}</div>
|
||||||
</SearchSelect>
|
</SearchSelect>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Relays">
|
<Field label="Relays">
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
multiple
|
multiple
|
||||||
value={dvmItem.relays}
|
value={dvmItem.relays || []}
|
||||||
search={$searchRelayUrls}
|
search={$searchRelayUrls}
|
||||||
termToItem={normalizeRelayUrl}
|
termToItem={normalizeRelayUrl}
|
||||||
onChange={relays => onChange({...dvmItem, relays})}>
|
onChange={relays => onChange({...dvmItem, relays})}>
|
||||||
@ -61,17 +65,17 @@
|
|||||||
</SearchSelect>
|
</SearchSelect>
|
||||||
<p slot="info">Select which relays requests to this DVM should be sent to.</p>
|
<p slot="info">Select which relays requests to this DVM should be sent to.</p>
|
||||||
</Field>
|
</Field>
|
||||||
{#each dvmItem.tags as [type, value], i (i + key)}
|
{#each dvmItem.tags || [] as [type, value], i (i + key)}
|
||||||
<div class="flex gap-2 items-center justify-between">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<i class="fa fa-trash cursor-pointer" on:click={() => removeTag(i)} />
|
<i class="fa fa-trash cursor-pointer" on:click={() => removeTag(i)} />
|
||||||
<div class="flex gap-2 items-center justify-end">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex items-center gap-3">
|
||||||
<label>Type</label>
|
<label>Type</label>
|
||||||
<Input bind:value={type} on:change={() => onChange(dvmItem)} />
|
<Input bind:value={type} on:change={() => onChange(dvmItem)} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex items-center gap-3">
|
||||||
<label>Value</label>
|
<label>Value</label>
|
||||||
<Input bind:value={value} on:change={() => onChange(dvmItem)} />
|
<Input bind:value on:change={() => onChange(dvmItem)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +83,4 @@
|
|||||||
<Anchor on:click={addTag} class="cursor-pointer">
|
<Anchor on:click={addTag} class="cursor-pointer">
|
||||||
<i class="fa fa-plus" /> Add tag
|
<i class="fa fa-plus" /> Add tag
|
||||||
</Anchor>
|
</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>
|
</FlexColumn>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {Storage} from "hurdak"
|
import {Storage} from "hurdak"
|
||||||
import {prop} from "ramda"
|
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import type {Feed} from "@welshman/feeds"
|
import type {Feed} from "@welshman/feeds"
|
||||||
import {createScroller} from "src/util/misc"
|
import {createScroller} from "src/util/misc"
|
||||||
@ -59,7 +58,7 @@
|
|||||||
if (feedLoader.compiler.canCompile(opts.feed)) {
|
if (feedLoader.compiler.canCompile(opts.feed)) {
|
||||||
const requests = await feedLoader.compiler.compile(opts.feed)
|
const requests = await feedLoader.compiler.compile(opts.feed)
|
||||||
|
|
||||||
filters = requests.flatMap(r => r.filters)
|
filters = requests.flatMap(r => r.filters || [])
|
||||||
} else {
|
} else {
|
||||||
filters = [{ids: []}]
|
filters = [{ids: []}]
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {omit} from "ramda"
|
|
||||||
import {quantify, pluralize, displayList} from "hurdak"
|
import {quantify, pluralize, displayList} from "hurdak"
|
||||||
import {isNil, clamp} from "@welshman/lib"
|
import {isNil, clamp} from "@welshman/lib"
|
||||||
import type {DynamicFilter, Feed} from "@welshman/feeds"
|
import {Tags} from "@welshman/util"
|
||||||
import {FeedType, Scope, getSubFeeds} from "@welshman/feeds"
|
import {
|
||||||
|
FeedType,
|
||||||
|
isScopeFeed,
|
||||||
|
isSearchFeed,
|
||||||
|
isAuthorFeed,
|
||||||
|
isCreatedAtFeed,
|
||||||
|
makeSearchFeed,
|
||||||
|
makeScopeFeed,
|
||||||
|
makeIntersectionFeed,
|
||||||
|
Scope,
|
||||||
|
hasSubFeeds,
|
||||||
|
getFeedArgs,
|
||||||
|
feedsFromTags,
|
||||||
|
} from "@welshman/feeds"
|
||||||
import {slide} from "src/util/transition"
|
import {slide} from "src/util/transition"
|
||||||
import {formatTimestampAsDate, getStringWidth} from "src/util/misc"
|
import {formatTimestampAsDate, getStringWidth} from "src/util/misc"
|
||||||
import Card from "src/partials/Card.svelte"
|
|
||||||
import Popover from "src/partials/Popover.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 Menu from "src/partials/Menu.svelte"
|
||||||
import MenuItem from "src/partials/MenuItem.svelte"
|
import MenuItem from "src/partials/MenuItem.svelte"
|
||||||
import Chip from "src/partials/Chip.svelte"
|
import Chip from "src/partials/Chip.svelte"
|
||||||
import Toggle from "src/partials/Toggle.svelte"
|
import Toggle from "src/partials/Toggle.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
import {router} from "src/app/util"
|
||||||
import FeedForm from "src/app/shared/FeedForm.svelte"
|
import {displayRelayUrl, displayPubkey, displayList as displayList2, userLists} from "src/engine"
|
||||||
import {feedLoader, displayRelayUrl, displayPubkey} from "src/engine"
|
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const openModal = () => {
|
const openListMenu = () => {
|
||||||
isOpen = true
|
listMenuIsOpen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeListMenu = () => {
|
||||||
isOpen = false
|
listMenuIsOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSearch = () => {
|
const showSearch = () => {
|
||||||
@ -40,12 +51,22 @@
|
|||||||
value = {...value, shouldHideReplies: !value.shouldHideReplies}
|
value = {...value, shouldHideReplies: !value.shouldHideReplies}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPart = (filter: DynamicFilter) => {
|
const getSearch = feed => getFeedArgs(feed)?.find(isSearchFeed)?.[1] as string | null
|
||||||
saveFeed([feed[0], {...feed[1], ...filter}] as Feed)
|
|
||||||
|
const saveFeed = feed => {
|
||||||
|
value = {...value, feed}
|
||||||
|
search = getSearch(feed)
|
||||||
|
closeListMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeParts = (keys: string[]) => {
|
const setFeed = thisFeed => {
|
||||||
saveFeed([feed[0], omit(keys, feed[1])] as Feed)
|
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 = () => {
|
const onSearchFocus = () => {
|
||||||
@ -66,31 +87,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
setPart({search: text})
|
setFeed(makeSearchFeed(text))
|
||||||
} else {
|
} else {
|
||||||
removeParts(["search"])
|
removeFeed(subFeeds.find(isSearchFeed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFeed = f => {
|
const loadList = list => saveFeed(makeIntersectionFeed(...feedsFromTags(Tags.fromEvent(list))))
|
||||||
feed = f
|
|
||||||
value = {...value, feed}
|
|
||||||
search = feed[1]?.search
|
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayPeople = pubkeys =>
|
const displayPeople = pubkeys =>
|
||||||
pubkeys.length === 1 ? displayPubkey(pubkeys[0]) : `${pubkeys.length} people`
|
pubkeys.length === 1 ? displayPubkey(pubkeys[0]) : `${pubkeys.length} people`
|
||||||
|
|
||||||
const displayTopics = topics => (topics.length === 1 ? topics[0] : `${topics.length} topics`)
|
const displayTopics = topics => (topics.length === 1 ? topics[0] : `${topics.length} topics`)
|
||||||
|
|
||||||
let isOpen = false
|
let listMenuIsOpen = false
|
||||||
let search = value.feed[1]?.search
|
|
||||||
let searchFocused = false
|
let searchFocused = false
|
||||||
|
let search = getSearch(value.feed)
|
||||||
|
|
||||||
$: feed = value.feed
|
$: feed = hasSubFeeds(value.feed) ? value.feed : [FeedType.Intersection, value.feed]
|
||||||
$: feedType = feed[0]
|
$: subFeeds = getFeedArgs(feed)
|
||||||
$: subFeeds = getSubFeeds(feed)
|
$: currentScopeFeed = subFeeds.find(f => isScopeFeed(f) || isAuthorFeed(f))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="-mb-2">
|
<div class="-mb-2">
|
||||||
@ -99,103 +115,105 @@
|
|||||||
<Toggle scale={0.6} value={!value.shouldHideReplies} on:change={toggleReplies} />
|
<Toggle scale={0.6} value={!value.shouldHideReplies} on:change={toggleReplies} />
|
||||||
<small class="text-neutral-200">Show replies</small>
|
<small class="text-neutral-200">Show replies</small>
|
||||||
</div>
|
</div>
|
||||||
<i class="fa fa-sliders cursor-pointer p-2" on:click={openModal} />
|
<div class="relative lg:hidden">
|
||||||
<slot name="controls" />
|
<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>
|
||||||
<div class="mb-2 mr-2 inline-block py-1">Showing notes:</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">
|
<Chip class="mb-2 mr-2 inline-block">
|
||||||
Custom feed ({quantify(subFeeds.length, "selection")})
|
Custom feed ({quantify(subFeeds.length, "selection")})
|
||||||
</Chip>
|
</Chip>
|
||||||
{/if}
|
{:else}
|
||||||
{#if feedType === FeedType.Relay}
|
<Popover
|
||||||
<Chip class="mb-2 mr-2 inline-block">
|
class="inline-block"
|
||||||
On {feed[1].length === 1 ? displayRelayUrl(feed[1][0]) : `${feed[1].length} relays`}
|
placement="bottom-end"
|
||||||
</Chip>
|
theme="transparent"
|
||||||
{:else if feedType === FeedType.List}
|
opts={{hideOnClick: true}}>
|
||||||
<Chip class="mb-2 mr-2 inline-block">
|
<div slot="trigger" class="cursor-pointer">
|
||||||
From {quantify(feed.slice(1).length, "list")}
|
<Chip class="mb-2 mr-2 inline-block">
|
||||||
</Chip>
|
{#if currentScopeFeed && isScopeFeed(currentScopeFeed)}
|
||||||
{:else if feedType === FeedType.DVM}
|
From {displayList(getFeedArgs(currentScopeFeed))}
|
||||||
<Chip class="mb-2 mr-2 inline-block">
|
{:else if currentScopeFeed && isAuthorFeed(currentScopeFeed)}
|
||||||
From {quantify(feed.slice(1).length, "DVM")}
|
From {quantify(getFeedArgs(currentScopeFeed).length, "author")}
|
||||||
</Chip>
|
{:else}
|
||||||
{:else if feedType === FeedType.Filter}
|
From global
|
||||||
{#if feed.length > 2}
|
{/if}
|
||||||
<Chip class="mb-2 mr-2 inline-block">
|
<i class="fa fa-caret-down p-1" />
|
||||||
From {quantify(feed.slice(1).length, "filter")}
|
</Chip>
|
||||||
</Chip>
|
</div>
|
||||||
{:else}
|
<div slot="tooltip">
|
||||||
{#await feedLoader.compiler.compile(feed)}
|
<Menu>
|
||||||
<!-- pass -->
|
<MenuItem on:click={() => setFeed(makeScopeFeed(Scope.Follows))}>
|
||||||
{:then [{ filters: [filter] }]}
|
<i class="fa fa-user-plus mr-2" /> Follows
|
||||||
<Popover
|
</MenuItem>
|
||||||
class="inline-block"
|
<MenuItem on:click={() => setFeed(makeScopeFeed(Scope.Network))}>
|
||||||
placement="bottom-end"
|
<i class="fa fa-share-nodes mr-2" /> Network
|
||||||
theme="transparent"
|
</MenuItem>
|
||||||
opts={{hideOnClick: true}}>
|
<MenuItem on:click={() => removeFeed(currentScopeFeed)}>
|
||||||
<div slot="trigger" class="cursor-pointer">
|
<i class="fa fa-earth-americas mr-2" /> Global
|
||||||
<Chip class="mb-2 mr-2 inline-block">
|
</MenuItem>
|
||||||
{#if feed[1].scopes}
|
</Menu>
|
||||||
From {displayList(feed[1].scopes)}
|
</div>
|
||||||
{:else if filter.authors}
|
</Popover>
|
||||||
From {quantify(filter.authors.length, "author")}
|
{#each subFeeds as subFeed}
|
||||||
{:else}
|
{@const feedType = subFeed[0]}
|
||||||
From global
|
{#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}
|
{/if}
|
||||||
<i class="fa fa-caret-down p-1" />
|
{/each}
|
||||||
</Chip>
|
{/if}
|
||||||
</div>
|
</Chip>
|
||||||
<div slot="tooltip">
|
{/if}
|
||||||
<Menu>
|
{/each}
|
||||||
<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}
|
|
||||||
<Chip class="cursor-pointer" on:click={showSearch}>
|
<Chip class="cursor-pointer" on:click={showSearch}>
|
||||||
<div class="flex h-6 items-center justify-center">
|
<div class="flex h-6 items-center justify-center">
|
||||||
<i class="fa fa-search" />
|
<i class="fa fa-search" />
|
||||||
@ -216,10 +234,3 @@
|
|||||||
</Chip>
|
</Chip>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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">
|
<script lang="ts">
|
||||||
import {assocPath} from "ramda"
|
import {FeedType, makeIntersectionFeed, hasSubFeeds, getFeedArgs} from "@welshman/feeds"
|
||||||
import {quantify, switcherFn, updatePath} from "hurdak"
|
|
||||||
import {inc} from "@welshman/lib"
|
|
||||||
import {FeedType, hasSubFeeds, getSubFeeds} from "@welshman/feeds"
|
|
||||||
import Icon from "src/partials/Icon.svelte"
|
import Icon from "src/partials/Icon.svelte"
|
||||||
import SelectTiles from "src/partials/SelectTiles.svelte"
|
import SelectTiles from "src/partials/SelectTiles.svelte"
|
||||||
import Card from "src/partials/Card.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 Anchor from "src/partials/Anchor.svelte"
|
||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
import Field from "src/partials/Field.svelte"
|
import Field from "src/partials/Field.svelte"
|
||||||
import Select from "src/partials/Select.svelte"
|
import FeedFormPeople from "src/app/shared/FeedFormPeople.svelte"
|
||||||
import SearchSelect from "src/partials/SearchSelect.svelte"
|
import FeedFormTopics from "src/app/shared/FeedFormTopics.svelte"
|
||||||
import FilterField from "src/app/shared/FilterField.svelte"
|
import FeedFormRelays from "src/app/shared/FeedFormRelays.svelte"
|
||||||
import DVMField from "src/app/shared/DVMField.svelte"
|
import FeedFormDVMs from "src/app/shared/FeedFormDVMs.svelte"
|
||||||
import FeedFormRelay from "src/app/shared/FeedFormRelay.svelte"
|
import FeedFormAdvanced from "src/app/shared/FeedFormAdvanced.svelte"
|
||||||
import {searchRelayUrls, searchListAddrs, displayListByAddress, displayRelayUrl} from "src/engine"
|
|
||||||
|
|
||||||
export let feed
|
export let feed
|
||||||
export let onChange
|
export let onChange
|
||||||
export let onCancel
|
export let onCancel
|
||||||
|
|
||||||
const controller = {
|
enum FormType {
|
||||||
pushCursor: i => {
|
Advanced = "advanced",
|
||||||
cursor = [...cursor, i]
|
DVMs = "dvms",
|
||||||
},
|
People = "people",
|
||||||
popCursor: i => {
|
Relays = "relays",
|
||||||
cursor = cursor.slice(0, -1)
|
Topics = "topics",
|
||||||
},
|
|
||||||
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)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTypeChange = type => {
|
const normalize = feed => (isNormalized(feed) ? feed : [FeedType.Intersection, feed])
|
||||||
if (hasSubFeeds([type])) {
|
|
||||||
if (hasSubFeeds(current)) {
|
const isNormalized = feed =>
|
||||||
controller.setAtCursor([type, ...current.slice(1)])
|
feed[0] === FeedType.Intersection && getFeedArgs(feed).every(f => !hasSubFeeds(f))
|
||||||
} else {
|
|
||||||
controller.setAtCursor([type, current])
|
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]) =>
|
const onFormTypeChange = newFormType => {
|
||||||
switcherFn(type, {
|
if (formType === newFormType) {
|
||||||
[FeedType.Filter]: () => quantify(feed.length, "filter"),
|
return
|
||||||
[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"),
|
|
||||||
})
|
|
||||||
|
|
||||||
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))
|
formType = newFormType
|
||||||
$: current = cursor.reduce((f, i) => f[i], feed)
|
}
|
||||||
$: subFeeds = getSubFeeds(current)
|
|
||||||
$: feedType = current[0]
|
const onFeedChange = newFeed => {
|
||||||
|
feed = newFeed
|
||||||
|
}
|
||||||
|
|
||||||
|
let formType = inferFormType(feed)
|
||||||
|
|
||||||
|
$: console.log(JSON.stringify(normalize(feed), null, 2))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FlexColumn class="pb-32">
|
<FlexColumn class="pb-32">
|
||||||
<Card>
|
<Card>
|
||||||
<Field label="Choose a feed type">
|
<Field label="Choose a feed type">
|
||||||
<SelectTiles
|
<SelectTiles
|
||||||
options={[FeedType.Filter, FeedType.Relay, FeedType.DVM, "advanced"]}
|
options={[FormType.People, FormType.Topics, FormType.Relays, FormType.DVMs]}
|
||||||
onChange={onTypeChange}
|
onChange={onFormTypeChange}
|
||||||
value={feedType}>
|
value={formType}>
|
||||||
<div slot="item" class="flex flex-col items-center" let:option let:active>
|
<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"} />
|
<Icon icon="people-nearby" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
|
||||||
<span class="staatliches text-2xl">Standard</span>
|
<span class="staatliches text-2xl">People</span>
|
||||||
{:else if option === FeedType.Relay}
|
{: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"} />
|
<Icon icon="server" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
|
||||||
<span class="staatliches text-2xl">Relays</span>
|
<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"} />
|
<Icon icon="network" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
|
||||||
<span class="staatliches text-2xl">DVMs</span>
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SelectTiles>
|
</SelectTiles>
|
||||||
</Field>
|
</Field>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Anchor underline on:click={() => onFormTypeChange(FormType.Advanced)}>Advanced mode</Anchor>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{#if feedType === FeedType.Relay}
|
{#if formType === FormType.People}
|
||||||
<FeedFormRelay feed={current} {controller} />
|
<FeedFormPeople feed={normalize(feed)} onChange={onFeedChange} />
|
||||||
<Field label="Which relays would you like to use?">
|
{:else if formType === FormType.Topics}
|
||||||
<SearchSelect
|
<FeedFormTopics feed={normalize(feed)} onChange={onFeedChange} />
|
||||||
multiple
|
{:else if formType === FormType.Relays}
|
||||||
value={current[1] || []}
|
<FeedFormRelays feed={normalize(feed)} onChange={onFeedChange} />
|
||||||
search={$searchRelayUrls}
|
{:else if formType === FormType.DVMs}
|
||||||
onChange={urls => controller.setAtCursor(urls, [1])}>
|
<FeedFormDVMs feed={normalize(feed)} onChange={onFeedChange} />
|
||||||
<span slot="item" let:item>{displayRelayUrl(item)}</span>
|
{:else if formType === FormType.Advanced}
|
||||||
</SearchSelect>
|
<FeedFormAdvanced {feed} onChange={onFeedChange} />
|
||||||
<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}
|
{/if}
|
||||||
{#each subFeeds as subFeed, i (displayFeed(subFeed) + i)}
|
<div class="flex items-center justify-between gap-3">
|
||||||
<Card class="flex items-center justify-between">
|
<Anchor button on:click={onCancel}>Discard</Anchor>
|
||||||
<div class="flex items-center gap-3">
|
<Anchor button accent on:click={() => onChange(feed)}>Save feed</Anchor>
|
||||||
<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>
|
</div>
|
||||||
</FlexColumn>
|
</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">
|
<script lang="ts">
|
||||||
import {pluck, identity} from "ramda"
|
import {pluck} from "ramda"
|
||||||
|
import {FeedType} from "@welshman/feeds"
|
||||||
import {fuzzy} from "src/util/misc"
|
import {fuzzy} from "src/util/misc"
|
||||||
import Field from "src/partials/Field.svelte"
|
import Field from "src/partials/Field.svelte"
|
||||||
import SearchSelect from "src/partials/SearchSelect.svelte"
|
import SearchSelect from "src/partials/SearchSelect.svelte"
|
||||||
|
|
||||||
export let filter
|
export let feed
|
||||||
export let onChange
|
export let onChange
|
||||||
export let onRemove
|
|
||||||
|
|
||||||
const change = kinds => onChange({...filter, kinds})
|
const onKindsChange = kinds => onChange([FeedType.Kind, ...kinds])
|
||||||
|
|
||||||
const kinds = [
|
const kinds = [
|
||||||
{label: "Note", kind: 1},
|
{label: "Note", kind: 1},
|
||||||
@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
const searchKindItems = fuzzy(kinds, {keys: ["kind", "label"]})
|
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 displayKind = kind => {
|
||||||
const option = kinds.find(k => k.kind === kind)
|
const option = kinds.find(k => k.kind === kind)
|
||||||
@ -47,11 +47,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field>
|
<Field label="What kind of content do you want to see?">
|
||||||
<span slot="label" class="flex cursor-pointer items-center gap-2" on:click={onRemove}>
|
<SearchSelect multiple search={searchKinds} value={feed.slice(1)} onChange={onKindsChange}>
|
||||||
<i class="fa fa-trash fa-sm" /> Kinds
|
|
||||||
</span>
|
|
||||||
<SearchSelect multiple search={searchKinds} value={filter.kinds || []} onChange={change} termToItem={identity}>
|
|
||||||
<div slot="item" let:item>{displayKind(item)}</div>
|
<div slot="item" let:item>{displayKind(item)}</div>
|
||||||
</SearchSelect>
|
</SearchSelect>
|
||||||
</Field>
|
</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()
|
isDeleted = isDeleted.get()
|
||||||
|
|
||||||
constructor(readonly opts: FeedOpts) {
|
constructor(readonly opts: FeedOpts) {
|
||||||
// @ts-ignore
|
|
||||||
window.feed = this
|
|
||||||
|
|
||||||
// Use a custom feed loader so we can intercept the filters
|
// Use a custom feed loader so we can intercept the filters
|
||||||
this.feedLoader = new CoreFeedLoader({
|
this.feedLoader = new CoreFeedLoader({
|
||||||
...baseFeedLoader.options,
|
...baseFeedLoader.options,
|
||||||
@ -171,7 +168,6 @@ export class FeedLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentIds = new Set<string>()
|
|
||||||
const notesWithParent = notes.filter(e => {
|
const notesWithParent = notes.filter(e => {
|
||||||
if (repostKinds.includes(e.kind)) {
|
if (repostKinds.includes(e.kind)) {
|
||||||
return false
|
return false
|
||||||
@ -187,17 +183,9 @@ export class FeedLoader {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const id of ids) {
|
|
||||||
parentIds.add(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (parentIds.size === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const selections = hints.merge(notesWithParent.map(hints.EventParents)).getSelections()
|
const selections = hints.merge(notesWithParent.map(hints.EventParents)).getSelections()
|
||||||
|
|
||||||
for (const {relay, values} of selections) {
|
for (const {relay, values} of selections) {
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
<script lang="ts">
|
<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 Calendar from "src/app/shared/Calendar.svelte"
|
||||||
import {env, loadGroupMessages} from "src/engine"
|
import {env, loadGroupMessages} from "src/engine"
|
||||||
|
|
||||||
const feed = $env.FORCE_GROUP
|
const feed = $env.FORCE_GROUP
|
||||||
? feedFromFilter({kinds: [31923], "#a": [$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) {
|
if ($env.FORCE_GROUP) {
|
||||||
loadGroupMessages([$env.FORCE_GROUP])
|
loadGroupMessages([$env.FORCE_GROUP])
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {Tags} from "@welshman/util"
|
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 type {Feed as TFeed} from "@welshman/feeds"
|
||||||
import {theme} from "src/partials/state"
|
import {theme} from "src/partials/state"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
@ -11,10 +18,10 @@
|
|||||||
import {session, canSign, lists, userLists} from "src/engine"
|
import {session, canSign, lists, userLists} from "src/engine"
|
||||||
|
|
||||||
export let relays = []
|
export let relays = []
|
||||||
export let feed: TFeed = scopeFeed(Scope.Follows)
|
export let feed: TFeed = makeScopeFeed(Scope.Follows)
|
||||||
|
|
||||||
if (relays.length > 0) {
|
if (relays.length > 0) {
|
||||||
feed = intersectionFeed(relayFeed(...relays), feed)
|
feed = makeIntersectionFeed(makeRelayFeed(...relays), feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = Math.random()
|
let key = Math.random()
|
||||||
@ -31,15 +38,15 @@
|
|||||||
const urls = tags.values("r").valueOf()
|
const urls = tags.values("r").valueOf()
|
||||||
|
|
||||||
if (authors.length > 0) {
|
if (authors.length > 0) {
|
||||||
feed = authorFeed(...authors)
|
feed = makeAuthorFeed(...authors)
|
||||||
} else if (topics.length > 0) {
|
} else if (topics.length > 0) {
|
||||||
feed = tagFeed("#t", ...topics)
|
feed = makeTagFeed("#t", ...topics)
|
||||||
} else {
|
} else {
|
||||||
feed = scopeFeed(Scope.Follows)
|
feed = makeScopeFeed(Scope.Follows)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urls.length > 0) {
|
if (urls.length > 0) {
|
||||||
feed = intersectionFeed(relayFeed(...urls), feed)
|
feed = makeIntersectionFeed(makeRelayFeed(...urls), feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
key = Math.random()
|
key = Math.random()
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
<script lang="ts">
|
<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 Card from "src/partials/Card.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Feed from "src/app/shared/Feed.svelte"
|
import Feed from "src/app/shared/Feed.svelte"
|
||||||
@ -8,7 +14,7 @@
|
|||||||
|
|
||||||
const feed = $env.FORCE_GROUP
|
const feed = $env.FORCE_GROUP
|
||||||
? feedFromFilter({kinds: [30402], "#a": [$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()
|
const createListing = () => router.at("notes/create").qp({type: "listing"}).open()
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
import {now, sortBy} from "@welshman/lib"
|
import {now, sortBy} from "@welshman/lib"
|
||||||
import {PublishStatus} from "@welshman/net"
|
import {PublishStatus} from "@welshman/net"
|
||||||
import Tile from "src/partials/Tile.svelte"
|
import Tile from "src/partials/Tile.svelte"
|
||||||
import AltColor from "src/partials/AltColor.svelte"
|
|
||||||
import Subheading from "src/partials/Subheading.svelte"
|
import Subheading from "src/partials/Subheading.svelte"
|
||||||
import PublishCard from "src/app/shared/PublishCard.svelte"
|
import PublishCard from "src/app/shared/PublishCard.svelte"
|
||||||
import type {PublishInfo} from "src/engine"
|
import type {PublishInfo} from "src/engine"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {batch} from "hurdak"
|
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 type {Feed as TFeed} from "@welshman/feeds"
|
||||||
import {getAvgRating} from "src/util/nostr"
|
import {getAvgRating} from "src/util/nostr"
|
||||||
import Feed from "src/app/shared/Feed.svelte"
|
import Feed from "src/app/shared/Feed.svelte"
|
||||||
@ -11,13 +11,13 @@
|
|||||||
import {deriveRelay, normalizeRelayUrl, displayRelay, getMinWot} from "src/engine"
|
import {deriveRelay, normalizeRelayUrl, displayRelay, getMinWot} from "src/engine"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let feed: TFeed = wotFeed({min: getMinWot()})
|
export let feed: TFeed = makeWOTFeed({min: getMinWot()})
|
||||||
|
|
||||||
let reviews = []
|
let reviews = []
|
||||||
let activeTab = "notes"
|
let activeTab = "notes"
|
||||||
|
|
||||||
$: url = normalizeRelayUrl(url)
|
$: url = normalizeRelayUrl(url)
|
||||||
$: feed = intersectionFeed(relayFeed(url), feed)
|
$: feed = makeIntersectionFeed(makeRelayFeed(url), feed)
|
||||||
$: rating = getAvgRating(reviews)
|
$: rating = getAvgRating(reviews)
|
||||||
|
|
||||||
const relay = deriveRelay(url)
|
const relay = deriveRelay(url)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {tagFeed} from "@welshman/feeds"
|
import {makeTagFeed} from "@welshman/feeds"
|
||||||
import Feed from "src/app/shared/Feed.svelte"
|
import Feed from "src/app/shared/Feed.svelte"
|
||||||
import Heading from "src/partials/Heading.svelte"
|
import Heading from "src/partials/Heading.svelte"
|
||||||
import TopicActions from "src/app/shared/TopicActions.svelte"
|
import TopicActions from "src/app/shared/TopicActions.svelte"
|
||||||
@ -13,4 +13,4 @@
|
|||||||
<TopicActions {topic} />
|
<TopicActions {topic} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Feed feed={tagFeed("#t", topic)} />
|
<Feed feed={makeTagFeed("#t", topic)} />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {assocPath, uniq} from "ramda"
|
import {assocPath, uniq} from "ramda"
|
||||||
import {seconds} from "hurdak"
|
import {seconds} from "hurdak"
|
||||||
import {now} from "@welshman/lib"
|
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 {sessions} from "src/engine/session/state"
|
||||||
import {session} from "src/engine/session/derived"
|
import {session} from "src/engine/session/derived"
|
||||||
import {loadPubkeys, subscribe} from "src/engine/network/utils"
|
import {loadPubkeys, subscribe} from "src/engine/network/utils"
|
||||||
@ -21,9 +21,9 @@ export const loadAllMessages = ({reload = false} = {}) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loader = loadAll(
|
const loader = loadAll(
|
||||||
intersectionFeed(
|
makeIntersectionFeed(
|
||||||
relayFeed(...hints.User().getUrls()),
|
makeRelayFeed(...hints.User().getUrls()),
|
||||||
unionFeed(
|
makeUnionFeed(
|
||||||
feedFromFilter({kinds: [4], authors: [pubkey], since}),
|
feedFromFilter({kinds: [4], authors: [pubkey], since}),
|
||||||
feedFromFilter({kinds: [4, 1059], "#p": [pubkey], since}),
|
feedFromFilter({kinds: [4, 1059], "#p": [pubkey], since}),
|
||||||
),
|
),
|
||||||
|
@ -5,9 +5,9 @@ import type {LoadOpts} from "@welshman/feeds"
|
|||||||
import {
|
import {
|
||||||
FeedLoader,
|
FeedLoader,
|
||||||
Scope,
|
Scope,
|
||||||
relayFeed,
|
makeRelayFeed,
|
||||||
intersectionFeed,
|
makeIntersectionFeed,
|
||||||
unionFeed,
|
makeUnionFeed,
|
||||||
feedFromFilter,
|
feedFromFilter,
|
||||||
} from "@welshman/feeds"
|
} from "@welshman/feeds"
|
||||||
import {giftWrapKinds, generatePrivateKey} from "src/util/nostr"
|
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 sk = generatePrivateKey()
|
||||||
const event = await dvmRequest({kind, tags, relays, sk, timeout: 3000})
|
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
|
return pubkeys.length === 0 ? env.get().DEFAULT_FOLLOWS : pubkeys
|
||||||
},
|
},
|
||||||
getPubkeysForWotRange: (min, max) => {
|
getPubkeysForWOTRange: (min, max) => {
|
||||||
const pubkeys = []
|
const pubkeys = []
|
||||||
const $user = user.get()
|
const $user = user.get()
|
||||||
const thresholdMin = maxWot.get() * min
|
const thresholdMin = maxWot.get() * min
|
||||||
@ -143,7 +143,10 @@ export const sync = (fromUrl, toUrl, filters) => {
|
|||||||
|
|
||||||
worker.addGlobalHandler(event => publish({event, relays: [toUrl]}))
|
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, {
|
return loadAll(feed, {
|
||||||
onEvent: e => worker.push(e as Event),
|
onEvent: e => worker.push(e as Event),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import {pushToMapKey} from "@welshman/lib"
|
import {pushToMapKey, clamp} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
Router,
|
Router,
|
||||||
normalizeRelayUrl as normalize,
|
normalizeRelayUrl as normalize,
|
||||||
@ -161,13 +161,13 @@ export const hints = new Router({
|
|||||||
const oneHour = 60 * oneMinute
|
const oneHour = 60 * oneMinute
|
||||||
const oneDay = 24 * oneHour
|
const oneDay = 24 * oneHour
|
||||||
const oneWeek = 7 * oneDay
|
const oneWeek = 7 * oneDay
|
||||||
|
const {count = 0, faults = []} = relays.key(url).get() || {}
|
||||||
const connection = NetworkContext.pool.get(url, {autoConnect: false})
|
const connection = NetworkContext.pool.get(url, {autoConnect: false})
|
||||||
|
|
||||||
// If we haven't connected, consult our relay record and see if there has
|
// 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,
|
// been a recent fault. If there has been, penalize the relay. If there have been several,
|
||||||
// don't use the relay.
|
// don't use the relay.
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
const faults = relays.key(url).get()?.faults || []
|
|
||||||
const lastFault = last(faults) || 0
|
const lastFault = last(faults) || 0
|
||||||
|
|
||||||
if (faults.filter(n => n > Date.now() - oneHour).length > 10) {
|
if (faults.filter(n => n > Date.now() - oneHour).length > 10) {
|
||||||
@ -192,7 +192,7 @@ export const hints = new Router({
|
|||||||
[ConnectionStatus.Closed]: 0.6,
|
[ConnectionStatus.Closed]: 0.6,
|
||||||
[ConnectionStatus.Slow]: 0.5,
|
[ConnectionStatus.Slow]: 0.5,
|
||||||
[ConnectionStatus.Ok]: 1,
|
[ConnectionStatus.Ok]: 1,
|
||||||
default: 0.5,
|
default: clamp([0.5, 1], count / 1000),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -8,28 +8,30 @@
|
|||||||
export let initialValue = null
|
export let initialValue = null
|
||||||
export let value = initialValue
|
export let value = initialValue
|
||||||
|
|
||||||
let prev = value
|
|
||||||
let date = value ? createLocalDate(value) : new Date()
|
|
||||||
|
|
||||||
const className = cx(
|
const className = cx(
|
||||||
$$props.class,
|
$$props.class,
|
||||||
"rounded-full shadow-inset py-2 px-4 w-full placeholder:text-neutral-400",
|
"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",
|
"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 => {
|
const setDate = d => {
|
||||||
try {
|
try {
|
||||||
value = formatDateAsLocalISODate(d).slice(0, 10)
|
setValue(formatDateAsLocalISODate(d).slice(0, 10))
|
||||||
date = d
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prev !== value) {
|
|
||||||
onChange?.(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
prev = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
@ -38,11 +40,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => setValue(null)
|
||||||
value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
$: value && setDate(date)
|
let date = toDate(value)
|
||||||
|
|
||||||
|
$: date ? setDate(date) : setValue(null)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cx(className, "relative")}>
|
<div class={cx(className, "relative")}>
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
|
||||||
|
export let inert = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Anchor
|
<Anchor
|
||||||
{...$$props}
|
{...$$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>
|
on:click>
|
||||||
<slot />
|
<slot />
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {fly} from "src/util/transition"
|
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
|
let popover
|
||||||
|
|
||||||
const removePadding = () => {
|
const removePadding = () => {
|
||||||
@ -36,9 +43,23 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</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
|
<div
|
||||||
class="absolute left-0 right-0 top-0 z-popover"
|
class="absolute left-0 right-0 top-0 z-popover"
|
||||||
|
class:top-0={position === "bottom"}
|
||||||
|
class:bottom-0={position === "top"}
|
||||||
bind:this={popover}
|
bind:this={popover}
|
||||||
transition:fly|local={{y: 20, duration: 200}}>
|
transition:fly|local={{y: 20, duration: 200}}>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
|
||||||
import {without} from "ramda"
|
import {without} from "ramda"
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
|
@ -13,4 +13,14 @@
|
|||||||
)
|
)
|
||||||
</script>
|
</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