Switch from custom data to lists

This commit is contained in:
Jonathan Staab 2023-04-14 11:09:27 -05:00
parent 54553bbbc8
commit 5b548cccab
27 changed files with 367 additions and 344 deletions

View File

@ -1,5 +1,13 @@
# Changelog
## 0.2.24
- [x] Replace localforage with loki.js for storage
- [x] Fix a bunch of bugs in content parsing
- [x] Add lists/custom feeds
- [x] Refactor component hiararchy
- [x] Re-work how modals stack
## 0.2.23
- [x] Fix modal scroll position for nested modals

View File

@ -5,11 +5,14 @@
- [x] Add topic search, keep cache of topics
- [x] Ability to create custom feeds
- [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)
- [ ] Replace some modals instead of pushing
- [ ] Test anonymous with lists
- [ ] Test hardcoded relay, currently you get asked to pick a relay if not logged in
- [ ] Claim relays bounty
- [ ] Fix notifications
- [ ] Queue context requests to avoid having too many concurrent subscriptions
- [ ] Advanced search
- Select timeframe, authors, p tags, t tags
- [ ] Some lnurls aren't working npub1y3k2nheva29y9ej8a22e07epuxrn04rvgy28wvs54y57j7vsxxuq0gvp4j
- [ ] Global search modal that searches within current feed
- [ ] Fix force relays on login: http://localhost:5173/messages/npub1l66wvfm7dxhd6wmvpukpjpyhvwtlxzu0qqajqxjfpr4rlfa8hl5qlkfr3q
@ -64,7 +67,7 @@
# UI/Features
- [ ] Remember message/chat status
- [ ] Linkify topics
- [ ] Allow sharing of lists/following other people's lists
- [ ] Add suggestion list for topics on compose
- [ ] Badges link to https://badges.page/p/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
- [ ] Add QR code that pre-fills follows and relays for a new user

View File

@ -35,8 +35,7 @@ const setPetnames = petnames => new PublishableEvent(3, {tags: petnames})
const setMutes = mutes => new PublishableEvent(10000, {tags: mutes})
const setFeeds = feeds =>
new PublishableEvent(30078, {content: JSON.stringify(feeds), tags: [["d", "coracle/feeds"]]})
const createList = list => new PublishableEvent(30001, {tags: list})
const createRoom = room =>
new PublishableEvent(40, {content: JSON.stringify(pick(roomAttrs, room))})
@ -202,7 +201,7 @@ export default {
setRelays,
setPetnames,
setMutes,
setFeeds,
createList,
createRoom,
updateRoom,
createChatMessage,

View File

@ -210,7 +210,7 @@ export const people = new Table("people", "pubkey", {
})
export const userEvents = new Table("userEvents", "id", {max: 2000, sort: sortByCreatedAt})
export const notifications = new Table("notifications", "id")
export const notifications = new Table("notifications", "id", {sort: sortByCreatedAt})
export const contacts = new Table("contacts", "pubkey")
export const rooms = new Table("rooms", "id")
export const relays = new Table("relays", "url")

View File

@ -1,4 +1,4 @@
import {uniq, nth, objOf, pick, identity} from "ramda"
import {uniq, prop, reject, nth, uniqBy, objOf, pick, identity} from "ramda"
import {nip05} from "nostr-tools"
import {noop, ensurePlural, chunk} from "hurdak/lib/hurdak"
import {
@ -222,11 +222,16 @@ addHandler(
)
addHandler(
30078,
profileHandler("feeds", (e, p) => {
if (Tags.from(e).type("d").values().first() === "coracle/feeds") {
return tryJson(() => JSON.parse(e.content))
}
30001,
profileHandler("lists", (e, p) => uniqBy(prop("id"), p.lists.concat(e)))
)
addHandler(
5,
profileHandler("lists", (e, p) => {
const ids = new Set(Tags.from(e).type("e").values().all())
return reject(e => ids.has(e.id), p.lists)
})
)

View File

@ -1,4 +1,4 @@
import type {CustomFeed, Relay} from "src/util/types"
import type {Relay, MyEvent} from "src/util/types"
import type {Readable} from "svelte/store"
import {
slice,
@ -37,14 +37,14 @@ const profile = synced("agent/user/profile", {
petnames: [],
relays: [],
mutes: [],
feeds: [],
lists: [],
})
const settings = derived(profile, prop("settings"))
const petnames = derived(profile, prop("petnames"))
const relays = derived(profile, prop("relays")) as Readable<Array<Relay>>
const mutes = derived(profile, prop("mutes")) as Readable<Array<[string, string]>>
const feeds = derived(profile, prop("feeds")) as Readable<Array<CustomFeed>>
const lists = derived(profile, prop("lists")) as Readable<Array<MyEvent>>
const canPublish = derived(
[keys.pubkey, relays],
@ -166,23 +166,20 @@ export default {
return this.updateMutes(reject(t => t[1] === pubkey))
},
// Feeds
// Lists
feeds,
getFeeds: () => profileCopy.feeds,
updateFeeds(f) {
const $feeds = f(profileCopy.feeds)
lists,
getLists: () => profileCopy.lists,
async putList(id, name, params, relays) {
const tags = [["d", name]].concat(params).concat(relays)
profile.update(assoc("feeds", $feeds))
if (keys.canSign()) {
return cmd.setFeeds($feeds).publish(profileCopy.relays)
if (id) {
await cmd.deleteEvent([id]).publish(profileCopy.relays)
}
await cmd.createList(tags).publish(profileCopy.relays)
},
addFeed(feed) {
return this.updateFeeds($feeds => $feeds.concat(feed))
},
removeFeed(id) {
return this.updateFeeds($feeds => reject(whereEq({id}), $feeds))
removeList(id) {
return cmd.deleteEvent([id]).publish(profileCopy.relays)
},
}

View File

@ -17,9 +17,9 @@
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 ListList from "src/app/views/ListList.svelte"
import ListSelect from "src/app/views/ListSelect.svelte"
import ListEdit from "src/app/views/ListEdit.svelte"
import RelayAdd from "src/app/views/RelayAdd.svelte"
const {stack} = modal
@ -64,12 +64,12 @@
{#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 === "list/list"}
<ListList />
{:else if m.type === "list/select"}
<ListSelect item={m.item} />
{:else if m.type === "list/edit"}
<ListEdit list={m.list} />
{:else if m.type === "message"}
<Content size="lg">
<div class="text-center">{m.message}</div>

View File

@ -1,11 +0,0 @@
<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

@ -0,0 +1,24 @@
<script lang="ts">
import {identity} from "ramda"
import {quantify} from "hurdak/lib/hurdak"
import {Tags} from "src/util/nostr"
export let list
const tags = Tags.from(list)
const topics = tags.type("t").all()
const authors = tags.type("p").all()
const relays = tags.type("r").all()
const summary = [
topics.length > 0 && quantify(topics.length, "topic"),
authors.length > 0 && quantify(authors.length, "author"),
]
.filter(identity)
.join(", ")
</script>
<p>
{summary}
{relays.length > 0 ? `on ${quantify(relays.length, "relay")}` : ""}
</p>

View File

@ -99,7 +99,7 @@
class="note-reply relative z-10 my-2 flex flex-col gap-1"
bind:this={container}
on:click|stopPropagation>
<div class={`border border-${borderColor} rounded border-solid`}>
<div class={`border border-${borderColor} border-solid rounded-2xl overflow-hidden`}>
<div class="bg-gray-7" class:rounded-b={data.mentions.length === 0}>
<Compose bind:this={reply} onSubmit={send}>
<button

View File

@ -9,7 +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"
import {addToList} from "src/app/state"
export let person
@ -24,8 +24,8 @@
actions = []
actions.push({
onClick: () => addToFeed("authors", person.pubkey),
label: "Add to feed",
onClick: () => addToList("p", person.pubkey),
label: "Add to list",
icon: "scroll",
})

View File

@ -3,7 +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"
import {addToList} from "src/app/state"
export let relay
@ -32,8 +32,8 @@
}
actions.push({
onClick: () => addToFeed("relays", relay.url),
label: "Add to feed",
onClick: () => addToList("r", relay.url),
label: "Add to list",
icon: "scroll",
})

View File

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

View File

@ -9,7 +9,7 @@ import {createMap, doPipe, first} from "hurdak/lib/hurdak"
import {warn} from "src/util/logger"
import {hash} from "src/util/misc"
import {synced, now, timedelta} from "src/util/misc"
import {Tags, isNotification} from "src/util/nostr"
import {Tags, isNotification, userKinds} from "src/util/nostr"
import {findReplyId} from "src/util/nostr"
import {modal, toast} from "src/partials/state"
import {notifications, watch, userEvents, contacts, rooms} from "src/agent/db"
@ -34,7 +34,7 @@ export const goToPerson = pubkey => {
}
}
export const addToFeed = (key, value) => modal.push({type: "feed/select", key, value})
export const addToList = (type, value) => modal.push({type: "list/select", item: {type, value}})
// Menu
@ -222,7 +222,7 @@ export const loadAppData = async pubkey => {
listen(pubkey)
// Make sure the user and their network is loaded
await network.loadPeople([pubkey], {force: true})
await network.loadPeople([pubkey], {force: true, kinds: userKinds})
await network.loadPeople(getUserFollows())
}
}

View File

@ -1,99 +0,0 @@
<script>
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 {searchTopics, searchPeople, searchRelays, getPersonWithFallback} from "src/agent/db"
import user from "src/agent/user"
export let feed = {}
let values = {
id: feed.id,
name: feed.name,
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")
}
if (find(f => f.id !== values.id && f.name === values.name, $feeds)) {
return toast.show("error", "That name is already in use")
}
const data = {
...values,
authors: values.authors.length > 0 ? pluck("pubkey", values.authors) : null,
topics: values.topics.length > 0 ? pluck("name", values.topics) : null,
relays: values.relays.length > 0 ? pluck("url", values.relays) : null,
}
if (!data.id) {
user.addFeed({id: randomId(), ...data})
} else {
user.updateFeeds($feeds => $feeds.map(when(propEq("id", data.id), always(data))))
}
toast.show("info", "Your feed has been saved!")
modal.pop()
}
</script>
<form on:submit|preventDefault={submit}>
<Content>
<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>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>Filters</strong>
<MultiSelect {search} bind:value={values.params}>
<div slot="item" let:item>
{#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">
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>
</div>
</Content>
</form>

View File

@ -1,52 +0,0 @@
<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

@ -1,45 +0,0 @@
<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,7 +1,7 @@
<script lang="ts">
import type {CustomFeed} from "src/util/types"
import {prop, objOf, find, propEq} from "ramda"
import {prop, uniq, indexBy, objOf, filter as _filter} from "ramda"
import {shuffle, synced} from "src/util/misc"
import {Tags} from "src/util/nostr"
import {modal} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
@ -12,74 +12,57 @@
import {sampleRelays, getAllPubkeyWriteRelays, getUserReadRelays} from "src/agent/relays"
import user from "src/agent/user"
const {feeds} = user
const defaultFeeds = [
{id: "follows", name: "Follows", authors: "follows"},
{id: "network", name: "Network", authors: "network"},
] as Array<CustomFeed>
const {lists} = user
const activeTab = synced("views/Feeds/activeTab", "Follows")
let activeTab = synced("views/Feeds/activeTab", "Follows")
let relays, filter, tabs, feed
$: allFeeds = defaultFeeds.concat($feeds)
let relays, filter, tabs
$: listsByName = indexBy(l => Tags.from(l).getMeta("d"), $lists)
$: {
tabs = allFeeds.map(prop("name")).slice(0, 2)
const defaultTabs = ["Follows", "Network"]
const customTabs = Object.keys(listsByName)
const validTabs = defaultTabs.concat(customTabs)
if (!tabs.includes($activeTab)) {
tabs = tabs.concat($activeTab)
} else if ($feeds.length > 0) {
tabs = tabs.concat($feeds[0].name)
if (!validTabs.includes($activeTab)) {
$activeTab = validTabs[0]
}
tabs = uniq(defaultTabs.concat($activeTab).concat(customTabs)).slice(0, 3)
}
$: {
feed = find(propEq("name", $activeTab), allFeeds)
if ($activeTab === "Follows") {
const authors = shuffle(getUserFollows()).slice(0, 256)
if (!feed) {
feed = allFeeds[0]
$activeTab = feed.name
}
}
$: {
let {authors, topics} = feed
if (authors === "follows") {
authors = shuffle(getUserFollows()).slice(0, 256)
filter = {authors}
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
} else if (authors === "network") {
authors = shuffle(getUserNetwork()).slice(0, 256)
} else if ($activeTab === "Network") {
const authors = shuffle(getUserNetwork()).slice(0, 256)
filter = {authors}
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
} else if (feed.relays) {
relays = feed.relays.map(objOf("url"))
} else {
relays = sampleRelays(getUserReadRelays())
const list = listsByName[$activeTab]
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())
}
// Separate notes and reactions into two queries since otherwise reactions dominate,
// we never find their parents (or reactions are mostly to a few posts), and the feed sucks
filter = [1, 7].map(kind => {
const filter = {kinds: [kind]} as Record<string, any>
if (authors) {
filter.authors = authors
}
if (topics) {
filter["#t"] = topics
}
return filter
})
filter = [1, 7].map(kind => ({...filter, kinds: [kind]}))
}
const setActiveTab = tab => {
$activeTab = tab
}
const showFeedsList = () => {
modal.push({type: "feed/list"})
const showLists = () => {
modal.push({type: "list/list"})
}
document.title = $activeTab
@ -96,29 +79,32 @@
{/if}
<div>
<Tabs {tabs} activeTab={$activeTab} {setActiveTab}>
{#if $feeds.length > 0}
{#if $lists.length > 1}
<Popover placement="bottom" opts={{hideOnClick: true}} theme="transparent">
<i slot="trigger" class="fa fa-ellipsis-v cursor-pointer p-2" />
<div
slot="tooltip"
class="flex flex-col items-start rounded border border-solid border-gray-8 bg-black">
{#each $feeds as feed (feed.name)}
<button
class="w-full py-2 px-3 text-left hover:bg-gray-7"
on:click={() => {
$activeTab = feed.name
}}>
<i class="fa fa-scroll fa-sm mr-1" />
{feed.name}
</button>
{#each $lists as e (e.id)}
{@const meta = Tags.from(e).asMeta()}
{#if meta.d !== $activeTab}
<button
class="w-full py-2 px-3 text-left hover:bg-gray-7"
on:click={() => {
$activeTab = meta.d
}}>
<i class="fa fa-scroll fa-sm mr-1" />
{meta.d}
</button>
{/if}
{/each}
<button on:click={showFeedsList} class="w-full py-2 px-3 text-left hover:bg-gray-7">
<button on:click={showLists} 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={showFeedsList} />
<i class="fa fa-ellipsis-v cursor-pointer p-1" on:click={showLists} />
{/if}
</Tabs>
{#key $activeTab}

View File

@ -0,0 +1,89 @@
<script>
import {pluck, find} from "ramda"
import {Tags, displayPerson, displayRelay} 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 {searchTopics, searchPeople, searchRelays, getPersonWithFallback} from "src/agent/db"
import user from "src/agent/user"
export let list
const tags = Tags.wrap(list?.tags || [])
let values = {
name: tags.getMeta("d") || "",
params: tags.type(["t", "p"]).all(),
relays: tags.type("r").all(),
}
const search = q => {
if (q.startsWith("#")) {
return $searchTopics(q).map(({name}) => ["t", name])
}
return $searchPeople(q).map(({pubkey}) => ["p", pubkey])
}
const _searchRelays = q => pluck("url", $searchRelays(q)).map(url => ["r", url])
const submit = () => {
if (!values.name) {
return toast.show("error", "A name is required for your list")
}
if (find(e => e.id !== list.id && Tags.from(e).getMeta("d") === values.name, user.getLists())) {
return toast.show("error", "That name is already in use")
}
const {name, params, relays} = values
user.putList(list?.id, name, params, relays)
toast.show("info", "Your list has been saved!")
modal.pop()
}
</script>
<form on:submit|preventDefault={submit}>
<Content>
<Heading class="text-center">{values.id ? "Edit" : "Add"} list</Heading>
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<strong>Name</strong>
<Input bind:value={values.name} placeholder="My list" />
<p class="text-sm text-gray-4">
Lists are identified by their name, so this has to be unique.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Topics and People</strong>
<MultiSelect {search} bind:value={values.params}>
<div slot="item" let:item>
{#if item[0] === "p"}
{displayPerson(getPersonWithFallback(item[1]))}
{:else}
#{item[1]}
{/if}
</div>
</MultiSelect>
<p class="text-sm text-gray-4">Type "@" to look for people, and "#" to look for topics.</p>
</div>
<div class="flex flex-col gap-1">
<strong>Relays</strong>
<MultiSelect search={_searchRelays} bind:value={values.relays}>
<div slot="item" let:item>
{displayRelay({url: item[1]})}
</div>
</MultiSelect>
<p class="text-sm text-gray-4">
Select which relays to limit this list to. If you leave this blank, your default relays
will be used.
</p>
</div>
<Button type="submit" class="text-center">Save</Button>
</div>
</Content>
</form>

View File

@ -0,0 +1,57 @@
<script type="ts">
import {indexBy} from "ramda"
import {Tags} from "src/util/nostr"
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 ListSummary from "src/app/shared/ListSummary.svelte"
import user from "src/agent/user"
const {lists} = user
$: listsByName = indexBy(l => Tags.from(l).getMeta("d"), $lists)
const createFeed = () => {
modal.push({type: "list/edit"})
}
const removeList = e => {
user.removeList(e.id)
}
const editList = list => {
modal.push({type: "list/edit", list})
}
</script>
<Content>
<div class="flex items-center justify-between">
<Heading>Your Lists</Heading>
<Anchor type="button-accent" on:click={createFeed}>
<i class="fa fa-plus" /> List
</Anchor>
</div>
<p>
Lists allow you to group people and topics to create custom feeds. You can create new lists by
handing using the "<i class="fa fa-plus" /> List" button above, or by clicking the
<i class="fa fa-scroll px-1" /> icon that appears throughout Coracle.
</p>
{#each $lists as e (e.id)}
{@const meta = Tags.from(e).asMeta()}
<div class="flex justify-start gap-3">
<i
class="fa fa-sm fa-trash cursor-pointer py-3"
on:click|stopPropagation={() => removeList(e)} />
<div class="flex w-full justify-between">
<div>
<strong>{meta.d}</strong>
<ListSummary list={e} />
</div>
<Anchor on:click={() => editList(e)}>Edit</Anchor>
</div>
</div>
{:else}
<p class="text-center py-12">You don't have any lists yet.</p>
{/each}
</Content>

View File

@ -0,0 +1,43 @@
<script type="ts">
import {updateIn} from "hurdak/lib/hurdak"
import {Tags} from "src/util/nostr"
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 ListSummary from "src/app/shared/ListSummary.svelte"
import user from "src/agent/user"
export let item
const {lists} = user
const label = item.type === "p" ? "person" : "topic"
const modifyList = updateIn("tags", tags => tags.concat([[item.type, item.value]]))
const selectlist = list => {
modal.replace({type: "list/edit", list: modifyList(list)})
}
</script>
<Content size="lg">
<div class="flex items-center justify-between">
<Heading>Select a List</Heading>
<Anchor type="button-accent" on:click={() => selectlist({})}>
<i class="fa fa-plus" /> List
</Anchor>
</div>
<p>
Select a list to modify. The selected {label} will be added to it as an additional filter.
</p>
{#each $lists as e (e.id)}
{@const meta = Tags.from(e).asMeta()}
<BorderLeft on:click={() => selectlist(e)}>
<strong>{meta.d}</strong>
<ListSummary list={e} />
</BorderLeft>
{:else}
<p class="text-center py-12">You don't have any custom lists yet.</p>
{/each}
</Content>

View File

@ -1,6 +1,8 @@
<script lang="ts">
import {last} from "ramda"
import {displayPerson} from "src/util/nostr"
import {parseHex} from "src/util/html"
import {theme, getThemeColor} from "src/partials/state"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import PersonActions from "src/app/shared/PersonActions.svelte"
@ -16,13 +18,29 @@
const person = watch("people", () => getPersonWithFallback(pubkey))
let rgb, rgba
$: relays = sampleRelays(getPubkeyWriteRelays(pubkey))
$: {
const color = parseHex(getThemeColor($theme, "gray-7"))
rgba = `rgba(${color.join(", ")}, 0.4)`
rgb = `rgba(${color.join(", ")})`
}
document.title = displayPerson($person)
</script>
<div
class="absolute left-0 h-64 w-full -mt-2"
style="background-size: cover;
background-image:
linear-gradient(to bottom, {rgba}, {rgb}),
url('{$person.kind0?.banner}')" />
<Content>
<div class="flex gap-4 text-gray-1">
<div class="flex gap-4 text-gray-1 z-10">
<PersonCircle person={$person} size={16} class="sm:h-32 sm:w-32" />
<div class="flex flex-grow flex-col gap-4">
<div class="flex items-start justify-between gap-4">

View File

@ -11,7 +11,7 @@
<div class={className}>
<div class="flex items-center gap-2">
<button class="fa fa-times cursor-pointer" on:click|preventDefault />
<i class="fa fa-times cursor-pointer" on:click|preventDefault />
<slot />
</div>
</div>

View File

@ -20,7 +20,7 @@
}
const select = item => {
value = value.concat(item)
value = value.concat([item])
term = ""
}
@ -45,10 +45,6 @@
}
}
if (!term && event.key === "Backspace") {
value = value.slice(0, -1)
}
if (suggestions?.get() && event.code === "ArrowUp") {
event.preventDefault()
suggestions.prev()

View File

@ -48,22 +48,30 @@ export const openModals = writable(0)
export const modal = {
stack: new WritableList([]) as WritableList<any>,
sync: $stack => {
sync: ($stack, opts) => {
const hash = $stack.length > 0 ? `#m=${$stack.length}` : ""
navigate(window.location.pathname + hash)
navigate(window.location.pathname + hash, opts)
return $stack
},
push: data => modal.stack.update($stack => modal.sync($stack.concat(data))),
pop: () => modal.stack.update($stack => modal.sync($stack.slice(0, -1))),
clear: async () => {
push(data) {
modal.stack.update($stack => modal.sync($stack.concat(data)))
},
async pop() {
modal.stack.update($stack => modal.sync($stack.slice(0, -1)))
await sleep(100)
},
async replace(data) {
await modal.pop()
modal.push(data)
},
async clear() {
const stackSize = (get(modal.stack) as any).length
// Reverse history so the back button doesn't bring our modal back up
for (let i = 0; i < stackSize; i++) {
history.back()
await sleep(100)
await modal.pop()
}
},
}

View File

@ -40,6 +40,9 @@ export class Tags {
asMeta() {
return fromPairs(this.tags)
}
getMeta(k) {
return this.type(k).values().first()
}
values() {
return new Tags(this.tags.map(t => t[1]))
}
@ -47,7 +50,9 @@ export class Tags {
return new Tags(this.tags.filter(f))
}
type(type) {
return new Tags(this.tags.filter(t => t[0] === type))
const types = ensurePlural(type)
return new Tags(this.tags.filter(t => types.includes(t[0])))
}
equals(value) {
return new Tags(this.tags.filter(t => t[1] === value))

View File

@ -37,11 +37,3 @@ export type Room = {
about?: string
picture?: string
}
export type CustomFeed = {
id: string
name: string
authors?: "follows" | "network" | Array<string>
topics?: Array<string>
relays?: Array<string>
}