mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-19 11:43:35 +00:00
Switch from custom data to lists
This commit is contained in:
parent
54553bbbc8
commit
5b548cccab
@ -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
|
||||
|
13
ROADMAP.md
13
ROADMAP.md
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
24
src/app/shared/ListSummary.svelte
Normal file
24
src/app/shared/ListSummary.svelte
Normal 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>
|
@ -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
|
||||
|
@ -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",
|
||||
})
|
||||
|
||||
|
@ -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",
|
||||
})
|
||||
|
||||
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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)}
|
||||
{#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 = feed.name
|
||||
$activeTab = meta.d
|
||||
}}>
|
||||
<i class="fa fa-scroll fa-sm mr-1" />
|
||||
{feed.name}
|
||||
{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}
|
||||
|
89
src/app/views/ListEdit.svelte
Normal file
89
src/app/views/ListEdit.svelte
Normal 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>
|
57
src/app/views/ListList.svelte
Normal file
57
src/app/views/ListList.svelte
Normal 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>
|
43
src/app/views/ListSelect.svelte
Normal file
43
src/app/views/ListSelect.svelte
Normal 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>
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user