mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Work on feed editing
This commit is contained in:
parent
b0107bf89b
commit
3a269db834
@ -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} />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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">
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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(),
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
})} />
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
|
@ -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}
|
||||
|
@ -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(),
|
||||
})
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 =>
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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")}>
|
||||
|
Loading…
Reference in New Issue
Block a user