Add buttons to add relays, people, and topics to feeds

This commit is contained in:
Jonathan Staab 2023-04-13 17:37:27 -05:00
parent 51b7de9c29
commit 54553bbbc8
20 changed files with 257 additions and 159 deletions

View File

@ -4,10 +4,12 @@
- [x] Improve topic suggestions and rendering
- [x] Add topic search, keep cache of topics
- [x] Ability to create custom feeds
- [ ] Bookmark icon opens "create feed" dialog with form pre-filled
- [ ] Add ability to follow topics - bookmark icon?
- [ ] Claim relays bounty
- [x] Bookmark icon opens "create feed" dialog with form pre-filled
- [ ] Use lists instead of custom app data
- [ ] Public/private toggle
- [ ] Add person to feed button (maybe lists make more sense for this?)
- [ ] Add 30078 to personKinds (except we'll have to get more involved)
- [ ] Claim relays bounty
- [ ] Some lnurls aren't working npub1y3k2nheva29y9ej8a22e07epuxrn04rvgy28wvs54y57j7vsxxuq0gvp4j
- [ ] Global search modal that searches within current feed
- [ ] Fix force relays on login: http://localhost:5173/messages/npub1l66wvfm7dxhd6wmvpukpjpyhvwtlxzu0qqajqxjfpr4rlfa8hl5qlkfr3q

View File

@ -17,6 +17,8 @@
import PersonProfileInfo from "src/app/views/PersonProfileInfo.svelte"
import PersonShare from "src/app/views/PersonShare.svelte"
import TopicFeed from "src/app/views/TopicFeed.svelte"
import FeedList from "src/app/views/FeedList.svelte"
import FeedSelect from "src/app/views/FeedSelect.svelte"
import FeedEdit from "src/app/views/FeedEdit.svelte"
import RelayAdd from "src/app/views/RelayAdd.svelte"
@ -62,6 +64,10 @@
{#key m.topic}
<TopicFeed topic={m.topic} />
{/key}
{:else if m.type === "feed/list"}
<FeedList />
{:else if m.type === "feed/select"}
<FeedSelect key={m.key} value={m.value} />
{:else if m.type === "feed/edit"}
<FeedEdit feed={m.feed} />
{:else if m.type === "message"}

View File

@ -0,0 +1,11 @@
<script lang="ts">
import {quantify} from "hurdak/lib/hurdak"
export let feed
</script>
<p>
{feed.topics ? quantify(feed.topics.length, "topic") : ""}
{feed.authors ? quantify(feed.authors.length, "author") : ""}
{feed.relays ? quantify(feed.relays.length, "relay") : ""}
</p>

View File

@ -2,7 +2,6 @@
import {nip19} from "nostr-tools"
import {find, last} from "ramda"
import {onMount} from "svelte"
import {navigate} from "svelte-routing"
import {quantify} from "hurdak/lib/hurdak"
import {findRootId, findReplyId, displayPerson} from "src/util/nostr"
import {formatTimestamp} from "src/util/misc"
@ -19,7 +18,7 @@
import {getRelaysForEventParent} from "src/agent/relays"
import {getPersonWithFallback} from "src/agent/db"
import {watch} from "src/agent/db"
import {routes} from "src/app/state"
import {routes, goToPerson} from "src/app/state"
import NoteContent from "src/app/shared/NoteContent.svelte"
export let note
@ -64,14 +63,6 @@
}
}
const goToAuthor = () => {
if (document.querySelector(".modal-content")) {
navigate(routes.person(note.pubkey))
} else {
modal.push({type: "person/feed", pubkey: note.pubkey})
}
}
const goToParent = async () => {
const relays = getRelaysForEventParent(note)
@ -135,7 +126,7 @@
<Anchor
type="unstyled"
class="flex items-center gap-2 pr-16 text-lg font-bold"
on:click={goToAuthor}>
on:click={() => goToPerson($author.pubkey)}>
<span>{displayPerson($author)}</span>
{#if $author.verified_as}
<i class="fa fa-circle-check text-sm text-accent" />
@ -147,7 +138,7 @@
<Anchor
type="unstyled"
class="flex items-center gap-2 pr-16 text-lg font-bold"
on:click={goToAuthor}>
on:click={() => goToPerson($author.pubkey)}>
<span>{displayPerson($author)}</span>
{#if $author.verified_as}
<i class="fa fa-circle-check text-sm text-accent" />

View File

@ -1,6 +1,5 @@
<script lang="ts">
import {objOf, reverse} from "ramda"
import {navigate} from "svelte-routing"
import {fly} from "svelte/transition"
import {splice} from "hurdak/lib/hurdak"
import {warn} from "src/util/logger"
@ -15,7 +14,7 @@
import user from "src/agent/user"
import network from "src/agent/network"
import {getPersonWithFallback} from "src/agent/db"
import {routes} from "src/app/state"
import {goToPerson} from "src/app/state"
export let note
export let anchorId = null
@ -123,18 +122,18 @@
<br />
{/each}
{:else if type === "topic"}
<Anchor on:click={e => { e.stopPropagation(); openTopic(value) }}>#{value}</Anchor>
<Anchor stopPropagation on:click={() => openTopic(value)}>#{value}</Anchor>
{:else if type === "link"}
<Anchor external href={value}>
{value.replace(/https?:\/\/(www\.)?/, "")}
</Anchor>
{:else if type.startsWith("nostr:")}
{#if value.pubkey}
@<Anchor href={"/" + value.entity}>
@<Anchor stopPropagation on:click={() => goToPerson(value.pubkey)}>
{displayPerson(getPersonWithFallback(value.pubkey))}
</Anchor>
{:else}
<Anchor href={"/" + value.entity}>
<Anchor stopPropagation href={"/" + value.entity}>
{value.entity.slice(0, 16) + "..."}
</Anchor>
{/if}
@ -145,7 +144,7 @@
{/each}
</p>
{#if showMedia && links.length > 0}
<div on:click={e => e.stopPropagation()}>
<div on:click|stopPropagation>
<MediaSet {links} />
</div>
{/if}
@ -160,9 +159,10 @@
<div class="mb-4 flex items-center gap-4">
<PersonCircle size={6} {person} />
<Anchor
stopPropagation
type="unstyled"
class="flex items-center gap-2"
on:click={() => navigate(routes.person(quote.pubkey))}>
on:click={() => goToPerson(quote.pubkey)}>
<h2 class="text-lg">{displayPerson(person)}</h2>
</Anchor>
</div>

View File

@ -9,6 +9,7 @@
import {getPubkeyWriteRelays} from "src/agent/relays"
import user from "src/agent/user"
import pool from "src/agent/pool"
import {addToFeed} from "src/app/state"
export let person
@ -22,6 +23,12 @@
$: {
actions = []
actions.push({
onClick: () => addToFeed("authors", person.pubkey),
label: "Add to feed",
icon: "scroll",
})
actions.push({onClick: share, label: "Share", icon: "share-nodes"})
if (user.getPubkey() !== person.pubkey && $canPublish) {

View File

@ -3,6 +3,7 @@
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import user from "src/agent/user"
import {getRelayWithFallback} from "src/agent/db"
import {addToFeed} from "src/app/state"
export let relay
@ -30,6 +31,12 @@
})
}
actions.push({
onClick: () => addToFeed("relays", relay.url),
label: "Add to feed",
icon: "scroll",
})
if (relay.contact) {
actions.push({
onClick: () => window.open("mailto:" + last(relay.contact.split(":"))),

View File

@ -0,0 +1,18 @@
<script lang="ts">
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import {addToFeed} from "src/app/state"
export let topic
let actions = []
$: {
actions.push({
onClick: () => addToFeed("topics", topic),
label: "Add to feed",
icon: "scroll",
})
}
</script>
<OverflowMenu {actions} />

View File

@ -1,6 +1,7 @@
import type {DisplayEvent} from "src/util/types"
import Bugsnag from "@bugsnag/js"
import {nip19} from "nostr-tools"
import {navigate} from "svelte-routing"
import {derived} from "svelte/store"
import {writable} from "svelte/store"
import {omit, pluck, sortBy, max, find, slice, propEq} from "ramda"
@ -25,6 +26,16 @@ export const routes = {
person: (pubkey, tab = "notes") => `/people/${nip19.npubEncode(pubkey)}/${tab}`,
}
export const goToPerson = pubkey => {
if (document.querySelector(".modal-content")) {
navigate(routes.person(pubkey))
} else {
modal.push({type: "person/feed", pubkey})
}
}
export const addToFeed = (key, value) => modal.push({type: "feed/select", key, value})
// Menu
export const menuIsOpen = writable(false)

View File

@ -1,13 +1,13 @@
<script>
import {when, find, objOf, pluck, always, propEq} from "ramda"
import {when, find, pluck, always, propEq} from "ramda"
import {randomId} from "hurdak/lib/hurdak"
import {displayPerson} from "src/util/nostr"
import {modal, toast} from "src/partials/state"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import Button from "src/partials/Button.svelte"
import Input from "src/partials/Input.svelte"
import MultiSelect from "src/partials/MultiSelect.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {searchTopics, searchPeople, searchRelays, getPersonWithFallback} from "src/agent/db"
import user from "src/agent/user"
@ -16,13 +16,27 @@
let values = {
id: feed.id,
name: feed.name,
authors: (feed.authors || []).map(objOf("pubkey")),
topics: (feed.topics || []).map(objOf("name")),
relays: (feed.relays || []).map(objOf("url")),
params: (feed.authors || [])
.map(pubkey => ({pubkey}))
.concat((feed.topics || []).map(name => ({name})))
.concat((feed.relays || []).map(url => ({url}))),
}
const {feeds} = user
const search = q => {
if (q.startsWith("~")) {
console.log($searchRelays(q))
return $searchRelays(q)
}
if (q.startsWith("#")) {
return $searchTopics(q)
}
return $searchPeople(q)
}
const submit = () => {
if (!values.name) {
return toast.show("error", "A name is required for your feed")
@ -55,56 +69,28 @@
<Heading class="text-center">{values.id ? "Edit" : "Add"} custom feed</Heading>
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<strong>Feed name</strong>
<strong>Name</strong>
<Input bind:value={values.name} placeholder="My custom feed" />
<p class="text-sm text-gray-4">
Custom feeds are identified by their name, so this has to be unique.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Limit by author</strong>
<MultiSelect
search={$searchPeople}
bind:value={values.authors}
placeholder="A list of authors">
<strong>Filters</strong>
<MultiSelect {search} bind:value={values.params}>
<div slot="item" let:item>
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
{#if item.pubkey}
{displayPerson(getPersonWithFallback(item.pubkey))}
{:else if item.name}
#{item.name}
{:else if item.url}
{item.url}
{/if}
</div>
</MultiSelect>
<p class="text-sm text-gray-4">
Only notes by the given authors will be shown in the feed.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Limit by topic</strong>
<MultiSelect
search={$searchTopics}
bind:value={values.topics}
delimiters={[",", " "]}
termToItem={objOf("name")}
placeholder="A list of topics">
<div slot="item" let:item>
#{item.name}
</div>
</MultiSelect>
<p class="text-sm text-gray-4">
Only notes with the given topics will be shown in the feed.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Limit by relay</strong>
<MultiSelect
search={$searchRelays}
bind:value={values.relays}
delimiters={[",", " "]}
termToItem={objOf("url")}
placeholder="A list of relays">
<div slot="item" let:item>
{item.url}
</div>
</MultiSelect>
<p class="text-sm text-gray-4">
Only notes found on the given relays will be shown in the feed.
Type "@" to look for people, "#" to look for topics, and "~" to look for relays. Custom
feeds will search for notes that match any item within each filter.
</p>
</div>
<Button type="submit" class="text-center">Save</Button>

View File

@ -0,0 +1,52 @@
<script type="ts">
import {modal} from "src/partials/state"
import Heading from "src/partials/Heading.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import FeedSummary from 'src/app/shared/FeedSummary.svelte'
import user from "src/agent/user"
const {feeds} = user
const createFeed = () => {
modal.push({type: "feed/edit"})
}
const removeFeed = feed => {
user.removeFeed(feed.id)
}
const editFeed = feed => {
modal.push({type: "feed/edit", feed})
}
</script>
<Content>
<div class="flex items-center justify-between">
<Heading>Custom Feeds</Heading>
<Anchor type="button-accent" on:click={createFeed}>
<i class="fa fa-plus" /> Feed
</Anchor>
</div>
<p>
You custom feeds are listed below. You can create new custom feeds by handing using the "add
feed" button, or by clicking the <i class="fa fa-scroll px-1" /> icon that appears throughout
Coracle.
</p>
{#each $feeds as feed (feed.name)}
<div class="flex justify-start gap-3">
<i
class="fa fa-sm fa-trash cursor-pointer py-3"
on:click|stopPropagation={() => removeFeed(feed)} />
<div class="flex w-full justify-between">
<div>
<strong>{feed.name}</strong>
<FeedSummary {feed} />
</div>
<Anchor on:click={() => editFeed(feed)}>Edit</Anchor>
</div>
</div>
{:else}
<p class="text-center py-12">You don't have any custom feeds yet.</p>
{/each}
</Content>

View File

@ -0,0 +1,45 @@
<script type="ts">
import {uniq} from 'ramda'
import {modal} from "src/partials/state"
import Heading from "src/partials/Heading.svelte"
import Anchor from "src/partials/Anchor.svelte"
import BorderLeft from "src/partials/BorderLeft.svelte"
import Content from "src/partials/Content.svelte"
import FeedSummary from 'src/app/shared/FeedSummary.svelte'
import user from "src/agent/user"
export let key
export let value
const label = key.slice(0, -1)
const {feeds} = user
const modifyFeed = feed => {
return {...feed, [key]: uniq((feed[key] || []).concat(value))}
}
const selectFeed = feed => {
modal.pop()
modal.push({type: "feed/edit", feed: modifyFeed(feed)})
}
</script>
<Content size="lg">
<div class="flex items-center justify-between">
<Heading>Select a Feed</Heading>
<Anchor type="button-accent" on:click={() => selectFeed({})}>
<i class="fa fa-plus" /> Feed
</Anchor>
</div>
<p>
Select a feed to modify. The selected {label} will be added to it as an additional filter.
</p>
{#each $feeds as feed (feed.name)}
<BorderLeft on:click={() => selectFeed(feed)}>
<strong>{feed.name}</strong>
<FeedSummary {feed} />
</BorderLeft>
{:else}
<p class="text-center py-12">You don't have any custom feeds yet.</p>
{/each}
</Content>

View File

@ -1,13 +1,10 @@
<script lang="ts">
import type {CustomFeed} from "src/util/types"
import {prop, objOf, find, propEq} from "ramda"
import {quantify} from "hurdak/lib/hurdak"
import {shuffle, synced} from "src/util/misc"
import {modal} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte"
import Heading from "src/partials/Heading.svelte"
import Tabs from "src/partials/Tabs.svelte"
import Popover from "src/partials/Popover.svelte"
import Feed from "src/app/shared/Feed.svelte"
@ -21,9 +18,7 @@
{id: "network", name: "Network", authors: "network"},
] as Array<CustomFeed>
let modalIsOpen = false
let activeTab = synced("views/Feeds/activeTab", "Follows")
let actions = []
let relays, filter, tabs, feed
$: allFeeds = defaultFeeds.concat($feeds)
@ -79,34 +74,12 @@
})
}
$: {
actions = []
actions.push({
onClick: toggleModal,
label: "Customize",
icon: "cog",
})
}
const setActiveTab = tab => {
$activeTab = tab
}
const toggleModal = () => {
modalIsOpen = !modalIsOpen
}
const createFeed = () => {
modal.push({type: "feed/edit"})
}
const editFeed = feed => {
modal.push({type: "feed/edit", feed})
}
const removeFeed = feed => {
user.removeFeed(feed.id)
const showFeedsList = () => {
modal.push({type: "feed/list"})
}
document.title = $activeTab
@ -139,13 +112,13 @@
{feed.name}
</button>
{/each}
<button on:click={toggleModal} class="w-full py-2 px-3 text-left hover:bg-gray-7">
<button on:click={showFeedsList} class="w-full py-2 px-3 text-left hover:bg-gray-7">
<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={toggleModal} />
<i class="fa fa-ellipsis-v cursor-pointer p-1" on:click={showFeedsList} />
{/if}
</Tabs>
{#key $activeTab}
@ -153,41 +126,3 @@
{/key}
</div>
</Content>
{#if modalIsOpen}
<Modal onEscape={toggleModal}>
<Content>
<div class="flex items-center justify-between">
<Heading>Custom Feeds</Heading>
<Anchor type="button-accent" on:click={createFeed}>
<i class="fa fa-plus" /> Feed
</Anchor>
</div>
<p>
You custom feeds are listed below. You can create new custom feeds by handing using the "add
feed" button, or by clicking the <i class="fa fa-scroll px-1" /> icon that appears throughout
Coracle.
</p>
{#each $feeds as feed (feed.name)}
<div class="flex justify-start gap-3">
<i
class="fa fa-sm fa-trash cursor-pointer py-3"
on:click|stopPropagation={() => removeFeed(feed)} />
<div class="flex w-full justify-between">
<div>
<strong>{feed.name}</strong>
<p>
{feed.topics ? quantify(feed.topics.length, "topic") : ""}
{feed.authors ? quantify(feed.authors.length, "author") : ""}
{feed.relays ? quantify(feed.relays.length, "relay") : ""}
</p>
</div>
<Anchor on:click={() => editFeed(feed)}>Edit</Anchor>
</div>
</div>
{:else}
<p class="text-center py-12">You don't have any custom feeds yet.</p>
{/each}
</Content>
</Modal>
{/if}

View File

@ -5,7 +5,7 @@
import Input from "src/partials/Input.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import BorderLeft from "src/partials/BorderLeft.svelte"
import PersonInfo from "src/app/shared/PersonInfo.svelte"
import {watch} from "src/agent/db"
import user from "src/agent/user"
@ -48,13 +48,9 @@
</Input>
{#each $search(q).slice(0, 50) as result (result.type + result.id)}
{#if result.type === "topic"}
<Anchor
type="unstyled"
class="flex gap-4 border-l-2 border-solid border-gray-7 py-2 px-4 transition-all
hover:border-accent hover:bg-gray-8"
on:click={() => openTopic(result.topic.name)}>
<BorderLeft on:click={() => openTopic(result.topic.name)}>
#{result.topic.name}
</Anchor>
</BorderLeft>
{:else if result.type === "person"}
<PersonInfo person={result.person} />
{/if}

View File

@ -2,14 +2,21 @@
import Feed from "src/app/shared/Feed.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import TopicActions from "src/app/shared/TopicActions.svelte"
import {sampleRelays, getUserReadRelays} from "src/agent/relays"
export let topic
const relays = sampleRelays(getUserReadRelays())
const filter = [{kinds: [1], "#t": [topic]}]
</script>
<Content>
<Heading class="text-center">#{topic}</Heading>
<div class="flex justify-between gap-2">
<Heading>#{topic}</Heading>
<div class="pt-5">
<TopicActions {topic} />
</div>
</div>
<Feed {relays} {filter} />
</Content>

View File

@ -1,12 +1,16 @@
<script>
import cx from "classnames"
import {switcher} from "hurdak/lib/hurdak"
import {createEventDispatcher} from "svelte"
export let stopPropagation = false
export let external = false
export let loading = false
export let type = "anchor"
export let href = null
const dispatch = createEventDispatcher()
let className
$: className = cx(
@ -25,8 +29,16 @@
"py-2 px-4 rounded bg-accent text-white whitespace-nowrap border border-solid border-accent-light hover:bg-accent-light",
})
)
const onClick = e => {
if (stopPropagation) {
e.stopPropagation()
}
dispatch("click", e)
}
</script>
<a on:click {...$$props} {href} class={className} target={external ? "_blank" : null}>
<a on:click={onClick} {...$$props} {href} class={className} target={external ? "_blank" : null}>
<slot />
</a>

View File

@ -0,0 +1,11 @@
<script lang="ts">
import Anchor from 'src/partials/Anchor.svelte'
</script>
<Anchor
type="unstyled"
class="flex gap-4 border-l-2 border-solid border-gray-6 py-2 px-4 transition-all
hover:border-accent hover:bg-gray-8"
on:click>
<slot />
</Anchor>

View File

@ -61,25 +61,25 @@
}
</script>
<div
class="shadow-inset cursor-text rounded border border-solid border-gray-3 bg-input py-2 px-4 text-black"
on:click={() => input.focus()}>
<div class="text-sm">
{#each value as item}
<Chip class="mr-1 mb-1" theme="light" on:click={() => remove(item)}>
<Chip class="mr-1 mb-1" theme="dark" on:click={() => remove(item)}>
<slot name="item" {item}>
{item}
</slot>
</Chip>
{/each}
<input
type="text"
class="w-full bg-input py-2 outline-0 placeholder:text-gray-5"
{placeholder}
bind:value={term}
bind:this={input}
on:keydown={onKeyDown} />
</div>
<input
type="text"
class="shadow-inset w-full cursor-text rounded border border-solid border-gray-3 bg-input bg-input py-2
py-2 px-4 text-black outline-0 placeholder:text-gray-5"
{placeholder}
bind:value={term}
bind:this={input}
on:keydown={onKeyDown} />
{#if search}
<div class="relative w-full">
<div class="absolute w-full">

View File

@ -31,7 +31,7 @@
{#each actions as { label, icon, onClick }}
<div class="relative z-10 cursor-pointer" on:click={onClick}>
<span class="absolute right-0 mr-12 mt-2">{label}</span>
<span class="absolute right-0 mr-12 mt-2 whitespace-nowrap">{label}</span>
<Anchor type="button-circle"><i class={`fa fa-${icon}`} /></Anchor>
</div>
{/each}

View File

@ -6,7 +6,7 @@ import {tryJson} from "src/util/misc"
import {invoiceAmount} from "src/util/lightning"
export const personKinds = [0, 2, 3, 10001, 10002]
export const userKinds = personKinds.concat([10000])
export const userKinds = personKinds.concat([10000, 30001])
export class Tags {
tags: Array<any>
@ -192,7 +192,8 @@ export const parseContent = ({content, tags = []}) => {
const parseTopic = () => {
const topic = first(text.match(/^#\w+/i))
if (topic) {
// Skip numeric topics
if (topic && !topic.match(/^#\d+$/)) {
return ["topic", topic, topic.slice(1)]
}
}