mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-19 11:43:35 +00:00
Merge lists, feed tabs, and advanced search into one interface
This commit is contained in:
parent
8f5ef24cec
commit
80386a833c
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
# 0.2.32
|
# 0.2.32
|
||||||
|
|
||||||
- [x] Add note preview
|
- [x] Add note preview when composing
|
||||||
|
- [x] Merge advanced search, feed options, and lists
|
||||||
|
|
||||||
# 0.2.31
|
# 0.2.31
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Current
|
# Current
|
||||||
|
|
||||||
- [ ] Clean up lists/advanced search
|
- [ ] Support other list types
|
||||||
- [ ] Use vida to stream development
|
- [ ] Use vida to stream development
|
||||||
- [ ] Fix connection management stuff. Have GPT help
|
- [ ] Fix connection management stuff. Have GPT help
|
||||||
- [ ] Add preview proxy thing
|
- [ ] Add preview proxy thing
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Filter} from "nostr-tools"
|
import type {DynamicFilter} from "src/util/types"
|
||||||
import {onMount, onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {last, equals, partition, always, uniqBy, sortBy, prop} from "ramda"
|
import {last, equals, partition, always, uniqBy, sortBy, prop} from "ramda"
|
||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
import {quantify} from "hurdak/lib/hurdak"
|
import {quantify} from "hurdak/lib/hurdak"
|
||||||
import {fuzzy, createScroller, now, timedelta} from "src/util/misc"
|
import {fuzzy, createScroller, now, timedelta} from "src/util/misc"
|
||||||
import {asDisplayEvent, mergeFilter} from "src/util/nostr"
|
import {asDisplayEvent} from "src/util/nostr"
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
import Modal from "src/partials/Modal.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
@ -16,11 +16,11 @@
|
|||||||
import Note from "src/app/shared/Note.svelte"
|
import Note from "src/app/shared/Note.svelte"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import {getUserReadRelays} from "src/agent/relays"
|
import {sampleRelays, getAllPubkeyWriteRelays} from "src/agent/relays"
|
||||||
import {mergeParents} from "src/app/state"
|
import {mergeParents, compileFilter} from "src/app/state"
|
||||||
|
|
||||||
export let filter = {} as Filter
|
export let relays = null
|
||||||
export let relays = getUserReadRelays()
|
export let filter = {} as DynamicFilter
|
||||||
export let delta = timedelta(6, "hours")
|
export let delta = timedelta(6, "hours")
|
||||||
export let shouldDisplay = always(true)
|
export let shouldDisplay = always(true)
|
||||||
export let parentsTimeout = 500
|
export let parentsTimeout = 500
|
||||||
@ -121,14 +121,17 @@
|
|||||||
let p = Promise.resolve()
|
let p = Promise.resolve()
|
||||||
|
|
||||||
// If we have a search term we need to use only relays that support search
|
// If we have a search term we need to use only relays that support search
|
||||||
const getRelays = () => (filter.search ? [{url: "wss://relay.nostr.band"}] : relays)
|
const getRelays = () =>
|
||||||
|
filter.search
|
||||||
|
? [{url: "wss://relay.nostr.band"}]
|
||||||
|
: sampleRelays(relays || getAllPubkeyWriteRelays(compileFilter(filter).authors || []))
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
const _key = key
|
const _key = key
|
||||||
|
|
||||||
// Wait for this page to load before trying again
|
// Wait for this page to load before trying again
|
||||||
await cursor.loadPage({
|
await cursor.loadPage({
|
||||||
filter,
|
filter: compileFilter(filter),
|
||||||
onChunk: chunk => {
|
onChunk: chunk => {
|
||||||
// Stack promises to avoid too many concurrent subscriptions
|
// Stack promises to avoid too many concurrent subscriptions
|
||||||
p = p.then(() => key === _key && onChunk(chunk))
|
p = p.then(() => key === _key && onChunk(chunk))
|
||||||
@ -136,7 +139,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = () => {
|
export const stop = () => {
|
||||||
notes = []
|
notes = []
|
||||||
notesBuffer = []
|
notesBuffer = []
|
||||||
scroller?.stop()
|
scroller?.stop()
|
||||||
@ -145,19 +148,21 @@
|
|||||||
key = Math.random()
|
key = Math.random()
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = (newFilter = {}) => {
|
export const start = (newFilter = null) => {
|
||||||
if (!equals(newFilter, filter)) {
|
if (!equals(newFilter, filter)) {
|
||||||
stop()
|
stop()
|
||||||
|
|
||||||
const _key = key
|
const _key = key
|
||||||
|
|
||||||
filter = {...filter, ...newFilter}
|
if (newFilter) {
|
||||||
|
filter = newFilter
|
||||||
|
}
|
||||||
|
|
||||||
// No point in subscribing if we have an end date
|
// No point in subscribing if we have an end date
|
||||||
if (!filter.until) {
|
if (!filter.until) {
|
||||||
sub = network.listen({
|
sub = network.listen({
|
||||||
relays: getRelays(),
|
relays: getRelays(),
|
||||||
filter: mergeFilter(filter, {since}),
|
filter: compileFilter({...filter, since}),
|
||||||
onChunk: chunk => {
|
onChunk: chunk => {
|
||||||
p = p.then(() => _key === key && onChunk(chunk))
|
p = p.then(() => _key === key && onChunk(chunk))
|
||||||
},
|
},
|
||||||
@ -178,7 +183,7 @@
|
|||||||
onDestroy(stop)
|
onDestroy(stop)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Content size="inherit">
|
<Content size="inherit" gap="gap-6">
|
||||||
{#if notesBuffer.length > 0}
|
{#if notesBuffer.length > 0}
|
||||||
<div class="pointer-events-none fixed left-0 top-0 z-10 mt-20 flex w-full justify-center">
|
<div class="pointer-events-none fixed left-0 top-0 z-10 mt-20 flex w-full justify-center">
|
||||||
<button
|
<button
|
||||||
@ -195,7 +200,9 @@
|
|||||||
{#if !hideControls}
|
{#if !hideControls}
|
||||||
<div class="flex justify-between gap-4" in:fly={{y: 20}}>
|
<div class="flex justify-between gap-4" in:fly={{y: 20}}>
|
||||||
<FilterSummary {filter} />
|
<FilterSummary {filter} />
|
||||||
<FeedAdvanced {filter} onChange={start} />
|
<FeedAdvanced {filter} onChange={start}>
|
||||||
|
<slot name="controls" slot="controls" />
|
||||||
|
</FeedAdvanced>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Filter} from "nostr-tools"
|
import type {DynamicFilter} from "src/util/types"
|
||||||
import {pluck, objOf} from "ramda"
|
import {pluck, objOf} from "ramda"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {createLocalDate} from "src/util/misc"
|
import {createLocalDate} from "src/util/misc"
|
||||||
@ -7,118 +7,157 @@
|
|||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
import Modal from "src/partials/Modal.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
|
import SelectButton from "src/partials/SelectButton.svelte"
|
||||||
import MultiSelect from "src/partials/MultiSelect.svelte"
|
import MultiSelect from "src/partials/MultiSelect.svelte"
|
||||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||||
import {searchTopics, searchPeople, getPersonWithFallback} from "src/agent/db"
|
import {searchTopics, searchPeople, getPersonWithFallback} from "src/agent/db"
|
||||||
|
|
||||||
|
export let filter
|
||||||
export let onChange
|
export let onChange
|
||||||
export let filter = {} as Filter
|
|
||||||
|
|
||||||
let _filter = {
|
const applyFilter = () => {
|
||||||
since: filter.since,
|
const newFilter = {} as DynamicFilter
|
||||||
until: filter.since,
|
|
||||||
search: filter.search || "",
|
if (_filter.since) {
|
||||||
authors: (filter.authors || []).map(getPersonWithFallback),
|
newFilter.since = createLocalDate(_filter.since).setHours(23, 59, 59, 0) / 1000
|
||||||
"#t": (filter["#t"] || []).map(objOf("name")),
|
}
|
||||||
"#p": (filter["#p"] || []).map(getPersonWithFallback),
|
|
||||||
|
if (_filter.until) {
|
||||||
|
newFilter.until = createLocalDate(_filter.until).setHours(23, 59, 59, 0) / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_filter.search) {
|
||||||
|
newFilter.search = _filter.search
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_filter["#t"].length > 0) {
|
||||||
|
newFilter["#t"] = pluck("name", _filter["#t"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_filter["#p"].length > 0) {
|
||||||
|
newFilter["#p"] = pluck("pubkey", _filter["#p"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(_filter.authors)) {
|
||||||
|
newFilter.authors = _filter.authors.length > 0 ? pluck("pubkey", _filter.authors) : "global"
|
||||||
|
} else {
|
||||||
|
newFilter.authors = _filter.authors
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal = null
|
const applySearch = debounce(200, applyFilter)
|
||||||
|
|
||||||
const applyFilter = debounce(300, () => {
|
const clearSearch = () => {
|
||||||
if (modal !== "maxi") {
|
_filter.search = ""
|
||||||
const newFilter = {} as Filter
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
if (_filter.since)
|
const onScopeChange = scope => {
|
||||||
newFilter.since = createLocalDate(_filter.since).setHours(23, 59, 59, 0) / 1000
|
_filter = {..._filter, authors: scope === "custom" ? [] : scope}
|
||||||
if (_filter.until)
|
}
|
||||||
newFilter.until = createLocalDate(_filter.until).setHours(23, 59, 59, 0) / 1000
|
|
||||||
if (_filter.authors.length > 0) newFilter.authors = pluck("pubkey", _filter.authors)
|
|
||||||
if (_filter.search) newFilter.search = _filter.search
|
|
||||||
if (_filter["#t"].length > 0) newFilter["#t"] = pluck("name", _filter["#t"])
|
|
||||||
if (_filter["#p"].length > 0) newFilter["#p"] = pluck("pubkey", _filter["#p"])
|
|
||||||
|
|
||||||
onChange(newFilter)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
modal = "maxi"
|
modal = "maxi"
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
applyFilter()
|
|
||||||
onEscape()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEscape = () => {
|
const onEscape = () => {
|
||||||
modal = null
|
modal = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
console.log("====")
|
||||||
|
onEscape()
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
let modal = null
|
||||||
|
let _filter = {
|
||||||
|
since: filter.since,
|
||||||
|
until: filter.since,
|
||||||
|
search: filter.search || "",
|
||||||
|
authors: Array.isArray(filter.authors)
|
||||||
|
? filter.authors.map(getPersonWithFallback)
|
||||||
|
: filter.authors || "network",
|
||||||
|
"#t": (filter["#t"] || []).map(objOf("name")),
|
||||||
|
"#p": (filter["#p"] || []).map(getPersonWithFallback),
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-end gap-3 p-2">
|
<div class="flex justify-end gap-1">
|
||||||
<i
|
<i
|
||||||
class="fa fa-search cursor-pointer"
|
class="fa fa-search cursor-pointer p-2"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
modal = modal ? null : "mini"
|
modal = modal ? null : "mini"
|
||||||
}} />
|
}} />
|
||||||
<i class="fa fa-sliders cursor-pointer" on:click={open} />
|
<i class="fa fa-sliders cursor-pointer p-2" on:click={open} />
|
||||||
|
<slot name="controls" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if modal}
|
{#if modal}
|
||||||
<Modal {onEscape} mini={modal === "mini"}>
|
<Modal {onEscape} mini={modal === "mini"}>
|
||||||
<Content size="lg">
|
<form on:submit|preventDefault={submit}>
|
||||||
<div class="flex flex-col gap-1">
|
<Content size="lg">
|
||||||
<strong>Search</strong>
|
<div class="flex flex-col gap-1">
|
||||||
<Input autofocus bind:value={_filter.search} on:input={applyFilter}>
|
<strong>Search</strong>
|
||||||
<i slot="before" class="fa fa-search" />
|
<Input autofocus bind:value={_filter.search} on:input={applySearch}>
|
||||||
</Input>
|
<i slot="before" class="fa fa-search" />
|
||||||
</div>
|
<i slot="after" class="fa fa-times cursor-pointer" on:click={clearSearch} />
|
||||||
{#if modal === "maxi"}
|
</Input>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
{#if modal === "maxi"}
|
||||||
<strong>Since</strong>
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<Input type="date" bind:value={_filter.since} />
|
<div class="flex flex-col gap-1">
|
||||||
|
<strong>Since</strong>
|
||||||
|
<Input type="date" bind:value={_filter.since} />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<strong>Until</strong>
|
||||||
|
<Input type="date" bind:value={_filter.until} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<strong>Until</strong>
|
<strong>Authors</strong>
|
||||||
<Input type="date" bind:value={_filter.until} />
|
<SelectButton
|
||||||
|
onChange={onScopeChange}
|
||||||
|
value={typeof _filter.authors === "string" ? _filter.authors : "custom"}
|
||||||
|
options={["follows", "network", "global", "custom"]} />
|
||||||
|
{#if Array.isArray(_filter.authors)}
|
||||||
|
<MultiSelect search={$searchPeople} bind:value={_filter.authors}>
|
||||||
|
<div slot="item" let:item>
|
||||||
|
<div class="-my-1">
|
||||||
|
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MultiSelect>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex flex-col gap-1">
|
<strong>Topics</strong>
|
||||||
<strong>Authors</strong>
|
<MultiSelect search={$searchTopics} bind:value={_filter["#t"]}>
|
||||||
<MultiSelect search={$searchPeople} bind:value={_filter.authors}>
|
<div slot="item" let:item>
|
||||||
<div slot="item" let:item>
|
<div class="-my-1">
|
||||||
<div class="-my-1">
|
#{item.name}
|
||||||
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MultiSelect>
|
||||||
</MultiSelect>
|
</div>
|
||||||
</div>
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex flex-col gap-1">
|
<strong>Mentions</strong>
|
||||||
<strong>Topics</strong>
|
<MultiSelect search={$searchPeople} bind:value={_filter["#p"]}>
|
||||||
<MultiSelect search={$searchTopics} bind:value={_filter["#t"]}>
|
<div slot="item" let:item>
|
||||||
<div slot="item" let:item>
|
<div class="-my-1">
|
||||||
<div class="-my-1">
|
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
||||||
#{item.name}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MultiSelect>
|
||||||
</MultiSelect>
|
</div>
|
||||||
</div>
|
<div class="flex justify-end">
|
||||||
<div class="flex flex-col gap-1">
|
<Anchor type="button-accent" on:click={submit}>Apply Filters</Anchor>
|
||||||
<strong>Mentions</strong>
|
</div>
|
||||||
<MultiSelect search={$searchPeople} bind:value={_filter["#p"]}>
|
{/if}
|
||||||
<div slot="item" let:item>
|
</Content>
|
||||||
<div class="-my-1">
|
</form>
|
||||||
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MultiSelect>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<Anchor type="button-accent" on:click={submit}>Apply Filters</Anchor>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Content>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -16,18 +16,9 @@
|
|||||||
const getFilterParts = f => {
|
const getFilterParts = f => {
|
||||||
const parts = []
|
const parts = []
|
||||||
|
|
||||||
if (filter.since && filter.until) {
|
if (typeof filter.authors === "string") {
|
||||||
const since = formatTimestampAsDate(filter.since)
|
parts.push(`From ${filter.authors}`)
|
||||||
const until = formatTimestampAsDate(filter.until)
|
} else if (filter.authors?.length > 0) {
|
||||||
|
|
||||||
parts.push(`Between ${since} and ${until}`)
|
|
||||||
} else if (filter.since) {
|
|
||||||
parts.push(`After ${formatTimestampAsDate(filter.since)}`)
|
|
||||||
} else if (filter.until) {
|
|
||||||
parts.push(`Before ${formatTimestampAsDate(filter.until)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.authors?.length > 0) {
|
|
||||||
parts.push(`By ${displayPeople(filter.authors)}`)
|
parts.push(`By ${displayPeople(filter.authors)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +34,17 @@
|
|||||||
parts.push(`Matching ${filter.search}`)
|
parts.push(`Matching ${filter.search}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter.since && filter.until) {
|
||||||
|
const since = formatTimestampAsDate(filter.since)
|
||||||
|
const until = formatTimestampAsDate(filter.until)
|
||||||
|
|
||||||
|
parts.push(`Between ${since} and ${until}`)
|
||||||
|
} else if (filter.since) {
|
||||||
|
parts.push(`After ${formatTimestampAsDate(filter.since)}`)
|
||||||
|
} else if (filter.until) {
|
||||||
|
parts.push(`Before ${formatTimestampAsDate(filter.until)}`)
|
||||||
|
}
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type {DisplayEvent} from "src/util/types"
|
import type {Filter} from "nostr-tools"
|
||||||
|
import type {DisplayEvent, DynamicFilter} from "src/util/types"
|
||||||
import Bugsnag from "@bugsnag/js"
|
import Bugsnag from "@bugsnag/js"
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import {navigate} from "svelte-routing"
|
import {navigate} from "svelte-routing"
|
||||||
@ -7,7 +8,7 @@ import {writable} from "svelte/store"
|
|||||||
import {max, omit, pluck, sortBy, find, slice, propEq} from "ramda"
|
import {max, omit, pluck, sortBy, find, slice, propEq} from "ramda"
|
||||||
import {createMap, doPipe, first} from "hurdak/lib/hurdak"
|
import {createMap, doPipe, first} from "hurdak/lib/hurdak"
|
||||||
import {warn} from "src/util/logger"
|
import {warn} from "src/util/logger"
|
||||||
import {hash, sleep, clamp} from "src/util/misc"
|
import {hash, shuffle, sleep, clamp} from "src/util/misc"
|
||||||
import {now, timedelta} from "src/util/misc"
|
import {now, timedelta} from "src/util/misc"
|
||||||
import {Tags, isNotification, userKinds} from "src/util/nostr"
|
import {Tags, isNotification, userKinds} from "src/util/nostr"
|
||||||
import {findReplyId} from "src/util/nostr"
|
import {findReplyId} from "src/util/nostr"
|
||||||
@ -18,7 +19,7 @@ import keys from "src/agent/keys"
|
|||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
import {getUserReadRelays, getUserRelays} from "src/agent/relays"
|
import {getUserReadRelays, getUserRelays} from "src/agent/relays"
|
||||||
import {getUserFollows} from "src/agent/social"
|
import {getUserFollows, getUserNetwork} from "src/agent/social"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
// Routing
|
// Routing
|
||||||
@ -83,10 +84,6 @@ export const logUsage = async name => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feed
|
|
||||||
|
|
||||||
export const feedsTab = writable("Follows")
|
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export const newNotifications = derived(
|
export const newNotifications = derived(
|
||||||
@ -289,3 +286,17 @@ export const publishWithToast = (relays, thunk) =>
|
|||||||
|
|
||||||
toast.show("info", message, pending.size ? null : 5)
|
toast.show("info", message, pending.size ? null : 5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Feeds
|
||||||
|
|
||||||
|
export const compileFilter = (filter: DynamicFilter): Filter => {
|
||||||
|
if (filter.authors === "global") {
|
||||||
|
filter = omit(["authors"], filter)
|
||||||
|
} else if (filter.authors === "follows") {
|
||||||
|
filter = {...filter, authors: shuffle(getUserFollows()).slice(0, 256)}
|
||||||
|
} else if (filter.authors === "network") {
|
||||||
|
filter = {...filter, authors: shuffle(getUserNetwork()).slice(0, 256)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
@ -1,63 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {prop, indexBy, objOf, filter as _filter} from "ramda"
|
import type {DynamicFilter} from "src/util/types"
|
||||||
import {shuffle} from "src/util/misc"
|
import {prop, indexBy, objOf} from "ramda"
|
||||||
import {Tags} from "src/util/nostr"
|
import {Tags} from "src/util/nostr"
|
||||||
import {modal, theme} from "src/partials/state"
|
import {modal, theme} from "src/partials/state"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Tabs from "src/partials/Tabs.svelte"
|
|
||||||
import Popover from "src/partials/Popover.svelte"
|
import Popover from "src/partials/Popover.svelte"
|
||||||
import Feed from "src/app/shared/Feed.svelte"
|
import Feed from "src/app/shared/Feed.svelte"
|
||||||
import {getUserFollows, getUserNetwork} from "src/agent/social"
|
|
||||||
import {sampleRelays, getAllPubkeyWriteRelays, getUserReadRelays} from "src/agent/relays"
|
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {feedsTab} from "src/app/state"
|
|
||||||
|
|
||||||
const {lists, canPublish} = user
|
const {lists} = user
|
||||||
const defaultTabs = ["Follows", "Network"]
|
|
||||||
|
|
||||||
let relays, filter
|
let relays = null
|
||||||
|
let filter = {kinds: [1, 1985], authors: "follows"} as DynamicFilter
|
||||||
|
let key = Math.random()
|
||||||
|
|
||||||
$: listsByName = indexBy(l => Tags.from(l).getMeta("d"), $lists)
|
$: listsByName = indexBy(l => Tags.from(l).getMeta("d"), $lists)
|
||||||
$: allTabs = defaultTabs.concat(Object.keys(listsByName))
|
|
||||||
$: $feedsTab = allTabs.includes($feedsTab) ? $feedsTab : defaultTabs[0]
|
|
||||||
$: visibleTabs = defaultTabs.includes($feedsTab) ? defaultTabs : [defaultTabs[0], $feedsTab]
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if ($feedsTab === "Follows") {
|
|
||||||
const authors = shuffle(getUserFollows()).slice(0, 256)
|
|
||||||
|
|
||||||
filter = {authors}
|
|
||||||
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
|
|
||||||
} else if ($feedsTab === "Network") {
|
|
||||||
const authors = shuffle(getUserNetwork()).slice(0, 256)
|
|
||||||
|
|
||||||
filter = {authors}
|
|
||||||
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
|
|
||||||
} else {
|
|
||||||
const list = listsByName[$feedsTab]
|
|
||||||
const tags = Tags.from(list)
|
|
||||||
const authors = tags.type("p").values().all()
|
|
||||||
const topics = tags.type("t").values().all()
|
|
||||||
const urls = tags.type("r").values().all()
|
|
||||||
|
|
||||||
filter = _filter(prop("length"), {authors, "#t": topics})
|
|
||||||
relays = urls.length > 0 ? urls.map(objOf("url")) : sampleRelays(getUserReadRelays())
|
|
||||||
}
|
|
||||||
|
|
||||||
filter = {...filter, kinds: [1, 1985]}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setActiveTab = tab => {
|
|
||||||
$feedsTab = tab
|
|
||||||
}
|
|
||||||
|
|
||||||
const showLists = () => {
|
const showLists = () => {
|
||||||
modal.push({type: "list/list"})
|
modal.push({type: "list/list"})
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = $feedsTab
|
const loadListFeed = name => {
|
||||||
|
const list = $lists.find(l => Tags.from(l).getMeta("d") === name)
|
||||||
|
const authors = Tags.from(list).type("p").values().all()
|
||||||
|
const topics = Tags.from(list).type("t").values().all()
|
||||||
|
const urls = Tags.from(list).type("r").values().all()
|
||||||
|
|
||||||
|
if (urls.length > 0) {
|
||||||
|
relays = urls.map(objOf("url"))
|
||||||
|
}
|
||||||
|
|
||||||
|
filter = {kinds: [1, 1985], authors: 'global'}
|
||||||
|
|
||||||
|
if (authors.length > 0) {
|
||||||
|
filter = {...filter, authors}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topics.length > 0) {
|
||||||
|
filter = {...filter, '#t': topics}
|
||||||
|
}
|
||||||
|
|
||||||
|
key = Math.random()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = "Feeds"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
@ -69,44 +57,41 @@
|
|||||||
</p>
|
</p>
|
||||||
</Content>
|
</Content>
|
||||||
{/if}
|
{/if}
|
||||||
<Tabs tabs={visibleTabs} activeTab={$feedsTab} {setActiveTab}>
|
{#key key}
|
||||||
{#if $canPublish}
|
<Feed {filter} {relays}>
|
||||||
{#if $lists.length > 0}
|
<div slot="controls">
|
||||||
<Popover placement="bottom" opts={{hideOnClick: true}} theme="transparent">
|
{#if $lists.length > 0}
|
||||||
<i slot="trigger" class="fa fa-ellipsis-v cursor-pointer p-2" />
|
<Popover placement="bottom" opts={{hideOnClick: true}} theme="transparent">
|
||||||
<div
|
<i slot="trigger" class="fa fa-ellipsis-v cursor-pointer p-2" />
|
||||||
slot="tooltip"
|
<div
|
||||||
class="flex flex-col items-start overflow-hidden rounded border border-solid border-gray-8 bg-black">
|
slot="tooltip"
|
||||||
{#each $lists as e (e.id)}
|
class="flex flex-col items-start overflow-hidden rounded border border-solid border-gray-8 bg-black">
|
||||||
{@const meta = Tags.from(e).asMeta()}
|
{#each $lists as e (e.id)}
|
||||||
|
{@const meta = Tags.from(e).asMeta()}
|
||||||
|
<button
|
||||||
|
class={cx("w-full py-2 px-3 text-left transition-colors", {
|
||||||
|
"hover:bg-gray-7": $theme === "dark",
|
||||||
|
"hover:bg-gray-1": $theme === "light",
|
||||||
|
})}
|
||||||
|
on:click={() => loadListFeed(meta.d)}>
|
||||||
|
<i class="fa fa-scroll fa-sm mr-1" />
|
||||||
|
{meta.d}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
<button
|
<button
|
||||||
|
on:click={showLists}
|
||||||
class={cx("w-full py-2 px-3 text-left transition-colors", {
|
class={cx("w-full py-2 px-3 text-left transition-colors", {
|
||||||
"hover:bg-gray-7": $theme === "dark",
|
"hover:bg-gray-7": $theme === "dark",
|
||||||
"hover:bg-gray-1": $theme === "light",
|
"hover:bg-gray-1": $theme === "light",
|
||||||
})}
|
})}>
|
||||||
on:click={() => {
|
<i class="fa fa-cog fa-sm mr-1" /> Customize
|
||||||
$feedsTab = meta.d
|
|
||||||
}}>
|
|
||||||
<i class="fa fa-scroll fa-sm mr-1" />
|
|
||||||
{meta.d}
|
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
</div>
|
||||||
<button
|
</Popover>
|
||||||
on:click={showLists}
|
{:else}
|
||||||
class={cx("w-full py-2 px-3 text-left transition-colors", {
|
<i class="fa fa-ellipsis-v cursor-pointer p-1" on:click={showLists} />
|
||||||
"hover:bg-gray-7": $theme === "dark",
|
{/if}
|
||||||
"hover:bg-gray-1": $theme === "light",
|
</div>
|
||||||
})}>
|
</Feed>
|
||||||
<i class="fa fa-cog fa-sm mr-1" /> Customize
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
{:else}
|
|
||||||
<i class="fa fa-ellipsis-v cursor-pointer p-1" on:click={showLists} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</Tabs>
|
|
||||||
{#key $feedsTab}
|
|
||||||
<Feed {relays} {filter} />
|
|
||||||
{/key}
|
{/key}
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -3,19 +3,21 @@
|
|||||||
|
|
||||||
export let options
|
export let options
|
||||||
export let value
|
export let value
|
||||||
|
export let onChange = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="inline-block">
|
<div class="inline-block">
|
||||||
<div class="flex cursor-pointer rounded border border-solid border-gray-1">
|
<div class="flex cursor-pointer overflow-hidden rounded-full border border-solid border-gray-1">
|
||||||
{#each options as option, i}
|
{#each options as option, i}
|
||||||
<div
|
<div
|
||||||
class={cx("px-4 py-2 transition-all", {
|
class={cx("px-4 py-2 transition-all", {
|
||||||
"border-l border-solid border-gray-1": i > 0,
|
"border-l border-solid border-gray-1": i > 0,
|
||||||
"bg-accent": value === option,
|
"bg-accent text-white": value === option,
|
||||||
})}
|
})}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
value = option
|
value = option
|
||||||
|
onChange?.(value)
|
||||||
}}>
|
}}>
|
||||||
{option}
|
{option}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type {Event} from "nostr-tools"
|
import type {Event, Filter} from "nostr-tools"
|
||||||
|
|
||||||
export type Relay = {
|
export type Relay = {
|
||||||
url: string
|
url: string
|
||||||
@ -37,3 +37,7 @@ export type Room = {
|
|||||||
about?: string
|
about?: string
|
||||||
picture?: string
|
picture?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DynamicFilter = Filter & {
|
||||||
|
authors?: string | string[]
|
||||||
|
} & Record<string, any>
|
||||||
|
Loading…
Reference in New Issue
Block a user