Add render support for feeds and new hud

This commit is contained in:
Jon Staab 2024-05-15 15:04:33 -07:00
parent 55db5f174f
commit d74790a4da
22 changed files with 199 additions and 122 deletions

View File

@ -1,7 +1,9 @@
<script lang="ts">
import {equals} from "ramda"
import {randomId} from "@welshman/lib"
import {seconds} from "hurdak"
import {randomId, now} from "@welshman/lib"
import {makeScopeFeed, Scope} from "@welshman/feeds"
import {PublishStatus} from "@welshman/net"
import {fly} from "src/util/transition"
import {toggleTheme, theme} from "src/partials/state"
import MenuItem from "src/partials/MenuItem.svelte"
@ -26,12 +28,37 @@
displayPubkey,
userListFeeds,
userFeeds,
publishes,
} from "src/engine"
const {page} = router
const followsFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Follows))})
const networkFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Network))})
const hud = publishes.derived($publishes => {
const pending = []
const success = []
const failure = []
for (const {created_at, request, status} of $publishes) {
if (created_at < now() - seconds(5, "minute")) {
continue
}
const statuses = Array.from(status.values())
if (statuses.includes(PublishStatus.Success)) {
success.push(request.event)
} else if (statuses.includes(PublishStatus.Pending)) {
pending.push(request.event)
} else {
failure.push(request.event)
}
}
return {pending, success, failure}
})
const closeSubMenu = () => {
subMenu = null
}
@ -228,18 +255,39 @@
</MenuItem>
</MenuDesktopSecondary>
{/if}
<div class="h-20 cursor-pointer border-t border-solid border-neutral-600 px-7 py-4">
{#if $pubkey}
<Anchor class="flex items-center gap-2" on:click={() => setSubMenu("account")}>
<PersonCircle class="h-10 w-10" pubkey={$pubkey} />
<div class="flex min-w-0 flex-col">
<span>@{displayPerson($user)}</span>
<PersonHandle class="text-sm" pubkey={$pubkey} />
</div>
</Anchor>
{:else}
<Anchor modal button accent href="/login">Log In</Anchor>
{/if}
<div>
<Anchor
modal
href="/publishes"
class="flex h-12 cursor-pointer items-center justify-between border-t border-solid border-neutral-600 pl-7 pr-12">
<div class="flex items-center gap-1" class:text-tinted-500={$hud.pending.length === 0}>
<i class="fa fa-hourglass" />
{$hud.pending.length}
</div>
<div class="flex items-center gap-1" class:text-tinted-500={$hud.success.length === 0}>
<i class="fa fa-cloud-arrow-up" />
{$hud.success.length}
</div>
<div class="flex items-center gap-1"
class:text-accent={$hud.failure.length > 0}
class:text-tinted-500={$hud.failure.length === 0}>
<i class="fa fa-triangle-exclamation" />
{$hud.failure.length}
</div>
</Anchor>
<div class="h-20 cursor-pointer border-t border-solid border-neutral-600 px-7 py-4">
{#if $pubkey}
<Anchor class="flex items-center gap-2" on:click={() => setSubMenu("account")}>
<PersonCircle class="h-10 w-10" pubkey={$pubkey} />
<div class="flex min-w-0 flex-col">
<span>@{displayPerson($user)}</span>
<PersonHandle class="text-sm" pubkey={$pubkey} />
</div>
</Anchor>
{:else}
<Anchor modal button accent href="/login">Log In</Anchor>
{/if}
</div>
</div>
</FlexColumn>
</div>

View File

@ -122,7 +122,7 @@
<i class="fa fa-rss" /> Feeds
</MenuMobileItem>
</div>
<div class="staatliches mt-8 block flex h-8 justify-center gap-2 px-8 text-neutral-300">
<div class="staatliches mt-8 block flex h-8 justify-center gap-2 px-8 text-tinted-400">
<Anchor class="hover:text-tinted-200" href="/about">About</Anchor> /
<Anchor external class="hover:text-tinted-200" href="/terms.html">Terms</Anchor> /
<Anchor external class="hover:text-tinted-200" href="/privacy.html">Privacy</Anchor>
@ -192,7 +192,7 @@
<i class="fa fa-paper-plane" /> Create Invite
</MenuMobileItem>
</div>
<div class="staatliches block flex h-8 justify-center gap-2 px-8 text-neutral-300">
<div class="staatliches block flex h-8 justify-center gap-2 px-8 text-tinted-400">
<Anchor class="hover:text-tinted-200" href="/logout" on:click={closeMenu}>Logout</Anchor> /
<Anchor class="hover:text-tinted-200" stopPropagation on:click={() => setSubMenu("accounts")}>
Switch Accounts

View File

@ -1,7 +1,4 @@
<script lang="ts">
import {now} from "@welshman/lib"
import {PublishStatus} from "@welshman/net"
import {quantify, seconds} from "hurdak"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import SearchResults from "src/app/shared/SearchResults.svelte"
@ -9,37 +6,13 @@
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {menuIsOpen, searchTerm} from "src/app/state"
import {router} from "src/app/util/router"
import {env, pubkey, canSign, hasNewNotifications, hasNewMessages, publishes} from "src/engine"
import {env, pubkey, canSign, hasNewNotifications, hasNewMessages} from "src/engine"
let innerWidth = 0
let searchInput
const {page} = router
const hud = publishes.derived($publishes => {
const pending = []
const success = []
const failure = []
for (const {created_at, request, status} of $publishes) {
if (created_at < now() - seconds(5, "minute")) {
continue
}
const statuses = Array.from(status.values())
if (statuses.includes(PublishStatus.Pending)) {
pending.push(request.event)
} else if (statuses.includes(PublishStatus.Success)) {
success.push(request.event)
} else {
failure.push(request.event)
}
}
return {pending, success, failure}
})
const openMenu = () => menuIsOpen.set(true)
const openSearch = () => router.at("/search").open()
@ -78,22 +51,6 @@
{#if innerWidth >= 1024}
<div
class="fixed left-0 right-0 top-0 z-nav flex h-16 items-center justify-end gap-8 bg-neutral-900 pl-4 pr-8">
<div class="absolute left-72 flex items-center gap-2 px-4 text-sm text-neutral-500">
{#if $hud.pending.length > 0}
<i class="fa fa-circle-notch fa-spin" />
Sending {quantify($hud.pending.length, "note")}.
{:else if $hud.failure.length > 0}
<i class="fa fa-triangle-exclamation" />
Failed to publish {quantify($hud.failure.length, "note")}.
{:else if $hud.success.length > 0}
<i class="fa fa-check" />
Successfully published {quantify($hud.success.length, "note")}.
{:else}
<i class="fa fa-check" />
No recent notes.
{/if}
<Anchor underline modal href="/publishes">Details</Anchor>
</div>
<div class="relative">
<div class="flex">
<Input

View File

@ -1,8 +1,10 @@
<script lang="ts">
import {NAMED_BOOKMARKS} from "@welshman/util"
import {NAMED_BOOKMARKS, addressToNaddr, decodeAddress} from "@welshman/util"
import FlexColumn from "src/partials/FlexColumn.svelte"
import Card from "src/partials/Card.svelte"
import Chip from "src/partials/Chip.svelte"
import Anchor from "src/partials/Anchor.svelte"
import CopyValueSimple from "src/partials/CopyValueSimple.svelte"
import FeedSummary from "src/app/shared/FeedSummary.svelte"
import {readFeed, readList, displayFeed, mapListToFeed} from "src/domain"
import {repository} from "src/engine"
@ -12,6 +14,7 @@
export let address
const event = repository.getEvent(address)
const deleted = repository.isDeleted(event)
const feed = address.startsWith(NAMED_BOOKMARKS)
? mapListToFeed(readList(event))
: readFeed(event)
@ -27,15 +30,27 @@
<div class="flex items-center justify-between">
<span class="staatliches flex items-center gap-3 text-xl">
<i class="fa fa-rss" />
<Anchor on:click={loadFeed} class={feed.title ? "" : "text-neutral-500"}>
<span class:text-neutral-400={!feed.title} class:line-through={deleted}>
{displayFeed(feed)}
</Anchor>
</span>
{#if deleted}
<Chip danger small>Deleted</Chip>
{/if}
</span>
<slot name="controls" />
<slot name="controls">
<Anchor on:click={loadFeed}>
Load feed
</Anchor>
</slot>
</div>
{#if feed.description}
<p>{feed.description}</p>
{/if}
<FeedSummary feed={feed.definition} />
<div class="flex items-start justify-between">
<FeedSummary feed={feed.definition} />
<div class="py-2">
<CopyValueSimple label="Feed address" value={addressToNaddr(decodeAddress(address))} />
</div>
</div>
</FlexColumn>
</Card>

View File

@ -117,12 +117,7 @@
{#if listMenuIsOpen}
<Popover2 absolute hideOnClick onClose={closeListMenu} class="right-0 top-8 w-60">
<Menu>
<MenuItem inert class="flex items-center justify-between bg-neutral-800 shadow">
<span class="staatliches text-lg">Your Feeds</span>
<Anchor href={router.at("feeds").toString()}>
<i class="fa fa-cog" />
</Anchor>
</MenuItem>
<MenuItem inert class="staatliches bg-neutral-800 text-lg shadow">Your Feeds</MenuItem>
<div class="max-h-96 overflow-auto">
<MenuItem on:click={() => setFeed(followsFeed)}>Follows</MenuItem>
<MenuItem on:click={() => setFeed(networkFeed)}>Network</MenuItem>
@ -136,6 +131,13 @@
{displayList(feed.list)}
</MenuItem>
{/each}
<div class="h-px bg-neutral-600" />
<MenuItem href={router.at("feeds").toString()} class="flex items-center gap-2">
<i class="fa fa-rss" /> Manage feeds
</MenuItem>
<MenuItem href={router.at("lists").toString()} class="flex items-center gap-2">
<i class="fa fa-list" /> Manage lists
</MenuItem>
</div>
</Menu>
</Popover2>

View File

@ -168,7 +168,7 @@
search={searchFeeds}
onChange={addFeed}
displayItem={displayPubkey}>
<i slot="before" class="fa fa-scroll" />
<i slot="before" class="fa fa-rss" />
<span slot="item" let:item>
<PersonBadge inert pubkey={item} />
</span>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import {quantify} from 'hurdak'
import {first} from '@welshman/lib'
import {Tags} from '@welshman/util'
import {defaultTagFeedMappings} from '@welshman/feeds'
import {quantify} from "hurdak"
import {first} from "@welshman/lib"
import {Tags, addressToNaddr, decodeAddress} from "@welshman/util"
import {defaultTagFeedMappings} from "@welshman/feeds"
import FlexColumn from "src/partials/FlexColumn.svelte"
import Card from "src/partials/Card.svelte"
import Chip from "src/partials/Chip.svelte"
@ -14,6 +14,7 @@
import {router} from "src/app/util"
export let address
export let inert = false
const tagTypes = defaultTagFeedMappings.map(first) as string[]
const event = repository.getEvent(address)
@ -22,8 +23,10 @@
const list = readList(event)
const loadFeed = () => {
globalFeed.set(mapListToFeed(list))
router.at("notes").push()
if (!inert) {
globalFeed.set(mapListToFeed(list))
router.at("notes").push()
}
}
</script>
@ -32,25 +35,28 @@
<div class="flex items-center justify-between">
<span class="staatliches flex items-center gap-3 text-xl">
<i class="fa fa-list" />
<span
class:text-neutral-400={!list.title}
class:line-through={deleted}>
<Anchor on:click={loadFeed}>
{displayList(list)}
</Anchor>
<span class:text-neutral-400={!list.title} class:line-through={deleted}>
{displayList(list)}
</span>
{#if deleted}
<Chip danger small>Deleted</Chip>
{/if}
</span>
<slot name="controls" />
<slot name="controls">
<Anchor on:click={loadFeed}>
Load as feed
</Anchor>
</div>
</div>
{#if list.description}
<p>{list.description}</p>
{/if}
<div class="flex items-center justify-between">
{quantify(tags.filterByKey(tagTypes).count(), 'item')}
<CopyValueSimple label="List address" value={address} class="text-neutral-400" />
{quantify(tags.filterByKey(tagTypes).count(), "item")}
<CopyValueSimple
label="List address"
value={addressToNaddr(decodeAddress(address))}
class="text-neutral-400" />
</div>
</FlexColumn>
</Card>

View File

@ -45,6 +45,7 @@
const submit = async () => {
const relays = hints.WriteRelays().getUrls()
const template = list.event ? editList(list) : createList(list)
console.log(template)
const pub = await createAndPublish({...template, relays})
showInfo("Your list has been saved!")

View File

@ -16,13 +16,14 @@
import NoteContentKind30311 from "src/app/shared/NoteContentKind30311.svelte"
import NoteContentKind30402 from "src/app/shared/NoteContentKind30402.svelte"
import NoteContentKind31337 from "src/app/shared/NoteContentKind31337.svelte"
import NoteContentKind31890 from "src/app/shared/NoteContentKind31890.svelte"
import NoteContentKind31923 from "src/app/shared/NoteContentKind31923.svelte"
import NoteContentKind32123 from "src/app/shared/NoteContentKind32123.svelte"
import NoteContentKind34550 from "src/app/shared/NoteContentKind34550.svelte"
import NoteContentKind35834 from "src/app/shared/NoteContentKind35834.svelte"
import NoteContentKindList from "src/app/shared/NoteContentKindList.svelte"
import {getSetting} from "src/engine"
import {LIST_KINDS} from 'src/domain'
import {LIST_KINDS} from "src/domain"
export let note
export let isQuote = false
@ -74,6 +75,8 @@
<NoteContentKind30402 {note} {showEntire} {showMedia} />
{:else if note.kind === 31337}
<NoteContentKind31337 {note} {showMedia} />
{:else if note.kind === 31890}
<NoteContentKind31890 {note} />
{:else if note.kind === 31923}
<NoteContentKind31923 {note} />
{:else if note.kind === 32123}

View File

@ -0,0 +1,8 @@
<script lang="ts">
import {getAddress} from "@welshman/util"
import FeedCard from "src/app/shared/FeedCard.svelte"
export let note
</script>
<FeedCard address={getAddress(note)} />

View File

@ -40,7 +40,7 @@
actions.push({
onClick: () => router.at("lists/select").qp({type: "p", value: pubkey}).open(),
label: "Add to list",
icon: "scroll",
icon: "list",
})
}

View File

@ -45,7 +45,7 @@
actions.push({
onClick: () => router.at("lists/select").qp({type: "r", value: url}).open(),
label: "Add to list",
icon: "scroll",
icon: "list",
})
actions.push({

View File

@ -14,7 +14,7 @@
actions.push({
onClick: () => router.at("lists/select").qp({type: "t", value: topic}).open(),
label: "Add to list",
icon: "scroll",
icon: "list",
})
}
}

View File

@ -43,5 +43,5 @@
</div>
{/each}
{#if $userFeeds.length === 0 && $userListFeeds.length === 0}
<p class="py-12 text-center">No feeds found.</p>
<p class="py-12 text-center">You don't have any lists yet.</p>
{/if}

View File

@ -1,13 +1,17 @@
<script lang="ts">
import Subheading from 'src/partials/Subheading.svelte'
import Subheading from "src/partials/Subheading.svelte"
import ListForm from "src/app/shared/ListForm.svelte"
import {router} from "src/app/util"
import {makeList} from "src/domain"
const list = makeList()
export let tags = []
const list = makeList({tags})
const hide = tags.length > 0 ? ["type"] : []
const exit = () => router.clearModals()
</script>
<Subheading class="text-center">Create list</Subheading>
<ListForm {list} {exit} />
<ListForm {list} {exit} {hide} />

View File

@ -1,4 +1,5 @@
<script lang="ts">
import {uniqBy, nth} from "@welshman/lib"
import Subheading from "src/partials/Subheading.svelte"
import ListForm from "src/app/shared/ListForm.svelte"
import {router} from "src/app/util"
@ -6,15 +7,18 @@
import {repository} from "src/engine"
export let address
export let tags = []
const event = repository.getEvent(address)
const list = {...readList(event), tags: uniqBy(nth(1), [...event.tags, ...tags])}
const exit = () => router.clearModals()
</script>
{#if event}
<Subheading class="text-center">Edit list</Subheading>
<ListForm showDelete list={readList(event)} {exit} />
<ListForm showDelete {list} {exit} hide={["type"]} />
{:else}
<p class="text-center">Sorry, we weren't able to find that list.</p>
{/if}

View File

@ -16,7 +16,7 @@
<i class="fa fa-plus" /> List
</Anchor>
</div>
{#each $userLists as list}
{#each $userLists as list (getAddress(list.event))}
{@const address = getAddress(list.event)}
<div in:fly={{y: 20}}>
<ListCard {address}>
@ -29,5 +29,5 @@
</div>
{/each}
{#if $userLists.length === 0}
<p class="py-12 text-center">No lists found.</p>
<p class="py-12 text-center">You don't have any lists yet.</p>
{/if}

View File

@ -1,39 +1,55 @@
<script lang="ts">
import {append, randomId} from "@welshman/lib"
import {getAddress} from "@welshman/util"
import {updateIn} from "src/util/misc"
import {first} from "@welshman/lib"
import {getAddress, Tags} from "@welshman/util"
import {defaultTagFeedMappings} from "@welshman/feeds"
import {quantify} from "hurdak"
import Subheading from "src/partials/Subheading.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import ListCard from "src/app/shared/ListCard.svelte"
import Card from "src/partials/Card.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import {router} from "src/app/util/router"
import {pubkey, userLists} from "src/engine"
import {userLists} from "src/engine"
import {displayList} from "src/domain"
export let type
export let value
const label = type === "p" ? "person" : "topic"
const tags = [[type, value]]
const modifyList = updateIn("tags", tags => append([type, value], tags))
const tagTypes = defaultTagFeedMappings.map(first) as string[]
const newList = () => ({address: `30003:${$pubkey}:${randomId()}`, tags: []})
const createList = () => router.at("lists/create").cx({tags}).replaceModal()
const selectlist = list => router.at("lists").of(getAddress(list.event)).at("edit").replaceModal()
const selectList = list =>
router.at("lists").of(getAddress(list.event)).at("edit").cx({tags}).replaceModal()
</script>
<Content size="lg">
<FlexColumn>
<div class="flex items-center justify-between">
<Subheading>Select a List</Subheading>
<Anchor button accent on:click={() => selectlist(newList())}>
<Anchor button accent on:click={createList}>
<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>
<p>Select a list to add your selection to.</p>
{#each $userLists as list (getAddress(list.event))}
<ListCard address={getAddress(list.event)} />
<Card interactive on:click={() => selectList(list)}>
<FlexColumn>
<div class="flex items-center justify-between">
<span class="staatliches flex items-center gap-3 text-xl">
<i class="fa fa-list" />
<span class:text-neutral-400={!list.title}>
{displayList(list)}
</span>
</span>
</div>
{#if list.description}
<p>{list.description}</p>
{/if}
{quantify(Tags.wrap(list.tags).filterByKey(tagTypes).count(), "item")}
</FlexColumn>
</Card>
{:else}
<p class="text-center py-12">You don't have any custom lists yet.</p>
<p class="text-center py-12">You don't have any lists yet.</p>
{/each}
</Content>
</FlexColumn>

View File

@ -13,8 +13,10 @@
$: recent = $publishes.filter(p => p.created_at > now() - seconds(24, "hour"))
$: relays = new Set(recent.flatMap(({request}) => request.relays))
$: pending = recent.filter(p => hasStatus(p, [PublishStatus.Pending]))
$: success = recent.filter(p => hasStatus(p, [PublishStatus.Success]))
$: pending = recent.filter(
p => hasStatus(p, [PublishStatus.Pending]) && !hasStatus(p, [PublishStatus.Success]),
)
</script>
<Subheading>Published Events</Subheading>

View File

@ -1094,7 +1094,7 @@ export const feeds = repository.filter([{kinds: [FEED]}]).derived($events => $ev
export const userFeeds = new Derived([feeds, pubkey], ([$feeds, $pubkey]: [Feed[], string]) =>
sortBy(
prop("title"),
f => f.title.toLowerCase(),
$feeds.filter(feed => feed.event.pubkey === $pubkey),
),
)
@ -1105,7 +1105,7 @@ export const lists = repository
export const userLists = new Derived([lists, pubkey], ([$lists, $pubkey]: [List[], string]) =>
sortBy(
prop("title"),
l => l.title.toLowerCase(),
$lists.filter(list => list.event.pubkey === $pubkey),
),
)
@ -1120,7 +1120,7 @@ export const userListFeeds = new Derived(
[listFeeds, pubkey],
([$listFeeds, $pubkey]: [Feed[], string]) =>
sortBy(
prop("title"),
l => l.title.toLowerCase(),
$listFeeds.filter(feed => feed.list.event.pubkey === $pubkey),
),
)

View File

@ -15,7 +15,11 @@
const share = () => router.at("qrcode").at(value).open()
</script>
<div class={cx($$props.class, "flex items-center gap-2")}>
<i class="fa-solid fa-copy cursor-pointer" on:click={copy} />
<i class="fa-solid fa-qrcode cursor-pointer" on:click={share} />
<div class={cx($$props.class, "flex items-center gap-1")}>
<div class="cursor-pointer px-1 text-neutral-400 transition-colors hover:text-neutral-100">
<i class="fa-solid fa-copy" on:click={copy} />
</div>
<div class="cursor-pointer px-1 text-neutral-400 transition-colors hover:text-neutral-100">
<i class="fa-solid fa-qrcode" on:click={share} />
</div>
</div>

View File

@ -18,6 +18,8 @@ import {
WRAP_NIP04,
ZAP_RESPONSE,
Tags,
decodeAddress,
addressToNaddr,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {schnorr} from "@noble/curves/secp256k1"
@ -146,6 +148,11 @@ export const parseAnything = async entity => {
}
}
// Interpret addresses as naddrs
if (entity.match(/^\d+:\w+:.*$/)) {
entity = addressToNaddr(decodeAddress(entity))
}
try {
return nip19.decode(entity)
} catch (e) {