Work on feed editing

This commit is contained in:
Jon Staab 2024-05-07 10:57:44 -07:00
parent b0107bf89b
commit 3a269db834
23 changed files with 372 additions and 304 deletions

View File

@ -1,7 +1,9 @@
<script lang="ts">
import cx from 'classnames'
import {equals} from 'ramda'
import {fly} from 'src/util/transition'
import {equals} from "ramda"
import {randomId} from "@welshman/lib"
import {Tags} from "@welshman/util"
import {makeScopeFeed, Scope, feedFromTags} from "@welshman/feeds"
import {fly} from "src/util/transition"
import {toggleTheme, theme} from "src/partials/state"
import MenuItem from "src/partials/MenuItem.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
@ -12,7 +14,7 @@
import MenuDesktopSecondary from "src/app/MenuDesktopSecondary.svelte"
import {feed, slowConnections} from "src/app/state"
import {router} from "src/app/util/router"
import {feedFromEvent} from 'src/domain'
import {readFeed, normalizeFeedDefinition} from "src/domain"
import {
env,
user,
@ -23,10 +25,14 @@
sessions,
displayPerson,
displayPubkey,
displayList,
userLists,
userFeeds,
} from "src/engine"
const {page} = router
const followsFeed = normalizeFeedDefinition(makeScopeFeed(Scope.Follows))
const networkFeed = normalizeFeedDefinition(makeScopeFeed(Scope.Network))
const closeSubMenu = () => {
subMenu = null
@ -41,20 +47,50 @@
)
}
const loadFeed = feed => router.at("notes").cx({feed}).push({key: randomId()})
let subMenu
$: isFeedPage = $page.path === "/notes"
$: isFeedPage = Boolean($page.path.match(/^\/(notes)?$/))
$: normalizedFeed = $feed ? normalizeFeedDefinition($feed) : null
</script>
{#if isFeedPage && $userFeeds.length > 0}
<div in:fly={{x: -100, duration: 200}} class="fixed bottom-0 left-72 top-0 w-60 bg-tinted-700 transition-colors text-lg pt-[5.75rem]">
{#if isFeedPage}
<div
in:fly={{x: -100, duration: 200}}
class="fixed bottom-0 left-72 top-0 w-60 bg-tinted-700 pt-24 transition-colors">
<MenuDesktopItem
class="!h-10 !text-lg"
isActive={equals(followsFeed, normalizedFeed)}
on:click={() => loadFeed(followsFeed)}>
Follows
</MenuDesktopItem>
<MenuDesktopItem
class="!h-10 !text-lg"
isActive={equals(networkFeed, normalizedFeed)}
on:click={() => loadFeed(networkFeed)}>
Network
</MenuDesktopItem>
{#each $userFeeds as event}
{@const thisFeed = feedFromEvent(event)}
<MenuDesktopItem isActive={equals(thisFeed.data, $feed)} on:click={() => feed.set(thisFeed.data)}>
{@const thisFeed = readFeed(event)}
<MenuDesktopItem
class="!h-10 !text-lg"
isActive={equals(thisFeed.definition, normalizedFeed)}
on:click={() => loadFeed(thisFeed.definition)}>
{thisFeed.name}
</MenuDesktopItem>
{/each}
<div class="absolute bottom-0 w-full px-7 py-4 h-20 staatliches">
{#each $userLists as list}
{@const definition = feedFromTags(Tags.fromEvent(list))}
<MenuDesktopItem
class="!h-10 !text-lg"
isActive={equals(definition, normalizedFeed)}
on:click={() => loadFeed(definition)}>
{displayList(list)}
</MenuDesktopItem>
{/each}
<div
class="staatliches absolute bottom-0 h-20 w-full px-6 py-4 text-tinted-500 hover:text-tinted-100">
<Anchor href="/feeds">Manage Feeds</Anchor>
</div>
</div>
@ -70,9 +106,12 @@
? import.meta.env.VITE_APP_WORDMARK_DARK
: import.meta.env.VITE_APP_WORDMARK_LIGHT} />
</Anchor>
<MenuDesktopItem path="/notes" isActive={$page.path.startsWith("/notes")} isAlt={isFeedPage}>Feed</MenuDesktopItem>
<MenuDesktopItem path="/notes" isActive={isFeedPage} isAlt={isFeedPage}>Feeds</MenuDesktopItem>
{#if !$env.FORCE_GROUP && $env.PLATFORM_RELAYS.length === 0}
<MenuDesktopItem path="/settings/relays" isActive={$page.path.startsWith("/settings/relays")} isAlt={isFeedPage}>
<MenuDesktopItem
path="/settings/relays"
isActive={$page.path.startsWith("/settings/relays")}
isAlt={isFeedPage}>
<div class="relative inline-block">
Relays
{#if $slowConnections.length > 0}
@ -81,7 +120,11 @@
</div>
</MenuDesktopItem>
{/if}
<MenuDesktopItem path="/notifications" disabled={!$canSign} isActive={$page.path.startsWith("/notifications")} isAlt={isFeedPage}>
<MenuDesktopItem
path="/notifications"
disabled={!$canSign}
isActive={$page.path.startsWith("/notifications")}
isAlt={isFeedPage}>
<div class="relative inline-block">
Notifications
{#if $hasNewNotifications}
@ -89,7 +132,11 @@
{/if}
</div>
</MenuDesktopItem>
<MenuDesktopItem path="/channels" disabled={!$canSign} isActive={$page.path.startsWith("/channels")} isAlt={isFeedPage}>
<MenuDesktopItem
path="/channels"
disabled={!$canSign}
isActive={$page.path.startsWith("/channels")}
isAlt={isFeedPage}>
<div class="relative inline-block">
Messages
{#if $hasNewMessages}
@ -97,12 +144,17 @@
{/if}
</div>
</MenuDesktopItem>
<MenuDesktopItem path="/events" isActive={$page.path.startsWith("/events")} isAlt={isFeedPage}>Calendar</MenuDesktopItem>
<MenuDesktopItem path="/events" isActive={$page.path.startsWith("/events")} isAlt={isFeedPage}
>Calendar</MenuDesktopItem>
{#if $env.ENABLE_MARKET}
<MenuDesktopItem path="/listings" isActive={$page.path.startsWith("/listings")} isAlt={isFeedPage}>Market</MenuDesktopItem>
<MenuDesktopItem
path="/listings"
isActive={$page.path.startsWith("/listings")}
isAlt={isFeedPage}>Market</MenuDesktopItem>
{/if}
{#if !$env.FORCE_GROUP}
<MenuDesktopItem path="/groups" isActive={$page.path.startsWith("/groups")} isAlt={isFeedPage}>Groups</MenuDesktopItem>
<MenuDesktopItem path="/groups" isActive={$page.path.startsWith("/groups")} isAlt={isFeedPage}
>Groups</MenuDesktopItem>
{/if}
<FlexColumn small class="absolute bottom-0 w-72">
<Anchor
@ -185,7 +237,7 @@
</MenuItem>
</MenuDesktopSecondary>
{/if}
<div class="cursor-pointer border-t border-solid border-neutral-600 px-7 py-4 h-20">
<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} />

View File

@ -17,12 +17,12 @@
</script>
<Anchor {...$$props} randomizeKey class={className} href={path} on:click>
<div class="absolute left-6 flex gap-5 whitespace-nowrap pt-2" class:-right-6={isActive}>
<div class="absolute left-6 flex gap-5 whitespace-nowrap pt-2" class:-right-3={isActive}>
<slot />
{#if isActive}
<div
in:fly|local={{x: 50, duration: 1000, easing: elasticOut}}
class="relative top-4 h-px w-full bg-accent mr-3" />
class="relative top-4 h-px w-full bg-accent" />
{/if}
</div>
</Anchor>

View File

@ -1,9 +0,0 @@
<script lang="ts">
import Anchor from "src/partials/Anchor.svelte"
const className = "relative staatliches h-12 block transition-all text-neutral-300 hover:text-tinted-200"
</script>
<Anchor {...$$props} class={className} on:click>
<slot />
</Anchor>

View File

@ -119,7 +119,7 @@
</div>
</MenuMobileItem>
<MenuMobileItem href="/notes" on:click={closeMenu}>
<i class="fa fa-rss" /> Feed
<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">

View File

@ -1,5 +1,5 @@
<script lang="ts">
import cx from 'classnames'
import cx from "classnames"
import {reverse} from "ramda"
import logger from "src/util/logger"
import Modal from "src/partials/Modal.svelte"
@ -11,6 +11,8 @@
let prevPage
$: isFeedPage = $page.path.match(/^\/(notes)?$/)
$: {
if ($modal) {
logger.info("modal", $modal, router.getProps($modal))
@ -59,9 +61,9 @@
<div
id="page"
class={cx("relative pb-32 text-neutral-100 lg:pt-16", {
'lg:ml-60': $page?.path !== "/notes",
'lg:ml-[33rem]': $page?.path === "/notes",
'pointer-events-none': $menuIsOpen,
"lg:ml-60": !isFeedPage,
"lg:ml-[33rem]": isFeedPage,
"pointer-events-none": $menuIsOpen,
})}>
{#if $page}
{@const promise = router.getMatch($page.path).route.component}

View File

@ -12,6 +12,7 @@
import {FeedLoader} from "src/app/util"
export let feed: Feed
export let address = null
export let anchor = null
export let eager = false
export let skipCache = false
@ -78,7 +79,7 @@
</script>
{#if showControls}
<FeedControls bind:value={opts} />
<FeedControls {address} bind:opts />
{/if}
<FlexColumn xl bind:element>

View File

@ -1,39 +1,38 @@
<script lang="ts">
import {quantify, displayList} from "hurdak"
import {isNil, randomId, clamp} from "@welshman/lib"
import {Tags, Kind, getAddress} from "@welshman/util"
import {debounce} from "throttle-debounce"
import {Tags, decodeAddress} from "@welshman/util"
import {
FeedType,
isScopeFeed,
isSearchFeed,
isAuthorFeed,
makeSearchFeed,
makeScopeFeed,
makeIntersectionFeed,
Scope,
hasSubFeeds,
getFeedArgs,
feedsFromTags,
feedFromTags,
} from "@welshman/feeds"
import {slide} from "src/util/transition"
import {getStringWidth} from "src/util/misc"
import Modal from "src/partials/Modal.svelte"
import Textarea from "src/partials/Textarea.svelte"
import Subheading from "src/partials/Subheading.svelte"
import Field from "src/partials/Field.svelte"
import Input from "src/partials/Input.svelte"
import Popover from "src/partials/Popover.svelte"
import Select from "src/partials/Select.svelte"
import Popover2 from "src/partials/Popover2.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Menu from "src/partials/Menu.svelte"
import MenuItem from "src/partials/MenuItem.svelte"
import Chip from "src/partials/Chip.svelte"
import Toggle from "src/partials/Toggle.svelte"
import FeedField from "src/app/shared/FeedField.svelte"
import FeedSummary from "src/app/shared/FeedSummary.svelte"
import {router} from "src/app/util"
import {hints, createAndPublish, displayList as displayList2, userLists} from "src/engine"
import {normalizeFeedDefinition, readFeed, initFeed, editFeed, createFeed} from "src/domain"
import {
hints,
repository,
createAndPublish,
displayList as displayList2,
userLists,
userFeeds,
} from "src/engine"
export let value
export let opts
export let address = null
const openListMenu = () => {
listMenuIsOpen = true
@ -59,106 +58,111 @@
nameIsOpen = false
}
const showSearch = () => {
search = search || ""
searchFocused = true
}
const hideSearch = () => {
search = null
}
const toggleReplies = () => {
value = {...value, shouldHideReplies: !value.shouldHideReplies}
opts = {...opts, shouldHideReplies: !opts.shouldHideReplies}
}
const getSearch = feed => getFeedArgs(feed)?.find(isSearchFeed)?.[1] as string | null
const getSearch = definition => (getFeedArgs(definition)?.find(isSearchFeed)?.[1] as string) || ""
const onDraftFeedChange = feed => {
draftFeed = feed
const onDraftFeedChange = definition => {
feed.definition = definition
}
const setFeed = feed => {
draftFeed = feed
value = {...value, feed}
search = getSearch(feed)
const setFeedDefinition = definition => {
opts = {...opts, feed: definition}
search = getSearch(definition)
closeListMenu()
closeForm()
closeName()
}
const setSubFeed = subFeed => {
const idx = feed.findIndex(f => f[0] === subFeed[0])
const idx = feed.definition.findIndex(f => f[0] === subFeed[0])
setFeed(idx >= 0 ? feed.toSpliced(idx, 1, subFeed) : [...feed, subFeed])
setFeedDefinition(
idx >= 0 ? feed.definition.toSpliced(idx, 1, subFeed) : [...feed.definition, subFeed],
)
}
const removeSubFeed = subFeed => {
setFeed(feed.filter(f => f !== subFeed))
setFeedDefinition(feed.definition.filter(f => f !== subFeed))
}
const saveFeed = async feed => {
const pub = await createAndPublish({
kind: Kind.Feed,
content: JSON.stringify(feed),
tags: [
["d", randomId()],
["name", name],
],
relays: hints.WriteRelays().getUrls(),
const setFeed = event => {
feed = readFeed(event)
setFeedDefinition(feed.definition)
}
const setList = list => {
feed = initFeed({
name: list.title,
list: list.address,
description: list.description,
definition: feedFromTags(Tags.fromEvent(list)),
identifier: decodeAddress(list.address).identifier,
})
address = getAddress(pub.request.event)
setFeed(feed)
setFeedDefinition(feed.definition)
}
const onSearchFocus = () => {
searchFocused = true
const saveFeed = async () => {
const relays = hints.WriteRelays().getUrls()
const template = feed.event ? editFeed(feed) : createFeed(feed)
const pub = await createAndPublish({...template, relays})
setFeed(pub.request.event)
}
const onSearchBlur = () => {
const onSearchBlur = debounce(500, () => {
const text = search.trim()
searchFocused = false
if (!text) {
hideSearch()
}
if (text) {
setSubFeed(makeSearchFeed(text))
} else {
removeSubFeed(subFeeds.find(isSearchFeed))
}
}
})
const loadList = list => setFeed(makeIntersectionFeed(...feedsFromTags(Tags.fromEvent(list))))
const normalize = feed => (hasSubFeeds(feed) ? feed : makeIntersectionFeed(feed))
let address = null
let formIsOpen = false
let nameIsOpen = false
let listMenuIsOpen = false
let searchFocused = false
let name = ""
let search = getSearch(value.feed)
let draftFeed = normalize(value.feed)
let feed = address
? readFeed(repository.getEvent(address))
: initFeed({definition: normalizeFeedDefinition(opts.feed)})
let search = getSearch(feed.definition)
$: feed = normalize(value.feed)
$: subFeeds = getFeedArgs(feed)
$: currentScopeFeed = subFeeds.find(f => isScopeFeed(f) || isAuthorFeed(f))
$: subFeeds = getFeedArgs(feed.definition as any)
</script>
<div class="-mb-2">
<div class="float-right flex h-8 items-center justify-end">
<div class="flex items-center gap-1 px-2">
<Toggle scale={0.6} value={!value.shouldHideReplies} on:change={toggleReplies} />
<small class="text-neutral-200">Show replies</small>
<div class="flex justify-between">
<Select
value={subFeeds.find(isScopeFeed)?.[1] || null}
class="hidden h-7 bg-tinted-700 text-neutral-200 sm:block">
<option value={Scope.Follows}>Follows</option>
<option value={Scope.Network}>Network</option>
<option value={null}>Global</option>
</Select>
<div class="flex flex-grow items-center justify-end gap-2">
<div class="flex">
<Input class="hidden h-7 bg-neutral-900 xs:block" on:input={onSearchBlur} bind:value={search}>
<div slot="after" class="hidden text-white xs:block">
<i class="fa fa-search" />
</div>
</Input>
<Anchor button low class="h-7 border-none xs:rounded-l-none" on:click={openForm}>
Filters ({feed.definition.length - 1})
</Anchor>
</div>
<div class="float-right flex h-8 items-center justify-end gap-2">
{#if opts.shouldHideReplies}
<Anchor button low class="h-7 border-none opacity-50" on:click={toggleReplies}
>Replies</Anchor>
{:else}
<Anchor button accent class="h-7 border-none" on:click={toggleReplies}>Replies</Anchor>
{/if}
<div class="relative lg:hidden">
<div
class="w-6 cursor-pointer rounded bg-neutral-700 text-center text-neutral-50 transition-colors hover:bg-neutral-600"
class="flex h-7 w-6 cursor-pointer items-center justify-center rounded bg-neutral-700 text-center text-neutral-50 transition-colors hover:bg-neutral-600"
on:click={openListMenu}>
<i class="fa fa-sm fa-ellipsis-v" />
</div>
@ -172,8 +176,11 @@
</Anchor>
</MenuItem>
<div class="max-h-96 overflow-auto">
{#each $userFeeds as event}
<MenuItem on:click={() => setFeed(event)}>{readFeed(event).name}</MenuItem>
{/each}
{#each $userLists as list}
<MenuItem on:click={() => loadList(list)}>{displayList2(list)}</MenuItem>
<MenuItem on:click={() => setList(list)}>{displayList2(list)}</MenuItem>
{/each}
</div>
</Menu>
@ -181,92 +188,22 @@
{/if}
</div>
</div>
<div class="mb-2 mr-2 inline-block py-1">Showing notes:</div>
{#if feed[0] !== FeedType.Intersection}
<Chip class="mb-2 mr-2 inline-block">
Custom feed ({quantify(subFeeds.length, "selection")})
</Chip>
<Chip class="cursor-pointer" on:click={openForm}>
<div class="flex h-6 items-center justify-center">
<i class="fa fa-cog" />
</div>
</Chip>
{:else}
<Popover
class="inline-block"
placement="bottom-end"
theme="transparent"
opts={{hideOnClick: true}}>
<div slot="trigger" class="cursor-pointer">
<Chip class="mb-2 mr-2 inline-block">
{#if currentScopeFeed && isScopeFeed(currentScopeFeed)}
From {displayList(getFeedArgs(currentScopeFeed))}
{:else if currentScopeFeed && isAuthorFeed(currentScopeFeed)}
From {quantify(getFeedArgs(currentScopeFeed).length, "author")}
{:else}
From global
{/if}
<i class="fa fa-caret-down p-1" />
</Chip>
</div>
<div slot="tooltip">
<Menu>
<MenuItem on:click={() => setSubFeed(makeScopeFeed(Scope.Follows))}>
<i class="fa fa-user-plus mr-2" /> Follows
</MenuItem>
<MenuItem on:click={() => setSubFeed(makeScopeFeed(Scope.Network))}>
<i class="fa fa-share-nodes mr-2" /> Network
</MenuItem>
<MenuItem on:click={() => removeSubFeed(currentScopeFeed)}>
<i class="fa fa-earth-americas mr-2" /> Global
</MenuItem>
<MenuItem on:click={openForm}>
<i class="fa fa-cog mr-2" /> Customize
</MenuItem>
</Menu>
</div>
</Popover>
{#each subFeeds as subFeed}
{#if ![FeedType.Search, FeedType.Scope, FeedType.Author].includes(subFeed[0])}
<FeedSummary feed={subFeed} />
{/if}
{/each}
<Chip class="cursor-pointer">
<div class="flex items-center">
<div class="flex h-6 w-4 items-center justify-center" on:click={showSearch}>
<i class="fa fa-search" />
</div>
{#if !isNil(search)}
{@const min = searchFocused ? 60 : 0}
{@const width = getStringWidth(search)}
<input
autofocus
class="bg-transparent pl-1 outline-none"
class:transition-all={width < min || !searchFocused}
style={`width: ${clamp([min, 150], width) + 10}px`}
transition:slide|local={{axis: "x", duration: 200}}
bind:value={search}
on:focus={onSearchFocus}
on:blur={onSearchBlur} />
{/if}
</div>
</Chip>
{/if}
</div>
{#if formIsOpen}
<Modal onEscape={closeForm}>
{#if address}
<Subheading>Edit {name}</Subheading>
{#if event}
<Subheading>Edit {feed.name}</Subheading>
{:else}
<Subheading>Customize your feed</Subheading>
{/if}
<FeedField feed={draftFeed} onChange={onDraftFeedChange} />
<FeedField feed={feed.definition} onChange={onDraftFeedChange} />
<div class="flex justify-between gap-2">
<Anchor button on:click={closeForm}>Discard</Anchor>
<div class="flex gap-2">
<Anchor button on:click={openName}>Save Feed</Anchor>
<Anchor button accent on:click={() => setFeed(draftFeed)}>Done</Anchor>
<Anchor button accent on:click={() => setFeedDefinition(feed.definition)}>Done</Anchor>
</div>
</div>
</Modal>
@ -275,11 +212,14 @@
{#if nameIsOpen}
<Modal onEscape={closeName}>
<Field label="What would you like to name this feed?">
<Input bind:value={name} />
<Input bind:value={feed.name} />
</Field>
<Field label="How would you describe this feed?">
<Textarea bind:value={feed.description} />
</Field>
<div class="flex justify-between gap-2">
<Anchor button on:click={closeName}>Cancel</Anchor>
<Anchor button accent on:click={() => saveFeed(draftFeed)}>Save</Anchor>
<Anchor button accent on:click={saveFeed}>Save</Anchor>
</div>
</Modal>
{/if}

View File

@ -14,6 +14,7 @@
export let feed
export let onChange = null
export let hideType = false
enum FormType {
Advanced = "advanced",
@ -76,6 +77,7 @@
</script>
<FlexColumn>
{#if !hideType}
<Card>
<Field label="Choose a feed type">
<SelectTiles
@ -84,7 +86,10 @@
value={formType}>
<div slot="item" class="flex flex-col items-center" let:option let:active>
{#if option === FormType.People}
<Icon icon="people-nearby" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
<Icon
icon="people-nearby"
class="h-12 w-12"
color={active ? "accent" : "tinted-800"} />
<span class="staatliches text-2xl">People</span>
{:else if option === FormType.Topics}
<span class="flex h-12 w-12 items-center justify-center" class:text-accent={active}>
@ -102,9 +107,11 @@
</SelectTiles>
</Field>
<div class="flex justify-end">
<Anchor underline on:click={() => onFormTypeChange(FormType.Advanced)}>Advanced mode</Anchor>
<Anchor underline on:click={() => onFormTypeChange(FormType.Advanced)}
>Advanced mode</Anchor>
</div>
</Card>
{/if}
{#if formType === FormType.People}
<FeedFormPeople feed={normalize(feed)} onChange={onFeedChange} />
{:else if formType === FormType.Topics}

View File

@ -38,7 +38,7 @@
Tags.fromEvent(result)
.filterByKey(["e", "a"])
.rejectByValue([pub.request.event.id])
.mapTo(t => hints.selection(t.value(), [t.mark()]))
.mapTo(t => hints.selection(t.value(), [t.nth(2)]))
.valueOf(),
)

View File

@ -64,7 +64,7 @@
const likesCount = tweened(0, {interpolate})
const zapsTotal = tweened(0, {interpolate})
const repliesCount = tweened(0, {interpolate})
const handler = handlers.key(tags.get("client")?.mark())
const handler = handlers.key(tags.get("client")?.nth(2))
const seenOn = tracker.data.derived(m =>
Array.from(m.get(note.id) || []).filter(url => url !== LOCAL_RELAY_URL),
)
@ -218,7 +218,7 @@
"pointer-events-none opacity-50": disableActions,
})}
on:click={replyCtrl?.start}>
<Icon icon="message" color={reply ? 'accent' : 'neutral-100'} />
<Icon icon="message" color={reply ? "accent" : "neutral-100"} />
{#if $repliesCount > 0}
<span transition:fly|local={{y: 5, duration: 100}} class="-mt-px">{$repliesCount}</span>
{/if}
@ -229,7 +229,7 @@
"pointer-events-none opacity-50": disableActions || !canZap,
})}
on:click={startZap}>
<Icon icon="bolt" color={zap ? 'accent' : 'neutral-100'} />
<Icon icon="bolt" color={zap ? "accent" : "neutral-100"} />
{#if $zapsTotal > 0}
<span transition:fly|local={{y: 5, duration: 100}} class="-mt-px"
>{formatSats($zapsTotal)}</span>
@ -244,7 +244,7 @@
on:click={() => (like ? deleteReaction(like) : react("+"))}>
<Icon
icon="heart"
color={like ? 'accent' : 'neutral-100'}
color={like ? "accent" : "neutral-100"}
class={cx("cursor-pointer", {
"fa-beat fa-beat-custom": like,
})} />

View File

@ -21,7 +21,11 @@
for (const e of events) {
const tags = Tags.fromEvent(e)
const topic = tags.whereKey("l").whereMark("#t").values().first()
const topic = tags
.whereKey("l")
.filter(t => t.last() === "#t")
.values()
.first()
if (!topic) {
continue

View File

@ -1,44 +1,73 @@
<script lang="ts">
import {randomId} from "@welshman/lib"
import {Kind, getAddress} from "@welshman/util"
import {makeIntersectionFeed, makeScopeFeed, Scope} from "@welshman/feeds"
import Card from "src/partials/Card.svelte"
import Field from "src/partials/Field.svelte"
import Subheading from "src/partials/Subheading.svelte"
import Modal from "src/partials/Modal.svelte"
import Input from "src/partials/Input.svelte"
import Textarea from "src/partials/Textarea.svelte"
import Anchor from "src/partials/Anchor.svelte"
import FeedField from "src/app/shared/FeedField.svelte"
import {router} from "src/app/util"
import {hints, createAndPublish} from "src/engine"
import {initFeed, readFeed, createFeed, editFeed} from "src/domain"
import {publishDeletionForEvent, repository, hints, createAndPublish} from "src/engine"
export let address = null
const values = address ? readFeed(repository.getEvent(address)) : initFeed()
const abort = () => router.pop()
const saveFeed = async () => {
const pub = await createAndPublish({
kind: Kind.Feed,
content: JSON.stringify(feed),
tags: [
["d", randomId()],
["name", name],
],
relays: hints.WriteRelays().getUrls(),
})
address = getAddress(pub.request.event)
const openDelete = () => {
deleteIsOpen = true
}
let name = ""
let feed = makeIntersectionFeed(makeScopeFeed(Scope.Follows))
const closeDelete = () => {
deleteIsOpen = false
}
const confirmDelete = () => {
publishDeletionForEvent(event)
router.at("feeds").push()
}
const saveFeed = async () => {
const relays = hints.WriteRelays().getUrls()
const template = values.event ? editFeed(values) : createFeed(values)
await createAndPublish({...template, relays})
}
let deleteIsOpen = false
</script>
<FeedField bind:feed />
<FeedField hideType={address} bind:feed={values.definition} />
<Card>
<Field label="What would you like to name this feed?">
<Input bind:value={name} />
<Input bind:value={values.name} />
</Field>
<Field label="How would you describe this feed?">
<Textarea bind:value={values.description} />
</Field>
</Card>
<div class="flex justify-between gap-2">
<Anchor button on:click={abort}>Discard</Anchor>
<div class="flex justify-between gap-2">
{#if values.event}
<Anchor button danger on:click={openDelete}>Delete</Anchor>
{/if}
<Anchor button accent on:click={saveFeed}>Save</Anchor>
</div>
</div>
{#if deleteIsOpen}
<Modal onEscape={closeDelete}>
<Subheading>Confirm deletion</Subheading>
<p>
Are you sure you want to delete your "{values.name}" feed?
</p>
<div class="flex gap-2">
<Anchor button on:click={closeDelete}>Cancel</Anchor>
<Anchor button accent on:click={confirmDelete}>Confirm</Anchor>
</div>
</Modal>
{/if}

View File

@ -6,8 +6,8 @@
import Card from "src/partials/Card.svelte"
import FeedSummary from "src/app/shared/FeedSummary.svelte"
import {router} from "src/app/util/router"
import {feedFromEvent} from "src/domain"
import {userFeeds, publishDeletion} from "src/engine"
import {readFeed} from "src/domain"
import {userFeeds} from "src/engine"
const createFeed = () => router.at("feeds/create").open()
@ -22,14 +22,14 @@
</div>
{#each $userFeeds as event (getAddress(event))}
{@const address = getAddress(event)}
{@const feed = feedFromEvent(event)}
{@const {name, description, definition} = readFeed(event)}
<Card>
<FlexColumn>
<div class="flex justify-between items-center">
<div class="flex items-center justify-between">
<span class="staatliches flex items-center gap-3 text-xl">
<i class="fa fa-rss" />
{#if feed.name}
{feed.name}
{#if name}
{name}
{:else}
<span class="text-neutral-500">No name</span>
{/if}
@ -38,10 +38,10 @@
<i class="fa fa-edit" /> Edit
</Anchor>
</div>
{#if feed.description}
<p>{feed.description}</p>
{#if description}
<p>{description}</p>
{/if}
<FeedSummary feed={feed.data} />
<FeedSummary feed={definition} />
</FlexColumn>
</Card>
{:else}

View File

@ -3,16 +3,14 @@
import Anchor from "src/partials/Anchor.svelte"
import Feed from "src/app/shared/Feed.svelte"
import {router} from "src/app/util/router"
import {feed} from 'src/app/state'
import {feed as feedStore} from "src/app/state"
import {session} from "src/engine"
export let address = null
export let feed = makeScopeFeed(Scope.Follows)
export let relays = []
feed.set(
relays.length > 0
? makeIntersectionFeed(makeRelayFeed(...relays), makeScopeFeed(Scope.Follows))
: makeScopeFeed(Scope.Follows)
)
feedStore.set(relays.length > 0 ? makeIntersectionFeed(makeRelayFeed(...relays), feed) : feed)
const showLogin = () => router.at("login").open()
@ -28,4 +26,4 @@
</div>
{/if}
<Feed skipCache showControls showGroup bind:feed={$feed} />
<Feed skipCache showControls showGroup bind:feed={$feedStore} {address} />

View File

@ -5,7 +5,7 @@
</script>
{#if $env.FORCE_GROUP}
<GroupDetail address={$env.FORCE_GROUP} activeTab="notes" />
<GroupDetail {...$$props} address={$env.FORCE_GROUP} activeTab="notes" />
{:else}
<Feeds />
<Feeds {...$$props} />
{/if}

View File

@ -1,11 +1,52 @@
import {fromPairs} from "@welshman/lib"
import {fromPairs, randomId} from "@welshman/lib"
import {Kind, Tags} from "@welshman/util"
import type {Rumor} from "@welshman/util"
import {makeIntersectionFeed} from "@welshman/feeds"
import {makeIntersectionFeed, hasSubFeeds} from "@welshman/feeds"
import type {Feed as IFeed} from "@welshman/feeds"
import {tryJson} from "src/util/misc"
export const feedFromEvent = (event: Rumor) => {
const {d, name = "", description = "", feed = ""} = fromPairs(event.tags)
const data = tryJson(() => JSON.parse(feed)) || makeIntersectionFeed()
return {name, description, data, event}
export type Feed = {
name: string
identifier: string
description: string
definition: IFeed
event?: Rumor
list?: string
}
export const normalizeFeedDefinition = feed =>
hasSubFeeds(feed) ? feed : makeIntersectionFeed(feed)
export const initFeed = (feed: Partial<Feed> = {}): Feed => ({
name: "",
description: "",
identifier: randomId(),
definition: makeIntersectionFeed(),
...feed,
})
export const readFeed = (event: Rumor): Feed => {
const {d: identifier, name = "", description = "", feed = ""} = fromPairs(event.tags)
const definition = tryJson(() => JSON.parse(feed)) || makeIntersectionFeed()
return {name, identifier, description, definition, event}
}
export const createFeed = ({identifier, definition, name, description}: Feed) => ({
kind: Kind.Feed,
content: description,
tags: [
["d", identifier],
["name", name],
["feed", JSON.stringify(definition)],
],
})
export const editFeed = (feed: Feed) => ({
kind: Kind.Feed,
content: feed.description,
tags: Tags.fromEvent(feed.event)
.setTag("name", feed.name)
.setTag("feed", JSON.stringify(feed.definition))
.unwrap(),
})

View File

@ -31,7 +31,7 @@ export const deriveHandlers = cached({
}
const tags = Tags.fromEvent(event).whereKey("a")
const tag = tags.whereMark("web").first() || tags.first()
const tag = tags.filter(t => t.last() === "web").first() || tags.first()
const address = tag?.value()
const handler = $handlers.get(address)

View File

@ -18,7 +18,8 @@ export const publishBookmarksList = (id, title, description, tags) => {
})
// migrate away from kind 30001
publishDeletion([`30001:${pubkey.get()}:${name}`])
publishDeletion([`30001:${pubkey.get()}:${id}`])
publishDeletion([`30001:${pubkey.get()}:${title}`])
}
export const publishCommunitiesList = addresses =>

View File

@ -1,4 +1,4 @@
import {max, filter, prop, omit, partition, equals} from "ramda"
import {max, prop, omit, partition, equals} from "ramda"
import {sleep, pickVals} from "hurdak"
import {Worker, derived} from "@welshman/lib"
import type {Rumor} from "@welshman/util"

View File

@ -6,7 +6,11 @@ import {addTopic, processTopics} from "./commands"
projections.addHandler(1, processTopics)
projections.addHandler(1985, (e: Event) => {
for (const name of Tags.fromEvent(e).whereKey("l").whereMark("#t").values().valueOf()) {
for (const name of Tags.fromEvent(e)
.whereKey("l")
.filter(t => t.last() === "#t")
.values()
.valueOf()) {
addTopic(e, name)
}
})

View File

@ -36,7 +36,7 @@
underline: underline,
"opacity-50 pointer-events-none": loading || disabled,
"bg-white text-black hover:bg-white-l": button && !accent && !low,
"text-base bg-tinted-700 text-tinted-200 hover:bg-white-l border border-solid border-tinted-600":
"text-base bg-tinted-700 text-tinted-200 hover:bg-tinted-600 border border-solid border-tinted-600":
button && low,
"bg-accent text-white hover:bg-accent": button && accent,
"text-danger border border-solid !border-danger": button && danger,

View File

@ -15,8 +15,7 @@
const showAfter = $$slots.after && !hideAfter
const className = cx(
$$props.class,
"rounded shadow-inset py-2 px-4 w-full placeholder:text-neutral-400",
"bg-white text-black",
"outline-none rounded shadow-inset py-2 px-4 w-full placeholder:text-neutral-400 text-neutral-900",
{"pl-10": showBefore, "pr-10": showAfter},
)
@ -40,7 +39,7 @@
on:input
on:keydown />
{#if showBefore}
<div class="absolute left-0 top-0 flex gap-2 px-4 pt-2 text-black opacity-75">
<div class="absolute left-0 top-0 flex items-center gap-2 px-4 text-black opacity-75">
<div>
<slot name="before" />
</div>
@ -48,7 +47,7 @@
{/if}
{#if showAfter}
<div
class="absolute right-0 top-0 m-px flex gap-2 rounded-full px-4 pt-2 text-black opacity-75">
class="absolute right-0 top-0 m-px flex items-center gap-2 rounded-full px-4 text-black opacity-75">
<div>
<slot name="after" />
</div>

View File

@ -5,11 +5,10 @@
export let onChange = null
export let wrapperClass = ""
const className = cx(
$$props.class,
"rounded text-neutral-100 shadow-inset py-2 px-4 pr-10 text-black w-full text-tinted-700",
{"pl-10": $$slots.before, "pr-10": $$slots.after},
)
const className = cx($$props.class, "rounded shadow-inset px-4 w-full cursor-pointer", {
"pl-10": $$slots.before,
"pr-10": $$slots.after,
})
</script>
<div class={cx(wrapperClass, "relative")}>