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
|
# 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
|
## 0.2.23
|
||||||
|
|
||||||
- [x] Fix modal scroll position for nested modals
|
- [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] Add topic search, keep cache of topics
|
||||||
- [x] Ability to create custom feeds
|
- [x] Ability to create custom feeds
|
||||||
- [x] Bookmark icon opens "create feed" dialog with form pre-filled
|
- [x] Bookmark icon opens "create feed" dialog with form pre-filled
|
||||||
- [ ] Use lists instead of custom app data
|
- [ ] Replace some modals instead of pushing
|
||||||
- [ ] Public/private toggle
|
- [ ] Test anonymous with lists
|
||||||
- [ ] Add person to feed button (maybe lists make more sense for this?)
|
- [ ] Test hardcoded relay, currently you get asked to pick a relay if not logged in
|
||||||
- [ ] Add 30078 to personKinds (except we'll have to get more involved)
|
|
||||||
- [ ] Claim relays bounty
|
- [ ] 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
|
- [ ] Some lnurls aren't working npub1y3k2nheva29y9ej8a22e07epuxrn04rvgy28wvs54y57j7vsxxuq0gvp4j
|
||||||
- [ ] Global search modal that searches within current feed
|
- [ ] Global search modal that searches within current feed
|
||||||
- [ ] Fix force relays on login: http://localhost:5173/messages/npub1l66wvfm7dxhd6wmvpukpjpyhvwtlxzu0qqajqxjfpr4rlfa8hl5qlkfr3q
|
- [ ] Fix force relays on login: http://localhost:5173/messages/npub1l66wvfm7dxhd6wmvpukpjpyhvwtlxzu0qqajqxjfpr4rlfa8hl5qlkfr3q
|
||||||
@ -64,7 +67,7 @@
|
|||||||
# UI/Features
|
# UI/Features
|
||||||
|
|
||||||
- [ ] Remember message/chat status
|
- [ ] Remember message/chat status
|
||||||
- [ ] Linkify topics
|
- [ ] Allow sharing of lists/following other people's lists
|
||||||
- [ ] Add suggestion list for topics on compose
|
- [ ] Add suggestion list for topics on compose
|
||||||
- [ ] Badges link to https://badges.page/p/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
- [ ] Badges link to https://badges.page/p/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
||||||
- [ ] Add QR code that pre-fills follows and relays for a new user
|
- [ ] 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 setMutes = mutes => new PublishableEvent(10000, {tags: mutes})
|
||||||
|
|
||||||
const setFeeds = feeds =>
|
const createList = list => new PublishableEvent(30001, {tags: list})
|
||||||
new PublishableEvent(30078, {content: JSON.stringify(feeds), tags: [["d", "coracle/feeds"]]})
|
|
||||||
|
|
||||||
const createRoom = room =>
|
const createRoom = room =>
|
||||||
new PublishableEvent(40, {content: JSON.stringify(pick(roomAttrs, room))})
|
new PublishableEvent(40, {content: JSON.stringify(pick(roomAttrs, room))})
|
||||||
@ -202,7 +201,7 @@ export default {
|
|||||||
setRelays,
|
setRelays,
|
||||||
setPetnames,
|
setPetnames,
|
||||||
setMutes,
|
setMutes,
|
||||||
setFeeds,
|
createList,
|
||||||
createRoom,
|
createRoom,
|
||||||
updateRoom,
|
updateRoom,
|
||||||
createChatMessage,
|
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 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 contacts = new Table("contacts", "pubkey")
|
||||||
export const rooms = new Table("rooms", "id")
|
export const rooms = new Table("rooms", "id")
|
||||||
export const relays = new Table("relays", "url")
|
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 {nip05} from "nostr-tools"
|
||||||
import {noop, ensurePlural, chunk} from "hurdak/lib/hurdak"
|
import {noop, ensurePlural, chunk} from "hurdak/lib/hurdak"
|
||||||
import {
|
import {
|
||||||
@ -222,11 +222,16 @@ addHandler(
|
|||||||
)
|
)
|
||||||
|
|
||||||
addHandler(
|
addHandler(
|
||||||
30078,
|
30001,
|
||||||
profileHandler("feeds", (e, p) => {
|
profileHandler("lists", (e, p) => uniqBy(prop("id"), p.lists.concat(e)))
|
||||||
if (Tags.from(e).type("d").values().first() === "coracle/feeds") {
|
)
|
||||||
return tryJson(() => JSON.parse(e.content))
|
|
||||||
}
|
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 type {Readable} from "svelte/store"
|
||||||
import {
|
import {
|
||||||
slice,
|
slice,
|
||||||
@ -37,14 +37,14 @@ const profile = synced("agent/user/profile", {
|
|||||||
petnames: [],
|
petnames: [],
|
||||||
relays: [],
|
relays: [],
|
||||||
mutes: [],
|
mutes: [],
|
||||||
feeds: [],
|
lists: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const settings = derived(profile, prop("settings"))
|
const settings = derived(profile, prop("settings"))
|
||||||
const petnames = derived(profile, prop("petnames"))
|
const petnames = derived(profile, prop("petnames"))
|
||||||
const relays = derived(profile, prop("relays")) as Readable<Array<Relay>>
|
const relays = derived(profile, prop("relays")) as Readable<Array<Relay>>
|
||||||
const mutes = derived(profile, prop("mutes")) as Readable<Array<[string, string]>>
|
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(
|
const canPublish = derived(
|
||||||
[keys.pubkey, relays],
|
[keys.pubkey, relays],
|
||||||
@ -166,23 +166,20 @@ export default {
|
|||||||
return this.updateMutes(reject(t => t[1] === pubkey))
|
return this.updateMutes(reject(t => t[1] === pubkey))
|
||||||
},
|
},
|
||||||
|
|
||||||
// Feeds
|
// Lists
|
||||||
|
|
||||||
feeds,
|
lists,
|
||||||
getFeeds: () => profileCopy.feeds,
|
getLists: () => profileCopy.lists,
|
||||||
updateFeeds(f) {
|
async putList(id, name, params, relays) {
|
||||||
const $feeds = f(profileCopy.feeds)
|
const tags = [["d", name]].concat(params).concat(relays)
|
||||||
|
|
||||||
profile.update(assoc("feeds", $feeds))
|
if (id) {
|
||||||
|
await cmd.deleteEvent([id]).publish(profileCopy.relays)
|
||||||
if (keys.canSign()) {
|
|
||||||
return cmd.setFeeds($feeds).publish(profileCopy.relays)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await cmd.createList(tags).publish(profileCopy.relays)
|
||||||
},
|
},
|
||||||
addFeed(feed) {
|
removeList(id) {
|
||||||
return this.updateFeeds($feeds => $feeds.concat(feed))
|
return cmd.deleteEvent([id]).publish(profileCopy.relays)
|
||||||
},
|
|
||||||
removeFeed(id) {
|
|
||||||
return this.updateFeeds($feeds => reject(whereEq({id}), $feeds))
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
import PersonProfileInfo from "src/app/views/PersonProfileInfo.svelte"
|
import PersonProfileInfo from "src/app/views/PersonProfileInfo.svelte"
|
||||||
import PersonShare from "src/app/views/PersonShare.svelte"
|
import PersonShare from "src/app/views/PersonShare.svelte"
|
||||||
import TopicFeed from "src/app/views/TopicFeed.svelte"
|
import TopicFeed from "src/app/views/TopicFeed.svelte"
|
||||||
import FeedList from "src/app/views/FeedList.svelte"
|
import ListList from "src/app/views/ListList.svelte"
|
||||||
import FeedSelect from "src/app/views/FeedSelect.svelte"
|
import ListSelect from "src/app/views/ListSelect.svelte"
|
||||||
import FeedEdit from "src/app/views/FeedEdit.svelte"
|
import ListEdit from "src/app/views/ListEdit.svelte"
|
||||||
import RelayAdd from "src/app/views/RelayAdd.svelte"
|
import RelayAdd from "src/app/views/RelayAdd.svelte"
|
||||||
|
|
||||||
const {stack} = modal
|
const {stack} = modal
|
||||||
@ -64,12 +64,12 @@
|
|||||||
{#key m.topic}
|
{#key m.topic}
|
||||||
<TopicFeed topic={m.topic} />
|
<TopicFeed topic={m.topic} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else if m.type === "feed/list"}
|
{:else if m.type === "list/list"}
|
||||||
<FeedList />
|
<ListList />
|
||||||
{:else if m.type === "feed/select"}
|
{:else if m.type === "list/select"}
|
||||||
<FeedSelect key={m.key} value={m.value} />
|
<ListSelect item={m.item} />
|
||||||
{:else if m.type === "feed/edit"}
|
{:else if m.type === "list/edit"}
|
||||||
<FeedEdit feed={m.feed} />
|
<ListEdit list={m.list} />
|
||||||
{:else if m.type === "message"}
|
{:else if m.type === "message"}
|
||||||
<Content size="lg">
|
<Content size="lg">
|
||||||
<div class="text-center">{m.message}</div>
|
<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"
|
class="note-reply relative z-10 my-2 flex flex-col gap-1"
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
on:click|stopPropagation>
|
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}>
|
<div class="bg-gray-7" class:rounded-b={data.mentions.length === 0}>
|
||||||
<Compose bind:this={reply} onSubmit={send}>
|
<Compose bind:this={reply} onSubmit={send}>
|
||||||
<button
|
<button
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import {getPubkeyWriteRelays} from "src/agent/relays"
|
import {getPubkeyWriteRelays} from "src/agent/relays"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
import {addToFeed} from "src/app/state"
|
import {addToList} from "src/app/state"
|
||||||
|
|
||||||
export let person
|
export let person
|
||||||
|
|
||||||
@ -24,8 +24,8 @@
|
|||||||
actions = []
|
actions = []
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
onClick: () => addToFeed("authors", person.pubkey),
|
onClick: () => addToList("p", person.pubkey),
|
||||||
label: "Add to feed",
|
label: "Add to list",
|
||||||
icon: "scroll",
|
icon: "scroll",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import OverflowMenu from "src/partials/OverflowMenu.svelte"
|
import OverflowMenu from "src/partials/OverflowMenu.svelte"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {getRelayWithFallback} from "src/agent/db"
|
import {getRelayWithFallback} from "src/agent/db"
|
||||||
import {addToFeed} from "src/app/state"
|
import {addToList} from "src/app/state"
|
||||||
|
|
||||||
export let relay
|
export let relay
|
||||||
|
|
||||||
@ -32,8 +32,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
onClick: () => addToFeed("relays", relay.url),
|
onClick: () => addToList("r", relay.url),
|
||||||
label: "Add to feed",
|
label: "Add to list",
|
||||||
icon: "scroll",
|
icon: "scroll",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import OverflowMenu from "src/partials/OverflowMenu.svelte"
|
import OverflowMenu from "src/partials/OverflowMenu.svelte"
|
||||||
import {addToFeed} from "src/app/state"
|
import {addToList} from "src/app/state"
|
||||||
|
|
||||||
export let topic
|
export let topic
|
||||||
|
|
||||||
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
actions.push({
|
actions.push({
|
||||||
onClick: () => addToFeed("topics", topic),
|
onClick: () => addToList("t", topic),
|
||||||
label: "Add to feed",
|
label: "Add to list",
|
||||||
icon: "scroll",
|
icon: "scroll",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import {createMap, doPipe, first} from "hurdak/lib/hurdak"
|
|||||||
import {warn} from "src/util/logger"
|
import {warn} from "src/util/logger"
|
||||||
import {hash} from "src/util/misc"
|
import {hash} from "src/util/misc"
|
||||||
import {synced, now, timedelta} 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 {findReplyId} from "src/util/nostr"
|
||||||
import {modal, toast} from "src/partials/state"
|
import {modal, toast} from "src/partials/state"
|
||||||
import {notifications, watch, userEvents, contacts, rooms} from "src/agent/db"
|
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
|
// Menu
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ export const loadAppData = async pubkey => {
|
|||||||
listen(pubkey)
|
listen(pubkey)
|
||||||
|
|
||||||
// Make sure the user and their network is loaded
|
// 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())
|
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">
|
<script lang="ts">
|
||||||
import type {CustomFeed} from "src/util/types"
|
import {prop, uniq, indexBy, objOf, filter as _filter} from "ramda"
|
||||||
import {prop, objOf, find, propEq} from "ramda"
|
|
||||||
import {shuffle, synced} from "src/util/misc"
|
import {shuffle, synced} from "src/util/misc"
|
||||||
|
import {Tags} from "src/util/nostr"
|
||||||
import {modal} from "src/partials/state"
|
import {modal} 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"
|
||||||
@ -12,74 +12,57 @@
|
|||||||
import {sampleRelays, getAllPubkeyWriteRelays, getUserReadRelays} from "src/agent/relays"
|
import {sampleRelays, getAllPubkeyWriteRelays, getUserReadRelays} from "src/agent/relays"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
const {feeds} = user
|
const {lists} = user
|
||||||
const defaultFeeds = [
|
const activeTab = synced("views/Feeds/activeTab", "Follows")
|
||||||
{id: "follows", name: "Follows", authors: "follows"},
|
|
||||||
{id: "network", name: "Network", authors: "network"},
|
|
||||||
] as Array<CustomFeed>
|
|
||||||
|
|
||||||
let activeTab = synced("views/Feeds/activeTab", "Follows")
|
let relays, filter, tabs
|
||||||
let relays, filter, tabs, feed
|
|
||||||
|
|
||||||
$: allFeeds = defaultFeeds.concat($feeds)
|
|
||||||
|
|
||||||
|
$: 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)) {
|
if (!validTabs.includes($activeTab)) {
|
||||||
tabs = tabs.concat($activeTab)
|
$activeTab = validTabs[0]
|
||||||
} else if ($feeds.length > 0) {
|
|
||||||
tabs = tabs.concat($feeds[0].name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
filter = {authors}
|
||||||
feed = allFeeds[0]
|
|
||||||
$activeTab = feed.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
let {authors, topics} = feed
|
|
||||||
|
|
||||||
if (authors === "follows") {
|
|
||||||
authors = shuffle(getUserFollows()).slice(0, 256)
|
|
||||||
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
|
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
|
||||||
} else if (authors === "network") {
|
} else if ($activeTab === "Network") {
|
||||||
authors = shuffle(getUserNetwork()).slice(0, 256)
|
const authors = shuffle(getUserNetwork()).slice(0, 256)
|
||||||
|
|
||||||
|
filter = {authors}
|
||||||
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
|
relays = sampleRelays(getAllPubkeyWriteRelays(authors))
|
||||||
} else if (feed.relays) {
|
|
||||||
relays = feed.relays.map(objOf("url"))
|
|
||||||
} else {
|
} 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,
|
// 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
|
// we never find their parents (or reactions are mostly to a few posts), and the feed sucks
|
||||||
filter = [1, 7].map(kind => {
|
filter = [1, 7].map(kind => ({...filter, kinds: [kind]}))
|
||||||
const filter = {kinds: [kind]} as Record<string, any>
|
|
||||||
|
|
||||||
if (authors) {
|
|
||||||
filter.authors = authors
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topics) {
|
|
||||||
filter["#t"] = topics
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setActiveTab = tab => {
|
const setActiveTab = tab => {
|
||||||
$activeTab = tab
|
$activeTab = tab
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFeedsList = () => {
|
const showLists = () => {
|
||||||
modal.push({type: "feed/list"})
|
modal.push({type: "list/list"})
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = $activeTab
|
document.title = $activeTab
|
||||||
@ -96,29 +79,32 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
<Tabs {tabs} activeTab={$activeTab} {setActiveTab}>
|
<Tabs {tabs} activeTab={$activeTab} {setActiveTab}>
|
||||||
{#if $feeds.length > 0}
|
{#if $lists.length > 1}
|
||||||
<Popover placement="bottom" opts={{hideOnClick: true}} theme="transparent">
|
<Popover placement="bottom" opts={{hideOnClick: true}} theme="transparent">
|
||||||
<i slot="trigger" class="fa fa-ellipsis-v cursor-pointer p-2" />
|
<i slot="trigger" class="fa fa-ellipsis-v cursor-pointer p-2" />
|
||||||
<div
|
<div
|
||||||
slot="tooltip"
|
slot="tooltip"
|
||||||
class="flex flex-col items-start rounded border border-solid border-gray-8 bg-black">
|
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)}
|
||||||
<button
|
{@const meta = Tags.from(e).asMeta()}
|
||||||
class="w-full py-2 px-3 text-left hover:bg-gray-7"
|
{#if meta.d !== $activeTab}
|
||||||
on:click={() => {
|
<button
|
||||||
$activeTab = feed.name
|
class="w-full py-2 px-3 text-left hover:bg-gray-7"
|
||||||
}}>
|
on:click={() => {
|
||||||
<i class="fa fa-scroll fa-sm mr-1" />
|
$activeTab = meta.d
|
||||||
{feed.name}
|
}}>
|
||||||
</button>
|
<i class="fa fa-scroll fa-sm mr-1" />
|
||||||
|
{meta.d}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/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
|
<i class="fa fa-cog fa-sm mr-1" /> Customize
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{#key $activeTab}
|
{#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">
|
<script lang="ts">
|
||||||
import {last} from "ramda"
|
import {last} from "ramda"
|
||||||
import {displayPerson} from "src/util/nostr"
|
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 Content from "src/partials/Content.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import PersonActions from "src/app/shared/PersonActions.svelte"
|
import PersonActions from "src/app/shared/PersonActions.svelte"
|
||||||
@ -16,13 +18,29 @@
|
|||||||
|
|
||||||
const person = watch("people", () => getPersonWithFallback(pubkey))
|
const person = watch("people", () => getPersonWithFallback(pubkey))
|
||||||
|
|
||||||
|
let rgb, rgba
|
||||||
|
|
||||||
$: relays = sampleRelays(getPubkeyWriteRelays(pubkey))
|
$: 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)
|
document.title = displayPerson($person)
|
||||||
</script>
|
</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>
|
<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" />
|
<PersonCircle person={$person} size={16} class="sm:h-32 sm:w-32" />
|
||||||
<div class="flex flex-grow flex-col gap-4">
|
<div class="flex flex-grow flex-col gap-4">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<div class={className}>
|
<div class={className}>
|
||||||
<div class="flex items-center gap-2">
|
<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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const select = item => {
|
const select = item => {
|
||||||
value = value.concat(item)
|
value = value.concat([item])
|
||||||
term = ""
|
term = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,10 +45,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!term && event.key === "Backspace") {
|
|
||||||
value = value.slice(0, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (suggestions?.get() && event.code === "ArrowUp") {
|
if (suggestions?.get() && event.code === "ArrowUp") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
suggestions.prev()
|
suggestions.prev()
|
||||||
|
@ -48,22 +48,30 @@ export const openModals = writable(0)
|
|||||||
|
|
||||||
export const modal = {
|
export const modal = {
|
||||||
stack: new WritableList([]) as WritableList<any>,
|
stack: new WritableList([]) as WritableList<any>,
|
||||||
sync: $stack => {
|
sync: ($stack, opts) => {
|
||||||
const hash = $stack.length > 0 ? `#m=${$stack.length}` : ""
|
const hash = $stack.length > 0 ? `#m=${$stack.length}` : ""
|
||||||
|
|
||||||
navigate(window.location.pathname + hash)
|
navigate(window.location.pathname + hash, opts)
|
||||||
|
|
||||||
return $stack
|
return $stack
|
||||||
},
|
},
|
||||||
push: data => modal.stack.update($stack => modal.sync($stack.concat(data))),
|
push(data) {
|
||||||
pop: () => modal.stack.update($stack => modal.sync($stack.slice(0, -1))),
|
modal.stack.update($stack => modal.sync($stack.concat(data)))
|
||||||
clear: async () => {
|
},
|
||||||
|
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
|
const stackSize = (get(modal.stack) as any).length
|
||||||
|
|
||||||
// Reverse history so the back button doesn't bring our modal back up
|
// Reverse history so the back button doesn't bring our modal back up
|
||||||
for (let i = 0; i < stackSize; i++) {
|
for (let i = 0; i < stackSize; i++) {
|
||||||
history.back()
|
await modal.pop()
|
||||||
await sleep(100)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,9 @@ export class Tags {
|
|||||||
asMeta() {
|
asMeta() {
|
||||||
return fromPairs(this.tags)
|
return fromPairs(this.tags)
|
||||||
}
|
}
|
||||||
|
getMeta(k) {
|
||||||
|
return this.type(k).values().first()
|
||||||
|
}
|
||||||
values() {
|
values() {
|
||||||
return new Tags(this.tags.map(t => t[1]))
|
return new Tags(this.tags.map(t => t[1]))
|
||||||
}
|
}
|
||||||
@ -47,7 +50,9 @@ export class Tags {
|
|||||||
return new Tags(this.tags.filter(f))
|
return new Tags(this.tags.filter(f))
|
||||||
}
|
}
|
||||||
type(type) {
|
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) {
|
equals(value) {
|
||||||
return new Tags(this.tags.filter(t => t[1] === value))
|
return new Tags(this.tags.filter(t => t[1] === value))
|
||||||
|
@ -37,11 +37,3 @@ export type Room = {
|
|||||||
about?: string
|
about?: string
|
||||||
picture?: 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