mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Compare commits
7 Commits
3e3d8ab618
...
e212297396
Author | SHA1 | Date | |
---|---|---|---|
|
e212297396 | ||
|
c9db37c92a | ||
|
148a63d95f | ||
|
a5517f2eff | ||
|
b92b0b71b5 | ||
|
c6cdcfb2f8 | ||
|
c80988882b |
@ -10,6 +10,8 @@
|
|||||||
- [x] Fix several community and calendar related bugs
|
- [x] Fix several community and calendar related bugs
|
||||||
- [x] Add reports using tagr-bot
|
- [x] Add reports using tagr-bot
|
||||||
- [x] Open links to coracle in same tab
|
- [x] Open links to coracle in same tab
|
||||||
|
- [x] Add global feeds
|
||||||
|
- [x] Add feed favorites
|
||||||
|
|
||||||
# 0.4.6
|
# 0.4.6
|
||||||
|
|
||||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -56,10 +56,10 @@
|
|||||||
"@getalby/bitcoin-connect": "^3.2.2",
|
"@getalby/bitcoin-connect": "^3.2.2",
|
||||||
"@scure/base": "^1.1.6",
|
"@scure/base": "^1.1.6",
|
||||||
"@welshman/content": "^0.0.5",
|
"@welshman/content": "^0.0.5",
|
||||||
"@welshman/feeds": "^0.0.11",
|
"@welshman/feeds": "^0.0.12",
|
||||||
"@welshman/lib": "^0.0.9",
|
"@welshman/lib": "^0.0.10",
|
||||||
"@welshman/net": "^0.0.13",
|
"@welshman/net": "^0.0.14",
|
||||||
"@welshman/util": "^0.0.14",
|
"@welshman/util": "^0.0.15",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
|
@ -236,3 +236,9 @@ body,
|
|||||||
.react-switch-bg {
|
.react-switch-bg {
|
||||||
border: 1px solid var(--neutral-600);
|
border: 1px solid var(--neutral-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* note content */
|
||||||
|
|
||||||
|
.note-content a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
import {NAMED_BOOKMARKS, toNostrURI, Address} from "@welshman/util"
|
import {NAMED_BOOKMARKS, toNostrURI, Address} from "@welshman/util"
|
||||||
import {slide} from "src/util/transition"
|
import {slide} from "src/util/transition"
|
||||||
import {boolCtrl} from "src/partials/utils"
|
import {boolCtrl} from "src/partials/utils"
|
||||||
@ -9,8 +10,15 @@
|
|||||||
import CopyValueSimple from "src/partials/CopyValueSimple.svelte"
|
import CopyValueSimple from "src/partials/CopyValueSimple.svelte"
|
||||||
import FeedSummary from "src/app/shared/FeedSummary.svelte"
|
import FeedSummary from "src/app/shared/FeedSummary.svelte"
|
||||||
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
|
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
|
||||||
import {readFeed, readList, displayFeed, mapListToFeed} from "src/domain"
|
import {readFeed, readList, displayFeed, mapListToFeed, getSingletonValues} from "src/domain"
|
||||||
import {repository} from "src/engine"
|
import {
|
||||||
|
hints,
|
||||||
|
pubkey,
|
||||||
|
repository,
|
||||||
|
addFeedFavorite,
|
||||||
|
removeFeedFavorite,
|
||||||
|
userFeedFavorites,
|
||||||
|
} from "src/engine"
|
||||||
import {globalFeed} from "src/app/state"
|
import {globalFeed} from "src/app/state"
|
||||||
import {router} from "src/app/util"
|
import {router} from "src/app/util"
|
||||||
|
|
||||||
@ -19,14 +27,19 @@
|
|||||||
const expandDefinition = boolCtrl()
|
const expandDefinition = boolCtrl()
|
||||||
const event = repository.getEvent(address)
|
const event = repository.getEvent(address)
|
||||||
const deleted = repository.isDeleted(event)
|
const deleted = repository.isDeleted(event)
|
||||||
|
const naddr = Address.from(address, hints.Event(event).getUrls()).toNaddr()
|
||||||
const feed = address.startsWith(NAMED_BOOKMARKS)
|
const feed = address.startsWith(NAMED_BOOKMARKS)
|
||||||
? mapListToFeed(readList(event))
|
? mapListToFeed(readList(event))
|
||||||
: readFeed(event)
|
: readFeed(event)
|
||||||
|
|
||||||
|
const toggleFavorite = () => (isFavorite ? removeFeedFavorite(address) : addFeedFavorite(address))
|
||||||
|
|
||||||
const loadFeed = () => {
|
const loadFeed = () => {
|
||||||
globalFeed.set(feed)
|
globalFeed.set(feed)
|
||||||
router.at("notes").push()
|
router.at("notes").push()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: isFavorite = getSingletonValues("a", $userFeedFavorites).has(address)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card class="flex gap-3">
|
<Card class="flex gap-3">
|
||||||
@ -69,7 +82,15 @@
|
|||||||
<i class="fa fa-angle-right" />
|
<i class="fa fa-angle-right" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<CopyValueSimple label="Feed address" value={toNostrURI(Address.from(address).toNaddr())} />
|
<div
|
||||||
|
class={cx("p-1 text-neutral-400 transition-colors hover:text-neutral-100", {
|
||||||
|
"cursor-pointer": feed.event.pubkey !== $pubkey,
|
||||||
|
"pointer-events-none opacity-25": feed.event.pubkey === $pubkey,
|
||||||
|
})}
|
||||||
|
on:click={toggleFavorite}>
|
||||||
|
<i class="fa fa-bookmark" class:text-accent={isFavorite} />
|
||||||
|
</div>
|
||||||
|
<CopyValueSimple label="Feed address" value={toNostrURI(naddr)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $expandDefinition.enabled}
|
{#if $expandDefinition.enabled}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {equals} from "ramda"
|
import {equals} from "ramda"
|
||||||
|
import {sortBy, uniqBy} from "@welshman/lib"
|
||||||
|
import {getAddress} from "@welshman/util"
|
||||||
import {isSearchFeed, makeSearchFeed, makeScopeFeed, Scope, getFeedArgs} from "@welshman/feeds"
|
import {isSearchFeed, makeSearchFeed, makeScopeFeed, Scope, getFeedArgs} from "@welshman/feeds"
|
||||||
import {toSpliced} from "src/util/misc"
|
import {toSpliced} from "src/util/misc"
|
||||||
import {boolCtrl} from "src/partials/utils"
|
import {boolCtrl} from "src/partials/utils"
|
||||||
@ -13,8 +15,8 @@
|
|||||||
import FeedForm from "src/app/shared/FeedForm.svelte"
|
import FeedForm from "src/app/shared/FeedForm.svelte"
|
||||||
import {router} from "src/app/util"
|
import {router} from "src/app/util"
|
||||||
import {globalFeed} from "src/app/state"
|
import {globalFeed} from "src/app/state"
|
||||||
import {normalizeFeedDefinition, displayList, readFeed, makeFeed, displayFeed} from "src/domain"
|
import {normalizeFeedDefinition, readFeed, makeFeed, displayFeed} from "src/domain"
|
||||||
import {userListFeeds, canSign, deleteEvent, userFeeds} from "src/engine"
|
import {userListFeeds, canSign, deleteEvent, userFeeds, userFavoritedFeeds} from "src/engine"
|
||||||
|
|
||||||
export let feed
|
export let feed
|
||||||
export let updateFeed
|
export let updateFeed
|
||||||
@ -25,6 +27,10 @@
|
|||||||
const listMenu = boolCtrl()
|
const listMenu = boolCtrl()
|
||||||
const followsFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Follows))})
|
const followsFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Follows))})
|
||||||
const networkFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Network))})
|
const networkFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Network))})
|
||||||
|
const allFeeds = uniqBy(
|
||||||
|
feed => getAddress(feed.event),
|
||||||
|
sortBy(displayFeed, [...$userFeeds, ...$userListFeeds, ...$userFavoritedFeeds]),
|
||||||
|
)
|
||||||
|
|
||||||
const openForm = () => {
|
const openForm = () => {
|
||||||
savePoint = {...feed}
|
savePoint = {...feed}
|
||||||
@ -130,20 +136,13 @@
|
|||||||
on:click={() => setFeed(networkFeed)}>
|
on:click={() => setFeed(networkFeed)}>
|
||||||
Network
|
Network
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{#each $userFeeds as feed}
|
{#each allFeeds as feed}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
active={equals(feed.definition, $globalFeed.definition)}
|
active={equals(feed.definition, $globalFeed.definition)}
|
||||||
on:click={() => setFeed(feed)}>
|
on:click={() => setFeed(feed)}>
|
||||||
{displayFeed(feed)}
|
{displayFeed(feed)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/each}
|
{/each}
|
||||||
{#each $userListFeeds as feed}
|
|
||||||
<MenuItem
|
|
||||||
active={equals(feed.definition, $globalFeed.definition)}
|
|
||||||
on:click={() => setFeed(feed)}>
|
|
||||||
{displayList(feed.list)}
|
|
||||||
</MenuItem>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{#if $canSign}
|
{#if $canSign}
|
||||||
<div class="bg-neutral-900">
|
<div class="bg-neutral-900">
|
||||||
|
@ -11,15 +11,18 @@
|
|||||||
isRelayFeed,
|
isRelayFeed,
|
||||||
isListFeed,
|
isListFeed,
|
||||||
isDVMFeed,
|
isDVMFeed,
|
||||||
|
isGlobalFeed,
|
||||||
makeListFeed,
|
makeListFeed,
|
||||||
makeDVMFeed,
|
makeDVMFeed,
|
||||||
makeScopeFeed,
|
makeScopeFeed,
|
||||||
makeTagFeed,
|
makeTagFeed,
|
||||||
makeRelayFeed,
|
makeRelayFeed,
|
||||||
|
makeGlobalFeed,
|
||||||
Scope,
|
Scope,
|
||||||
} from "@welshman/feeds"
|
} from "@welshman/feeds"
|
||||||
import {toSpliced} from "src/util/misc"
|
import {toSpliced} from "src/util/misc"
|
||||||
import Icon from "src/partials/Icon.svelte"
|
import Icon from "src/partials/Icon.svelte"
|
||||||
|
import SelectButton from "src/partials/SelectButton.svelte"
|
||||||
import SelectTiles from "src/partials/SelectTiles.svelte"
|
import SelectTiles from "src/partials/SelectTiles.svelte"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
@ -29,6 +32,7 @@
|
|||||||
export let feed
|
export let feed
|
||||||
|
|
||||||
enum FormType {
|
enum FormType {
|
||||||
|
Global = "global",
|
||||||
Advanced = "advanced",
|
Advanced = "advanced",
|
||||||
DVMs = "dvms",
|
DVMs = "dvms",
|
||||||
Lists = "lists",
|
Lists = "lists",
|
||||||
@ -60,25 +64,12 @@
|
|||||||
|
|
||||||
const inferFormType = feed => {
|
const inferFormType = feed => {
|
||||||
for (const subFeed of getFeedArgs(normalize(feed))) {
|
for (const subFeed of getFeedArgs(normalize(feed))) {
|
||||||
if ([FeedType.Scope, FeedType.Author].includes(subFeed[0])) {
|
if (isGlobalFeed(subFeed)) return FormType.Global
|
||||||
return FormType.People
|
if (isScopeFeed(subFeed) || isAuthorFeed(subFeed)) return FormType.People
|
||||||
}
|
if (isTagFeed(subFeed) && subFeed[1] === "#t") return FormType.Topics
|
||||||
|
if (isRelayFeed(subFeed)) return FormType.Relays
|
||||||
if (subFeed[0] === FeedType.Tag && subFeed[1] === "#t") {
|
if (isListFeed(subFeed)) return FormType.Lists
|
||||||
return FormType.Topics
|
if (isDVMFeed(subFeed)) return FormType.DVMs
|
||||||
}
|
|
||||||
|
|
||||||
if (subFeed[0] === FeedType.Relay) {
|
|
||||||
return FormType.Relays
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subFeed[0] === FeedType.List) {
|
|
||||||
return FormType.Lists
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subFeed[0] === FeedType.DVM) {
|
|
||||||
return FormType.DVMs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return FormType.Advanced
|
return FormType.Advanced
|
||||||
@ -96,7 +87,9 @@
|
|||||||
|
|
||||||
// Remove filters directly related to the previous type
|
// Remove filters directly related to the previous type
|
||||||
if (newFormType !== FormType.Advanced) {
|
if (newFormType !== FormType.Advanced) {
|
||||||
if (formType === FormType.People) {
|
if (formType === FormType.Global) {
|
||||||
|
removeSubFeed(isGlobalFeed)
|
||||||
|
} else if (formType === FormType.People) {
|
||||||
removeSubFeed(isPeopleFeed)
|
removeSubFeed(isPeopleFeed)
|
||||||
} else if (formType === FormType.Topics) {
|
} else if (formType === FormType.Topics) {
|
||||||
removeSubFeed(isTopicsFeed)
|
removeSubFeed(isTopicsFeed)
|
||||||
@ -112,7 +105,9 @@
|
|||||||
formType = newFormType
|
formType = newFormType
|
||||||
|
|
||||||
// Add a default filter depending on the new form type
|
// Add a default filter depending on the new form type
|
||||||
if (formType === FormType.People) {
|
if (formType === FormType.Global) {
|
||||||
|
prependDefaultSubFeed(isGlobalFeed, makeGlobalFeed())
|
||||||
|
} else if (formType === FormType.People) {
|
||||||
prependDefaultSubFeed(isPeopleFeed, makeScopeFeed(Scope.Follows))
|
prependDefaultSubFeed(isPeopleFeed, makeScopeFeed(Scope.Follows))
|
||||||
} else if (formType === FormType.Topics) {
|
} else if (formType === FormType.Topics) {
|
||||||
prependDefaultSubFeed(isTopicsFeed, makeTagFeed("#t"))
|
prependDefaultSubFeed(isTopicsFeed, makeTagFeed("#t"))
|
||||||
@ -132,25 +127,55 @@
|
|||||||
let formType = inferFormType(feed)
|
let formType = inferFormType(feed)
|
||||||
|
|
||||||
$: formTypeOptions = [
|
$: formTypeOptions = [
|
||||||
|
FormType.Global,
|
||||||
FormType.People,
|
FormType.People,
|
||||||
FormType.Topics,
|
FormType.Topics,
|
||||||
FormType.Relays,
|
FormType.Relays,
|
||||||
FormType.Lists,
|
FormType.Lists,
|
||||||
FormType.DVMs,
|
FormType.DVMs,
|
||||||
|
FormType.Advanced,
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
<Card class="-mb-8">
|
<Card>
|
||||||
<FlexColumn small>
|
<FlexColumn small>
|
||||||
<span class="staatliches text-lg">Choose a feed type</span>
|
<span class="staatliches text-lg">Choose a feed type</span>
|
||||||
|
<SelectButton
|
||||||
|
class="sm:hidden"
|
||||||
|
options={formTypeOptions}
|
||||||
|
onChange={onFormTypeChange}
|
||||||
|
value={formType}>
|
||||||
|
<div slot="item" class="flex items-center gap-2" let:option let:active>
|
||||||
|
{#if option === FormType.Global}
|
||||||
|
<i class="fa fa-earth-europe" /> Global
|
||||||
|
{:else if option === FormType.People}
|
||||||
|
<i class="fa fa-person" /> People
|
||||||
|
{:else if option === FormType.Topics}
|
||||||
|
<i class="fa fa-tags" /> Topics
|
||||||
|
{:else if option === FormType.Relays}
|
||||||
|
<i class="fa fa-server" /> Relays
|
||||||
|
{:else if option === FormType.Lists}
|
||||||
|
<i class="fa fa-bars-staggered" /> Lists
|
||||||
|
{:else if option === FormType.DVMs}
|
||||||
|
<i class="fa fa-circle-nodes" /> DVMs
|
||||||
|
{:else if option === FormType.Advanced}
|
||||||
|
<i class="fa fa-cogs" /> Advanced
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</SelectButton>
|
||||||
<SelectTiles
|
<SelectTiles
|
||||||
class="grid-cols-2 xs:grid-cols-3 md:grid-cols-5"
|
class="hidden grid-cols-4 sm:grid"
|
||||||
options={formTypeOptions}
|
options={formTypeOptions}
|
||||||
onChange={onFormTypeChange}
|
onChange={onFormTypeChange}
|
||||||
value={formType}>
|
value={formType}>
|
||||||
<div slot="item" class="flex flex-col items-center" let:option let:active>
|
<div slot="item" class="flex flex-col items-center" let:option let:active>
|
||||||
{#if option === FormType.People}
|
{#if option === FormType.Global}
|
||||||
|
<span class="flex h-12 w-12 items-center justify-center" class:text-accent={active}>
|
||||||
|
<i class="fa fa-2xl fa-earth-europe" />
|
||||||
|
</span>
|
||||||
|
<span class="staatliches text-2xl">Global</span>
|
||||||
|
{:else 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>
|
<span class="staatliches text-2xl">People</span>
|
||||||
{:else if option === FormType.Topics}
|
{:else if option === FormType.Topics}
|
||||||
@ -169,17 +194,25 @@
|
|||||||
{:else if option === FormType.DVMs}
|
{:else if option === FormType.DVMs}
|
||||||
<Icon icon="network" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
|
<Icon icon="network" class="h-12 w-12" color={active ? "accent" : "tinted-800"} />
|
||||||
<span class="staatliches text-2xl">DVMs</span>
|
<span class="staatliches text-2xl">DVMs</span>
|
||||||
|
{:else if option === FormType.Advanced}
|
||||||
|
<span class="flex h-12 w-12 items-center justify-center" class:text-accent={active}>
|
||||||
|
<i class="fa fa-2xl fa-cogs" />
|
||||||
|
</span>
|
||||||
|
<span class="staatliches text-2xl">Advanced</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SelectTiles>
|
</SelectTiles>
|
||||||
<div
|
|
||||||
class="flex cursor-pointer items-center justify-end gap-2 text-neutral-500"
|
|
||||||
on:click={() => onFormTypeChange(FormType.Advanced)}>
|
|
||||||
<span class="staatliches underline">Advanced Mode</span>
|
|
||||||
</div>
|
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</Card>
|
</Card>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
|
{#if formType === FormType.Global && feed.length === 2}
|
||||||
|
<Card class="flex gap-4 items-center">
|
||||||
|
<i class="fa fa-triangle-exclamation text-warning fa-xl" />
|
||||||
|
<p>
|
||||||
|
Be aware that feeds with no filters can result in obscene or otherwise objectionable content being displayed.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
{#if formType === FormType.Advanced}
|
{#if formType === FormType.Advanced}
|
||||||
<FeedFormAdvanced {feed} onChange={onFeedChange} />
|
<FeedFormAdvanced {feed} onChange={onFeedChange} />
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -11,7 +11,15 @@
|
|||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import FeedField from "src/app/shared/FeedField.svelte"
|
import FeedField from "src/app/shared/FeedField.svelte"
|
||||||
import {makeFeed, createFeed, editFeed, isMentionFeed, displayFeed} from "src/domain"
|
import {makeFeed, createFeed, editFeed, isMentionFeed, displayFeed} from "src/domain"
|
||||||
import {canSign, deleteEvent, createAndPublish, loadPubkeys, hints} from "src/engine"
|
import {
|
||||||
|
pubkey,
|
||||||
|
displayProfileByPubkey,
|
||||||
|
canSign,
|
||||||
|
deleteEvent,
|
||||||
|
createAndPublish,
|
||||||
|
loadPubkeys,
|
||||||
|
hints,
|
||||||
|
} from "src/engine"
|
||||||
|
|
||||||
export let feed
|
export let feed
|
||||||
export let exit
|
export let exit
|
||||||
@ -98,9 +106,18 @@
|
|||||||
<Anchor underline on:click={openSave} class="text-neutral-400">Save this feed</Anchor>
|
<Anchor underline on:click={openSave} class="text-neutral-400">Save this feed</Anchor>
|
||||||
</Card>
|
</Card>
|
||||||
{:else if draft.event || draft.list}
|
{:else if draft.event || draft.list}
|
||||||
|
{@const event = draft.event || draft.list.event}
|
||||||
<Card class="flex flex-col justify-between sm:flex-row">
|
<Card class="flex flex-col justify-between sm:flex-row">
|
||||||
<p>You are currently editing your {displayFeed(draft)} feed.</p>
|
{#if event.pubkey === $pubkey}
|
||||||
<Anchor underline on:click={startClone} class="text-neutral-400">
|
<p>You are currently editing "{displayFeed(draft)}" feed.</p>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
You are currently cloning "{displayFeed(draft)}" by @{displayProfileByPubkey(
|
||||||
|
event.pubkey,
|
||||||
|
)}.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<Anchor underline on:click={startClone} class="whitespace-nowrap text-neutral-400">
|
||||||
Create a new feed instead
|
Create a new feed instead
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Card>
|
</Card>
|
||||||
@ -108,7 +125,7 @@
|
|||||||
<Card class="flex flex-col justify-between sm:flex-row">
|
<Card class="flex flex-col justify-between sm:flex-row">
|
||||||
<p>You are currently creating a new feed.</p>
|
<p>You are currently creating a new feed.</p>
|
||||||
<Anchor underline on:click={stopClone} class="text-neutral-400">
|
<Anchor underline on:click={stopClone} class="text-neutral-400">
|
||||||
Edit your {displayFeed(feed)} feed instead
|
Edit "{displayFeed(feed)}" instead
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {tryCatch} from "@welshman/lib"
|
import {tryCatch} from "@welshman/lib"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
import Field from "src/partials/Field.svelte"
|
|
||||||
import Textarea from "src/partials/Textarea.svelte"
|
import Textarea from "src/partials/Textarea.svelte"
|
||||||
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
|
|
||||||
export let feed
|
export let feed
|
||||||
export let onChange
|
export let onChange
|
||||||
@ -35,18 +35,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Field label="Enter your custom feed below">
|
<FlexColumn>
|
||||||
|
<span class="staatliches text-lg">Enter your custom feed below</span>
|
||||||
<Textarea
|
<Textarea
|
||||||
class="h-72 whitespace-pre-wrap"
|
class="h-72 whitespace-pre-wrap"
|
||||||
value={json}
|
value={json}
|
||||||
on:input={onInput}
|
on:input={onInput}
|
||||||
on:focus={onFocus}
|
on:focus={onFocus}
|
||||||
on:blur={onBlur} />
|
on:blur={onBlur} />
|
||||||
</Field>
|
{#if !isValid && !isFocused}
|
||||||
{#if !isValid && !isFocused}
|
<p>
|
||||||
<p>
|
<i class="fa fa-triangle-exclamation" />
|
||||||
<i class="fa fa-triangle-exclamation" />
|
Your feed is currently invalid. Please double check that it is valid JSON.
|
||||||
Your feed is currently invalid. Please double check that it is valid JSON.
|
</p>
|
||||||
</p>
|
{/if}
|
||||||
{/if}
|
</FlexColumn>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import {toTitle} from "hurdak"
|
import {toTitle} from "hurdak"
|
||||||
import {
|
import {
|
||||||
getFeedArgs,
|
getFeedArgs,
|
||||||
|
isGlobalFeed,
|
||||||
isCreatedAtFeed,
|
isCreatedAtFeed,
|
||||||
isAuthorFeed,
|
isAuthorFeed,
|
||||||
isKindFeed,
|
isKindFeed,
|
||||||
@ -68,41 +69,48 @@
|
|||||||
{#each subFeeds as subFeed, i}
|
{#each subFeeds as subFeed, i}
|
||||||
{@const idx = i + 1}
|
{@const idx = i + 1}
|
||||||
{@const change = f => onSubFeedChange(idx, f)}
|
{@const change = f => onSubFeedChange(idx, f)}
|
||||||
<Card class="relative">
|
{@const canSave =
|
||||||
<FlexColumn>
|
isAuthorFeed(subFeed) ||
|
||||||
<FlexColumn small>
|
isRelayFeed(subFeed) ||
|
||||||
{#if isPeopleFeed(subFeed)}
|
isTopicFeed(subFeed) ||
|
||||||
<FeedFormSectionPeople feed={subFeed} onChange={change} />
|
isMentionFeed(subFeed)}
|
||||||
{:else if isRelayFeed(subFeed)}
|
{#if canSave || !isGlobalFeed(subFeed)}
|
||||||
<FeedFormSectionRelays feed={subFeed} onChange={change} />
|
<Card class="relative">
|
||||||
{:else if isTopicFeed(subFeed)}
|
<FlexColumn>
|
||||||
<FeedFormSectionTopics feed={subFeed} onChange={change} />
|
<FlexColumn small>
|
||||||
{:else if isMentionFeed(subFeed)}
|
{#if isPeopleFeed(subFeed)}
|
||||||
<FeedFormSectionMentions feed={subFeed} onChange={change} />
|
<FeedFormSectionPeople feed={subFeed} onChange={change} />
|
||||||
{:else if isKindFeed(subFeed)}
|
{:else if isRelayFeed(subFeed)}
|
||||||
<FeedFormSectionKinds feed={subFeed} onChange={change} />
|
<FeedFormSectionRelays feed={subFeed} onChange={change} />
|
||||||
{:else if isCreatedAtFeed(subFeed)}
|
{:else if isTopicFeed(subFeed)}
|
||||||
<FeedFormSectionCreatedAt feed={subFeed} onChange={change} />
|
<FeedFormSectionTopics feed={subFeed} onChange={change} />
|
||||||
{:else if isListFeed(subFeed)}
|
{:else if isMentionFeed(subFeed)}
|
||||||
<FeedFormSectionList feed={subFeed} onChange={change} />
|
<FeedFormSectionMentions feed={subFeed} onChange={change} />
|
||||||
{:else if isDVMFeed(subFeed)}
|
{:else if isKindFeed(subFeed)}
|
||||||
<FeedFormSectionDVM feed={subFeed} onChange={change} />
|
<FeedFormSectionKinds feed={subFeed} onChange={change} />
|
||||||
{:else}
|
{:else if isCreatedAtFeed(subFeed)}
|
||||||
No support for editing {toTitle(subFeed[0])} filters. Click "Advanced" to edit manually.
|
<FeedFormSectionCreatedAt feed={subFeed} onChange={change} />
|
||||||
|
{:else if isListFeed(subFeed)}
|
||||||
|
<FeedFormSectionList feed={subFeed} onChange={change} />
|
||||||
|
{:else if isDVMFeed(subFeed)}
|
||||||
|
<FeedFormSectionDVM feed={subFeed} onChange={change} />
|
||||||
|
{:else}
|
||||||
|
No support for editing {toTitle(subFeed[0])} filters. Click "Advanced" to edit manually.
|
||||||
|
{/if}
|
||||||
|
</FlexColumn>
|
||||||
|
{#if canSave}
|
||||||
|
<FeedFormSaveAsList feed={subFeed} onChange={change} />
|
||||||
{/if}
|
{/if}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
{#if isAuthorFeed(subFeed) || isRelayFeed(subFeed) || isTopicFeed(subFeed) || isMentionFeed(subFeed)}
|
{#if i > 0}
|
||||||
<FeedFormSaveAsList feed={subFeed} onChange={change} />
|
<div
|
||||||
|
class="absolute right-2 top-2 h-4 w-4 cursor-pointer"
|
||||||
|
on:click={() => onSubFeedRemove(idx)}>
|
||||||
|
<i class="fa fa-times" />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</FlexColumn>
|
</Card>
|
||||||
{#if i > 0}
|
{/if}
|
||||||
<div
|
|
||||||
class="absolute right-2 top-2 h-4 w-4 cursor-pointer"
|
|
||||||
on:click={() => onSubFeedRemove(idx)}>
|
|
||||||
<i class="fa fa-times" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Card>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
$: addresses = feed.slice(1).flatMap(it => it.addresses)
|
$: addresses = feed.slice(1).flatMap(it => it.addresses)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>Which lists would you like to use?</span>
|
<span class="staatliches text-lg">Which lists would you like to use?</span>
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
multiple
|
multiple
|
||||||
value={addresses}
|
value={addresses}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
$: scopes = isScopeFeed(feed) ? feed.slice(1) : ["custom"]
|
$: scopes = isScopeFeed(feed) ? feed.slice(1) : ["custom"]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>Which authors would you like to see?</span>
|
<span class="staatliches text-lg">Which authors would you like to see?</span>
|
||||||
<SelectButton
|
<SelectButton
|
||||||
multiple
|
multiple
|
||||||
value={scopes}
|
value={scopes}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NoteContentKind1 from "src/app/shared/NoteContentKind1.svelte"
|
import NoteContentKind1 from "src/app/shared/NoteContentKind1.svelte"
|
||||||
import {groups} from "src/engine"
|
import {deriveGroupMeta} from "src/engine"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
export let truncate = true
|
export let truncate = true
|
||||||
|
|
||||||
const group = groups.key(address)
|
const meta = deriveGroupMeta(address)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NoteContentKind1
|
<NoteContentKind1
|
||||||
note={{content: $group?.meta?.about || ""}}
|
note={{content: $meta?.about || ""}}
|
||||||
minLength={100}
|
minLength={100}
|
||||||
maxLength={140}
|
maxLength={140}
|
||||||
showEntire={!truncate} />
|
showEntire={!truncate} />
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ImageCircle from "src/partials/ImageCircle.svelte"
|
import ImageCircle from "src/partials/ImageCircle.svelte"
|
||||||
import PlaceholderCircle from "src/app/shared/PlaceholderCircle.svelte"
|
import PlaceholderCircle from "src/app/shared/PlaceholderCircle.svelte"
|
||||||
import {groups} from "src/engine"
|
import {deriveGroupMeta} from "src/engine"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
|
|
||||||
const group = groups.key(address)
|
const meta = deriveGroupMeta(address)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $group?.meta?.picture}
|
{#if $meta?.image}
|
||||||
<ImageCircle src={$group.meta.picture} class={$$props.class} />
|
<ImageCircle src={$meta.image} class={$$props.class} />
|
||||||
{:else}
|
{:else}
|
||||||
<PlaceholderCircle pubkey={address} class={$$props.class} />
|
<PlaceholderCircle pubkey={address} class={$$props.class} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,24 +1,7 @@
|
|||||||
<script context="module" lang="ts">
|
|
||||||
export type Values = {
|
|
||||||
id?: string
|
|
||||||
type: string
|
|
||||||
feeds: string[][]
|
|
||||||
relays: string[]
|
|
||||||
members?: string[]
|
|
||||||
list_publicly: boolean
|
|
||||||
meta: {
|
|
||||||
name: string
|
|
||||||
about: string
|
|
||||||
picture: string
|
|
||||||
banner: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {join, uniqBy} from "ramda"
|
import {join, uniqBy} from "ramda"
|
||||||
import {ucFirst} from "hurdak"
|
import {ucFirst} from "hurdak"
|
||||||
import {Address} from "@welshman/util"
|
import {Address, GROUP, COMMUNITY} from "@welshman/util"
|
||||||
import {toSpliced} from "src/util/misc"
|
import {toSpliced} from "src/util/misc"
|
||||||
import {fly} from "src/util/transition"
|
import {fly} from "src/util/transition"
|
||||||
import {formCtrl} from "src/partials/utils"
|
import {formCtrl} from "src/partials/utils"
|
||||||
@ -35,11 +18,12 @@
|
|||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
import Heading from "src/partials/Heading.svelte"
|
import Heading from "src/partials/Heading.svelte"
|
||||||
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
||||||
|
import type {GroupMeta} from "src/domain"
|
||||||
import {normalizeRelayUrl} from "src/domain"
|
import {normalizeRelayUrl} from "src/domain"
|
||||||
import {env, hints, relaySearch, feedSearch} from "src/engine"
|
import {env, hints, relaySearch, feedSearch} from "src/engine"
|
||||||
|
|
||||||
export let onSubmit
|
export let onSubmit
|
||||||
export let values: Values
|
export let values: GroupMeta & {members: string[]}
|
||||||
export let mode = "create"
|
export let mode = "create"
|
||||||
export let showMembers = false
|
export let showMembers = false
|
||||||
export let buttonText = "Save"
|
export let buttonText = "Save"
|
||||||
@ -83,7 +67,7 @@
|
|||||||
<div class="mb-4 flex flex-col items-center justify-center">
|
<div class="mb-4 flex flex-col items-center justify-center">
|
||||||
<Heading>{ucFirst(mode)} Group</Heading>
|
<Heading>{ucFirst(mode)} Group</Heading>
|
||||||
<p>
|
<p>
|
||||||
{#if values.type === "open"}
|
{#if values.kind === COMMUNITY}
|
||||||
An open forum where anyone can participate.
|
An open forum where anyone can participate.
|
||||||
{:else}
|
{:else}
|
||||||
A private place where members can talk.
|
A private place where members can talk.
|
||||||
@ -92,30 +76,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col gap-8">
|
<div class="flex w-full flex-col gap-8">
|
||||||
<Field label="Name">
|
<Field label="Name">
|
||||||
<Input bind:value={values.meta.name}>
|
<Input bind:value={values.name}>
|
||||||
<i slot="before" class="fa fa-clipboard" />
|
<i slot="before" class="fa fa-clipboard" />
|
||||||
</Input>
|
</Input>
|
||||||
<div slot="info">The name of the group</div>
|
<div slot="info">The name of the group</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Picture">
|
<Field label="Picture">
|
||||||
<ImageInput
|
<ImageInput
|
||||||
bind:value={values.meta.picture}
|
bind:value={values.image}
|
||||||
icon="image-portrait"
|
icon="image-portrait"
|
||||||
maxWidth={480}
|
maxWidth={480}
|
||||||
maxHeight={480} />
|
maxHeight={480} />
|
||||||
<div slot="info">A picture for the group</div>
|
<div slot="info">A picture for the group</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Banner">
|
<Field label="Banner">
|
||||||
<ImageInput bind:value={values.meta.banner} icon="image" maxWidth={4000} maxHeight={4000} />
|
<ImageInput bind:value={values.banner} icon="image" maxWidth={4000} maxHeight={4000} />
|
||||||
<div slot="info">A banner image for the group</div>
|
<div slot="info">A banner image for the group</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="About">
|
<Field label="About">
|
||||||
<Textarea bind:value={values.meta.about} />
|
<Textarea bind:value={values.about} />
|
||||||
<div slot="info">The group's decription</div>
|
<div slot="info">The group's decription</div>
|
||||||
</Field>
|
</Field>
|
||||||
{#if values.type === "closed"}
|
{#if values.kind === GROUP}
|
||||||
<FieldInline label="List Publicly">
|
<FieldInline label="List Publicly">
|
||||||
<Toggle bind:value={values.list_publicly} />
|
<Toggle bind:value={values.listing_is_public} />
|
||||||
<div slot="info">
|
<div slot="info">
|
||||||
If enabled, this will generate a public listing for the group. The member list and group
|
If enabled, this will generate a public listing for the group. The member list and group
|
||||||
messages will not be published.
|
messages will not be published.
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
import {deriveGroup, displayGroup, loadGroups} from "src/engine"
|
import {displayGroupMeta} from "src/domain"
|
||||||
|
import {deriveGroupMeta, loadGroups} from "src/engine"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
|
|
||||||
const group = deriveGroup(address)
|
const meta = deriveGroupMeta(address)
|
||||||
const path = router.at("groups").of(address).at("notes").toString()
|
const path = router.at("groups").of(address).at("notes").toString()
|
||||||
|
|
||||||
loadGroups([address])
|
loadGroups([address])
|
||||||
@ -13,6 +14,6 @@
|
|||||||
|
|
||||||
<span class={$$props.class}>
|
<span class={$$props.class}>
|
||||||
<Anchor modal underline href={path}>
|
<Anchor modal underline href={path}>
|
||||||
{displayGroup($group)}
|
{displayGroupMeta($meta)}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</span>
|
</span>
|
||||||
|
@ -2,17 +2,19 @@
|
|||||||
import {ellipsize} from "hurdak"
|
import {ellipsize} from "hurdak"
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {remove, intersection} from "@welshman/lib"
|
import {remove, intersection} from "@welshman/lib"
|
||||||
|
import {isGroupAddress, isCommunityAddress} from "@welshman/util"
|
||||||
import Chip from "src/partials/Chip.svelte"
|
import Chip from "src/partials/Chip.svelte"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
||||||
import PersonCircles from "src/app/shared/PersonCircles.svelte"
|
import PersonCircles from "src/app/shared/PersonCircles.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
import {displayGroup, deriveGroup, userFollows, communityListsByAddress, pubkey} from "src/engine"
|
import {displayGroupMeta} from "src/domain"
|
||||||
|
import {deriveGroupMeta, userFollows, communityListsByAddress, pubkey} from "src/engine"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
export let modal = false
|
export let modal = false
|
||||||
|
|
||||||
const group = deriveGroup(address)
|
const meta = deriveGroupMeta(address)
|
||||||
const members = derived(communityListsByAddress, $m => {
|
const members = derived(communityListsByAddress, $m => {
|
||||||
const allMembers = $m.get(address)?.map(l => l.event.pubkey) || []
|
const allMembers = $m.get(address)?.map(l => l.event.pubkey) || []
|
||||||
const otherMembers = remove($pubkey, allMembers)
|
const otherMembers = remove($pubkey, allMembers)
|
||||||
@ -37,20 +39,20 @@
|
|||||||
<div class="flex min-w-0 flex-grow flex-col justify-start gap-1">
|
<div class="flex min-w-0 flex-grow flex-col justify-start gap-1">
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-xl font-bold">
|
||||||
{displayGroup($group)}
|
{displayGroupMeta($meta)}
|
||||||
</h2>
|
</h2>
|
||||||
<slot name="actions">
|
<slot name="actions">
|
||||||
{#if address.startsWith("34550:")}
|
{#if isCommunityAddress(address)}
|
||||||
<Chip class="text-sm text-neutral-200"><i class="fa fa-unlock" /> Open</Chip>
|
<Chip class="text-sm text-neutral-200"><i class="fa fa-unlock" /> Open</Chip>
|
||||||
{/if}
|
{/if}
|
||||||
{#if address.startsWith("35834:")}
|
{#if isGroupAddress(address)}
|
||||||
<Chip class="text-sm text-neutral-200"><i class="fa fa-lock" /> Closed</Chip>
|
<Chip class="text-sm text-neutral-200"><i class="fa fa-lock" /> Closed</Chip>
|
||||||
{/if}
|
{/if}
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
{#if $group.meta?.about}
|
{#if $meta?.about}
|
||||||
<p class="text-start text-neutral-100">
|
<p class="text-start text-neutral-100">
|
||||||
{ellipsize($group.meta.about, 300)}
|
{ellipsize($meta.about, 300)}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $members.length > 0}
|
{#if $members.length > 0}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {groups, displayGroup} from "src/engine"
|
import {displayGroupMeta} from "src/domain"
|
||||||
|
import {deriveGroupMeta} from "src/engine"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
|
|
||||||
const group = groups.key(address)
|
const meta = deriveGroupMeta(address)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class={$$props.class}>{displayGroup($group)}</span>
|
<span class={$$props.class}>{displayGroupMeta($meta)}</span>
|
||||||
|
@ -9,12 +9,11 @@
|
|||||||
import Feed from "src/app/shared/Feed.svelte"
|
import Feed from "src/app/shared/Feed.svelte"
|
||||||
import NoteCreateInline from "src/app/shared/NoteCreateInline.svelte"
|
import NoteCreateInline from "src/app/shared/NoteCreateInline.svelte"
|
||||||
import {makeFeed, readFeed} from "src/domain"
|
import {makeFeed, readFeed} from "src/domain"
|
||||||
import {hints, repository, canSign, deriveGroup, load} from "src/engine"
|
import {hints, repository, canSign, deriveGroupMeta, load} from "src/engine"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
|
|
||||||
const group = deriveGroup(address)
|
const meta = deriveGroupMeta(address)
|
||||||
|
|
||||||
const mainFeed = feedFromFilter({kinds: remove(30402, noteKinds), "#a": [address]})
|
const mainFeed = feedFromFilter({kinds: remove(30402, noteKinds), "#a": [address]})
|
||||||
|
|
||||||
const setActiveTab = tab => {
|
const setActiveTab = tab => {
|
||||||
@ -27,7 +26,7 @@
|
|||||||
let feeds = [{name: "feed", feed: makeFeed({definition: mainFeed})}]
|
let feeds = [{name: "feed", feed: makeFeed({definition: mainFeed})}]
|
||||||
let feed = makeFeed({definition: mainFeed})
|
let feed = makeFeed({definition: mainFeed})
|
||||||
|
|
||||||
for (const feed of $group.feeds || []) {
|
for (const feed of $meta?.feeds || []) {
|
||||||
const [address, relay = "", name = ""] = feed.slice(1)
|
const [address, relay = "", name = ""] = feed.slice(1)
|
||||||
|
|
||||||
if (!Address.isAddress(address)) {
|
if (!Address.isAddress(address)) {
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {isCommunityAddress} from "@welshman/util"
|
||||||
import Chip from "src/partials/Chip.svelte"
|
import Chip from "src/partials/Chip.svelte"
|
||||||
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
||||||
import GroupAbout from "src/app/shared/GroupAbout.svelte"
|
import GroupAbout from "src/app/shared/GroupAbout.svelte"
|
||||||
import GroupName from "src/app/shared/GroupName.svelte"
|
import GroupName from "src/app/shared/GroupName.svelte"
|
||||||
import {groups} from "src/engine"
|
import {deriveGroupMeta} from "src/engine"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
export let hideAbout = false
|
export let hideAbout = false
|
||||||
|
|
||||||
const group = groups.key(address)
|
const meta = deriveGroupMeta(address)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex gap-4 text-neutral-100">
|
<div class="flex gap-4 text-neutral-100">
|
||||||
@ -18,7 +19,7 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<GroupName class="text-2xl" {address} />
|
<GroupName class="text-2xl" {address} />
|
||||||
<Chip class="scale-75 border-neutral-200 text-neutral-200">
|
<Chip class="scale-75 border-neutral-200 text-neutral-200">
|
||||||
{#if address.startsWith("34550:")}
|
{#if isCommunityAddress(address)}
|
||||||
<i class="fa fa-unlock" />
|
<i class="fa fa-unlock" />
|
||||||
Open
|
Open
|
||||||
{:else}
|
{:else}
|
||||||
@ -29,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<slot name="actions" class="hidden xs:block" />
|
<slot name="actions" class="hidden xs:block" />
|
||||||
</div>
|
</div>
|
||||||
{#if !hideAbout && $group?.meta?.about}
|
{#if !hideAbout && $meta?.about}
|
||||||
<GroupAbout {address} />
|
<GroupAbout {address} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {without} from "ramda"
|
|
||||||
import {
|
import {
|
||||||
parse,
|
parse,
|
||||||
truncate,
|
truncate,
|
||||||
@ -16,7 +15,6 @@
|
|||||||
isAddress,
|
isAddress,
|
||||||
isNewline,
|
isNewline,
|
||||||
} from "@welshman/content"
|
} from "@welshman/content"
|
||||||
import MediaSet from "src/partials/MediaSet.svelte"
|
|
||||||
import QRCode from "src/partials/QRCode.svelte"
|
import QRCode from "src/partials/QRCode.svelte"
|
||||||
import NoteContentNewline from "src/app/shared/NoteContentNewline.svelte"
|
import NoteContentNewline from "src/app/shared/NoteContentNewline.svelte"
|
||||||
import NoteContentEllipsis from "src/app/shared/NoteContentEllipsis.svelte"
|
import NoteContentEllipsis from "src/app/shared/NoteContentEllipsis.svelte"
|
||||||
@ -44,16 +42,15 @@
|
|||||||
const isBoundary = i => {
|
const isBoundary = i => {
|
||||||
const parsed = shortContent[i]
|
const parsed = shortContent[i]
|
||||||
|
|
||||||
if (!parsed || isBoundary(parsed)) return true
|
if (!parsed || isNewline(parsed)) return true
|
||||||
if (isText(parsed)) return parsed.value.match(/^\s+$/)
|
if (isText(parsed)) return parsed.value.match(/^\s+$/)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStartOrEnd = i => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
|
const isStartAndEnd = i => Boolean(isBoundary(i - 1) && isBoundary(i + 1))
|
||||||
|
|
||||||
const getLinks = content =>
|
const isStartOrEnd = i => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
|
||||||
content.filter(x => isLink(x) && x.value.isMedia).map(x => x.value.url.toString())
|
|
||||||
|
|
||||||
$: shortContent = showEntire
|
$: shortContent = showEntire
|
||||||
? fullContent
|
? fullContent
|
||||||
@ -66,13 +63,11 @@
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
$: links = getLinks(shortContent)
|
|
||||||
$: extraLinks = without(links, getLinks(fullContent))
|
|
||||||
$: ellipsize = expandable && shortContent.find(isEllipsis)
|
$: ellipsize = expandable && shortContent.find(isEllipsis)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 overflow-hidden text-ellipsis"
|
class="note-content flex flex-col gap-2 overflow-hidden text-ellipsis"
|
||||||
style={ellipsize && "mask-image: linear-gradient(0deg, transparent 0px, black 100px)"}>
|
style={ellipsize && "mask-image: linear-gradient(0deg, transparent 0px, black 100px)"}>
|
||||||
<div>
|
<div>
|
||||||
{#each shortContent as parsed, i}
|
{#each shortContent as parsed, i}
|
||||||
@ -91,7 +86,7 @@
|
|||||||
<QRCode copyOnClick code={parsed.value} />
|
<QRCode copyOnClick code={parsed.value} />
|
||||||
</div>
|
</div>
|
||||||
{:else if isLink(parsed)}
|
{:else if isLink(parsed)}
|
||||||
<NoteContentLink value={parsed.value} showMedia={showMedia && isStartOrEnd(i)} />
|
<NoteContentLink value={parsed.value} showMedia={showMedia && isStartAndEnd(i)} />
|
||||||
{:else if isProfile(parsed)}
|
{:else if isProfile(parsed)}
|
||||||
<PersonLink pubkey={parsed.value.pubkey} />
|
<PersonLink pubkey={parsed.value.pubkey} />
|
||||||
{:else if (isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 2}
|
{:else if (isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 2}
|
||||||
@ -100,16 +95,11 @@
|
|||||||
<slot name="note-content" {quote} />
|
<slot name="note-content" {quote} />
|
||||||
</div>
|
</div>
|
||||||
</NoteContentQuote>
|
</NoteContentQuote>
|
||||||
{:else if !expandable && isEllipsis(parsed)}
|
|
||||||
{@html renderParsed(parsed)}
|
|
||||||
{:else}
|
{:else}
|
||||||
{@html renderParsed(parsed)}
|
{@html renderParsed(parsed)}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if showMedia && extraLinks.length > 0}
|
|
||||||
<MediaSet links={extraLinks} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ellipsize}
|
{#if ellipsize}
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
|
||||||
|
const coracleRegexp = /^(https?:\/\/)?(app\.)?coracle.social/
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
hidden = true
|
hidden = true
|
||||||
}
|
}
|
||||||
@ -17,18 +19,16 @@
|
|||||||
let hidden = false
|
let hidden = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url.includes('coracle.social/')}
|
{#if url.match(coracleRegexp)}
|
||||||
<Anchor
|
<Anchor
|
||||||
modal
|
modal
|
||||||
stopPropagation
|
stopPropagation
|
||||||
class="overflow-hidden text-ellipsis whitespace-nowrap underline"
|
class="overflow-hidden text-ellipsis whitespace-nowrap underline"
|
||||||
href={url.replace(/(https?:\/\/)?(app\.)?coracle.social/, '')}>
|
href={url.replace(coracleRegexp, '')}>
|
||||||
{displayUrl(url)}
|
{displayUrl(url)}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
{:else if showMedia && value.isMedia && !hidden}
|
{:else if showMedia && !hidden}
|
||||||
<div class="py-2">
|
<Media url={url} onClose={close} />
|
||||||
<Media url={url} onClose={close} />
|
|
||||||
</div>
|
|
||||||
{:else if isShareableRelayUrl(url)}
|
{:else if isShareableRelayUrl(url)}
|
||||||
<Anchor
|
<Anchor
|
||||||
modal
|
modal
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {derived} from "svelte/store"
|
||||||
import {uniq} from "@welshman/lib"
|
import {uniq} from "@welshman/lib"
|
||||||
import {parseAnything} from "src/util/nostr"
|
import {parseAnything} from "src/util/nostr"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import SearchSelect from "src/partials/SearchSelect.svelte"
|
import SearchSelect from "src/partials/SearchSelect.svelte"
|
||||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
import {profileSearch, createPeopleLoader} from "src/engine"
|
import {profileSearch, loadPubkeyProfiles, createPeopleLoader} from "src/engine"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let multiple = false
|
export let multiple = false
|
||||||
@ -16,27 +17,33 @@
|
|||||||
|
|
||||||
const {loading, load} = createPeopleLoader()
|
const {loading, load} = createPeopleLoader()
|
||||||
|
|
||||||
const search = term => {
|
const search = derived(profileSearch, $profileSearch => {
|
||||||
load(term)
|
return term => {
|
||||||
|
load(term)
|
||||||
|
|
||||||
parseAnything(term).then(result => {
|
parseAnything(term).then(result => {
|
||||||
if (result?.type === "npub") {
|
if (result?.type === "npub") {
|
||||||
value = uniq(value.concat(result.data))
|
loadPubkeyProfiles([result.data])
|
||||||
input.clearTerm()
|
value = uniq(value.concat(result.data))
|
||||||
}
|
input.clearTerm()
|
||||||
|
onChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
if (result?.type === "nprofile") {
|
if (result?.type === "nprofile") {
|
||||||
value = uniq(value.concat(result.data.pubkey))
|
loadPubkeyProfiles([result.data.pubkey])
|
||||||
input.clearTerm()
|
value = uniq(value.concat(result.data.pubkey))
|
||||||
}
|
input.clearTerm()
|
||||||
})
|
onChange(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return $profileSearch.searchValues(term)
|
return $profileSearch.searchValues(term)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
{search}
|
search={$search}
|
||||||
{onChange}
|
{onChange}
|
||||||
{multiple}
|
{multiple}
|
||||||
{autofocus}
|
{autofocus}
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
<p class="mt-4 text-lg">The following relays are still pending:</p>
|
<p class="mt-4 text-lg">The following relays are still pending:</p>
|
||||||
<div class="grid gap-2 sm:grid-cols-2">
|
<div class="grid gap-2 sm:grid-cols-2">
|
||||||
{#each pending as url}
|
{#each pending as url}
|
||||||
<RelayCard hideActions {url} />
|
<RelayCard hideDescription hideActions {url} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -104,7 +104,7 @@
|
|||||||
<p class="mt-4 text-lg">The following relays accepted your note:</p>
|
<p class="mt-4 text-lg">The following relays accepted your note:</p>
|
||||||
<div class="grid gap-2 sm:grid-cols-2">
|
<div class="grid gap-2 sm:grid-cols-2">
|
||||||
{#each success as url}
|
{#each success as url}
|
||||||
<RelayCard hideActions {url} />
|
<RelayCard hideDescription hideActions {url} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
export let claim = null
|
export let claim = null
|
||||||
export let ratings = null
|
export let ratings = null
|
||||||
export let showStatus = false
|
export let showStatus = false
|
||||||
|
export let hideDescription = false
|
||||||
export let hideRatingsCount = false
|
export let hideRatingsCount = false
|
||||||
export let hideActions = false
|
export let hideActions = false
|
||||||
export let showControls = false
|
export let showControls = false
|
||||||
@ -64,30 +65,32 @@
|
|||||||
</slot>
|
</slot>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<slot name="description">
|
{#if !hideDescription}
|
||||||
{#if $relay.description}
|
<slot name="description">
|
||||||
<p>{$relay.description}</p>
|
{#if $relay.description}
|
||||||
|
<p>{$relay.description}</p>
|
||||||
|
{/if}
|
||||||
|
</slot>
|
||||||
|
{#if !isNil($relay.count)}
|
||||||
|
<span class="flex items-center gap-1 text-sm text-neutral-400">
|
||||||
|
{#if $relay.contact}
|
||||||
|
<Anchor external underline href={$relay.contact}>{displayUrl($relay.contact)}</Anchor>
|
||||||
|
•
|
||||||
|
{/if}
|
||||||
|
{#if $relay.supported_nips}
|
||||||
|
<Popover>
|
||||||
|
<span slot="trigger" class="cursor-pointer underline">
|
||||||
|
{$relay.supported_nips.length} NIPs
|
||||||
|
</span>
|
||||||
|
<span slot="tooltip">
|
||||||
|
NIPs supported: {$relay.supported_nips.join(", ")}
|
||||||
|
</span>
|
||||||
|
</Popover>
|
||||||
|
•
|
||||||
|
{/if}
|
||||||
|
Seen {quantify($relay.count || 0, "time")}
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</slot>
|
|
||||||
{#if !isNil($relay.count)}
|
|
||||||
<span class="flex items-center gap-1 text-sm text-neutral-400">
|
|
||||||
{#if $relay.contact}
|
|
||||||
<Anchor external underline href={$relay.contact}>{displayUrl($relay.contact)}</Anchor>
|
|
||||||
•
|
|
||||||
{/if}
|
|
||||||
{#if $relay.supported_nips}
|
|
||||||
<Popover>
|
|
||||||
<span slot="trigger" class="cursor-pointer underline">
|
|
||||||
{$relay.supported_nips.length} NIPs
|
|
||||||
</span>
|
|
||||||
<span slot="tooltip">
|
|
||||||
NIPs supported: {$relay.supported_nips.join(", ")}
|
|
||||||
</span>
|
|
||||||
</Popover>
|
|
||||||
•
|
|
||||||
{/if}
|
|
||||||
Seen {quantify($relay.count || 0, "time")}
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if showControls && $canSign}
|
{#if showControls && $canSign}
|
||||||
<div class="-mx-6 my-1 h-px bg-tinted-700" />
|
<div class="-mx-6 my-1 h-px bg-tinted-700" />
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {sortBy, uniqBy} from "@welshman/lib"
|
||||||
import {getAddress} from "@welshman/util"
|
import {getAddress} from "@welshman/util"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {createScroller} from "src/util/misc"
|
import {createScroller} from "src/util/misc"
|
||||||
@ -8,15 +9,18 @@
|
|||||||
import Input from "src/partials/Input.svelte"
|
import Input from "src/partials/Input.svelte"
|
||||||
import FeedCard from "src/app/shared/FeedCard.svelte"
|
import FeedCard from "src/app/shared/FeedCard.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
|
import {displayFeed} from "src/domain"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
|
||||||
userFeeds,
|
userFeeds,
|
||||||
feedSearch,
|
feedSearch,
|
||||||
userListFeeds,
|
userListFeeds,
|
||||||
loadPubkeyFeeds,
|
loadPubkeyFeeds,
|
||||||
|
userFavoritedFeeds,
|
||||||
userFollows,
|
userFollows,
|
||||||
} from "src/engine"
|
} from "src/engine"
|
||||||
|
|
||||||
|
const favoritedFeeds = $userFavoritedFeeds
|
||||||
|
|
||||||
const createFeed = () => router.at("feeds/create").open()
|
const createFeed = () => router.at("feeds/create").open()
|
||||||
|
|
||||||
const editFeed = address => router.at("feeds").of(address).open()
|
const editFeed = address => router.at("feeds").of(address).open()
|
||||||
@ -29,6 +33,13 @@
|
|||||||
let limit = 20
|
let limit = 20
|
||||||
let element
|
let element
|
||||||
|
|
||||||
|
$: allUserFeeds = [...$userFeeds, ...$userListFeeds]
|
||||||
|
|
||||||
|
$: feeds = uniqBy(
|
||||||
|
feed => getAddress(feed.event),
|
||||||
|
sortBy(displayFeed, [...allUserFeeds, ...favoritedFeeds]),
|
||||||
|
)
|
||||||
|
|
||||||
loadPubkeyFeeds(Array.from($userFollows))
|
loadPubkeyFeeds(Array.from($userFollows))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@ -48,7 +59,7 @@
|
|||||||
<i class="fa fa-plus" /> Feed
|
<i class="fa fa-plus" /> Feed
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</div>
|
</div>
|
||||||
{#each $userFeeds as feed (getAddress(feed.event))}
|
{#each feeds as feed (feed.event.id)}
|
||||||
{@const address = getAddress(feed.event)}
|
{@const address = getAddress(feed.event)}
|
||||||
<div in:fly={{y: 20}}>
|
<div in:fly={{y: 20}}>
|
||||||
<FeedCard {address}>
|
<FeedCard {address}>
|
||||||
@ -60,7 +71,7 @@
|
|||||||
</FeedCard>
|
</FeedCard>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#each $userListFeeds as feed (getAddress(feed.list.event))}
|
{#each $userListFeeds as feed (feed.list.event.id)}
|
||||||
{@const address = getAddress(feed.list.event)}
|
{@const address = getAddress(feed.list.event)}
|
||||||
<div in:fly={{y: 20}}>
|
<div in:fly={{y: 20}}>
|
||||||
<FeedCard {address}>
|
<FeedCard {address}>
|
||||||
@ -85,7 +96,7 @@
|
|||||||
</Input>
|
</Input>
|
||||||
{#each $feedSearch
|
{#each $feedSearch
|
||||||
.searchValues(q)
|
.searchValues(q)
|
||||||
.filter(address => !address.includes($pubkey))
|
.filter(address => !feeds.find(feed => getAddress(feed.event) === address))
|
||||||
.slice(0, limit) as address (address)}
|
.slice(0, limit) as address (address)}
|
||||||
<FeedCard {address} />
|
<FeedCard {address} />
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {nth} from "@welshman/lib"
|
||||||
|
import {COMMUNITY, GROUP} from "@welshman/util"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
import Heading from "src/partials/Heading.svelte"
|
import Heading from "src/partials/Heading.svelte"
|
||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
import type {Values} from "src/app/shared/GroupDetailsForm.svelte"
|
|
||||||
import GroupDetailsForm from "src/app/shared/GroupDetailsForm.svelte"
|
import GroupDetailsForm from "src/app/shared/GroupDetailsForm.svelte"
|
||||||
|
import type {GroupMeta} from "src/domain"
|
||||||
import {
|
import {
|
||||||
env,
|
env,
|
||||||
pubkey,
|
pubkey,
|
||||||
@ -19,34 +21,38 @@
|
|||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
type: null,
|
kind: null,
|
||||||
|
name: "",
|
||||||
|
about: "",
|
||||||
|
image: "",
|
||||||
|
banner: "",
|
||||||
feeds: [],
|
feeds: [],
|
||||||
relays: $env.PLATFORM_RELAYS,
|
relays: $env.PLATFORM_RELAYS.map(url => ["relay", url]),
|
||||||
|
listing_is_public: false,
|
||||||
members: [$pubkey],
|
members: [$pubkey],
|
||||||
list_publicly: false,
|
moderators: [],
|
||||||
meta: {
|
identifier: "",
|
||||||
name: "",
|
|
||||||
about: "",
|
|
||||||
picture: "",
|
|
||||||
banner: "",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setType = type => {
|
const setKind = kind => {
|
||||||
initialValues.type = type
|
initialValues.kind = kind
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async ({type, feeds, relays, members, list_publicly, meta}: Values) => {
|
const onSubmit = async ({
|
||||||
const kind = type === "open" ? 34550 : 35834
|
kind,
|
||||||
const {id, address} = initGroup(kind, relays)
|
members,
|
||||||
|
listing_is_public,
|
||||||
|
...meta
|
||||||
|
}: GroupMeta & {members: string[]}) => {
|
||||||
|
const {identifier, address} = initGroup(kind, meta.relays.map(nth(1)))
|
||||||
|
|
||||||
await publishAdminKeyShares(address, [$pubkey])
|
await publishAdminKeyShares(address, [$pubkey])
|
||||||
|
|
||||||
if (type === "open") {
|
if (kind === COMMUNITY) {
|
||||||
await publishCommunityMeta(address, id, feeds, relays, meta)
|
await publishCommunityMeta(address, identifier, meta)
|
||||||
await publishCommunitiesList(deriveUserCommunities().get().concat(address))
|
await publishCommunitiesList(deriveUserCommunities().get().concat(address))
|
||||||
} else {
|
} else {
|
||||||
await publishGroupMeta(address, id, feeds, relays, meta, list_publicly)
|
await publishGroupMeta(address, identifier, meta, listing_is_public)
|
||||||
await publishGroupMembers(address, "set", members)
|
await publishGroupMembers(address, "set", members)
|
||||||
await publishGroupInvites(address, members)
|
await publishGroupInvites(address, members)
|
||||||
}
|
}
|
||||||
@ -55,12 +61,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !initialValues.type}
|
{#if !initialValues.kind}
|
||||||
<div class="mb-4 flex flex-col items-center justify-center">
|
<div class="mb-4 flex flex-col items-center justify-center">
|
||||||
<Heading>Create Group</Heading>
|
<Heading>Create Group</Heading>
|
||||||
<p>What type of group would you like to create?</p>
|
<p>What type of group would you like to create?</p>
|
||||||
</div>
|
</div>
|
||||||
<Card interactive on:click={() => setType("open")}>
|
<Card interactive on:click={() => setKind(COMMUNITY)}>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="flex items-center gap-4 text-xl">
|
<p class="flex items-center gap-4 text-xl">
|
||||||
@ -75,7 +81,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</Card>
|
</Card>
|
||||||
<Card interactive on:click={() => setType("closed")}>
|
<Card interactive on:click={() => setKind(GROUP)}>
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="flex items-center gap-4 text-xl">
|
<p class="flex items-center gap-4 text-xl">
|
||||||
@ -94,11 +100,11 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<i
|
<i
|
||||||
class="fa fa-2x fa-arrow-left absolute top-12 cursor-pointer"
|
class="fa fa-2x fa-arrow-left absolute top-12 cursor-pointer"
|
||||||
on:click={() => setType(null)} />
|
on:click={() => setKind(null)} />
|
||||||
<GroupDetailsForm
|
<GroupDetailsForm
|
||||||
{onSubmit}
|
{onSubmit}
|
||||||
values={initialValues}
|
values={initialValues}
|
||||||
showMembers={initialValues.type === "closed"}
|
showMembers={initialValues.kind === GROUP}
|
||||||
buttonText={`Create ${initialValues.type} group`} />
|
buttonText={initialValues.kind === GROUP ? "Create closed group" : "Create open group"} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
import {showInfo} from "src/partials/Toast.svelte"
|
import {showInfo} from "src/partials/Toast.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Subheading from "src/partials/Subheading.svelte"
|
import Subheading from "src/partials/Subheading.svelte"
|
||||||
import {groups, createAndPublish, hints, deriveAdminKeyForGroup, displayGroup} from "src/engine"
|
import {displayGroupMeta} from "src/domain"
|
||||||
|
import {deriveGroupMeta, createAndPublish, hints, deriveAdminKeyForGroup} from "src/engine"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
|
|
||||||
const group = groups.key(address)
|
const meta = deriveGroupMeta(address)
|
||||||
const adminKey = deriveAdminKeyForGroup(address)
|
const adminKey = deriveAdminKeyForGroup(address)
|
||||||
|
|
||||||
const abort = () => router.pop()
|
const abort = () => router.pop()
|
||||||
@ -31,7 +32,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Subheading>Delete Group</Subheading>
|
<Subheading>Delete Group</Subheading>
|
||||||
<p>Are you sure you want to delete {displayGroup($group)}?</p>
|
<p>Are you sure you want to delete {displayGroupMeta($meta)}?</p>
|
||||||
<p>
|
<p>
|
||||||
This will only hide this group from supporting clients. Messages sent to the group may not be
|
This will only hide this group from supporting clients. Messages sent to the group may not be
|
||||||
deleted from relays.
|
deleted from relays.
|
||||||
|
@ -13,13 +13,14 @@
|
|||||||
import GroupMembers from "src/app/shared/GroupMembers.svelte"
|
import GroupMembers from "src/app/shared/GroupMembers.svelte"
|
||||||
import GroupAdmin from "src/app/shared/GroupAdmin.svelte"
|
import GroupAdmin from "src/app/shared/GroupAdmin.svelte"
|
||||||
import GroupRestrictAccess from "src/app/shared/GroupRestrictAccess.svelte"
|
import GroupRestrictAccess from "src/app/shared/GroupRestrictAccess.svelte"
|
||||||
|
import {displayGroupMeta} from "src/domain"
|
||||||
import {
|
import {
|
||||||
env,
|
env,
|
||||||
GroupAccess,
|
GroupAccess,
|
||||||
displayGroup,
|
|
||||||
loadPubkeys,
|
loadPubkeys,
|
||||||
groupRequests,
|
groupRequests,
|
||||||
deriveGroup,
|
deriveGroup,
|
||||||
|
deriveGroupMeta,
|
||||||
deriveAdminKeyForGroup,
|
deriveAdminKeyForGroup,
|
||||||
deriveSharedKeyForGroup,
|
deriveSharedKeyForGroup,
|
||||||
deriveIsGroupMember,
|
deriveIsGroupMember,
|
||||||
@ -35,6 +36,7 @@
|
|||||||
export let claim = ""
|
export let claim = ""
|
||||||
|
|
||||||
const group = deriveGroup(address)
|
const group = deriveGroup(address)
|
||||||
|
const meta = deriveGroupMeta(address)
|
||||||
const status = deriveGroupStatus(address)
|
const status = deriveGroupStatus(address)
|
||||||
const isGroupMember = deriveIsGroupMember(address)
|
const isGroupMember = deriveIsGroupMember(address)
|
||||||
const sharedKey = deriveSharedKeyForGroup(address)
|
const sharedKey = deriveSharedKeyForGroup(address)
|
||||||
@ -86,21 +88,21 @@
|
|||||||
|
|
||||||
$: ({rgb, rgba} = $themeBackgroundGradient)
|
$: ({rgb, rgba} = $themeBackgroundGradient)
|
||||||
|
|
||||||
document.title = $group?.meta?.name || "Group Detail"
|
document.title = $meta?.name || "Group Detail"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 top-0 h-64 w-full"
|
class="absolute left-0 top-0 h-64 w-full"
|
||||||
style={`z-index: -1;
|
style={`z-index: -1;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-image: linear-gradient(to bottom, ${rgba}, ${rgb}), url('${$group?.meta?.banner}')`} />
|
background-image: linear-gradient(to bottom, ${rgba}, ${rgb}), url('${$meta?.banner}')`} />
|
||||||
|
|
||||||
<div class="flex gap-4 text-neutral-100">
|
<div class="flex gap-4 text-neutral-100">
|
||||||
<GroupCircle {address} class="mt-1 h-10 w-10 sm:h-32 sm:w-32" />
|
<GroupCircle {address} class="mt-1 h-10 w-10 sm:h-32 sm:w-32" />
|
||||||
<div class="flex min-w-0 flex-grow flex-col gap-4">
|
<div class="flex min-w-0 flex-grow flex-col gap-4">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<Anchor on:click={() => setActiveTab("notes")} class="text-2xl"
|
<Anchor on:click={() => setActiveTab("notes")} class="text-2xl"
|
||||||
>{displayGroup($group)}</Anchor>
|
>{displayGroupMeta($meta)}</Anchor>
|
||||||
<GroupActions {address} {claim} />
|
<GroupActions {address} {claim} />
|
||||||
</div>
|
</div>
|
||||||
<GroupAbout {address} />
|
<GroupAbout {address} />
|
||||||
|
@ -1,46 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {prop} from "ramda"
|
import {prop} from "ramda"
|
||||||
|
import {COMMUNITY} from "@welshman/util"
|
||||||
import {showInfo} from "src/partials/Toast.svelte"
|
import {showInfo} from "src/partials/Toast.svelte"
|
||||||
import type {Values} from "src/app/shared/GroupDetailsForm.svelte"
|
|
||||||
import GroupDetailsForm from "src/app/shared/GroupDetailsForm.svelte"
|
import GroupDetailsForm from "src/app/shared/GroupDetailsForm.svelte"
|
||||||
|
import type {GroupMeta} from "src/domain"
|
||||||
import {
|
import {
|
||||||
|
deriveGroup,
|
||||||
deleteGroupMeta,
|
deleteGroupMeta,
|
||||||
publishGroupMeta,
|
publishGroupMeta,
|
||||||
publishCommunityMeta,
|
publishCommunityMeta,
|
||||||
getGroupId,
|
deriveGroupMeta,
|
||||||
getGroupName,
|
|
||||||
deriveGroup,
|
|
||||||
} from "src/engine"
|
} from "src/engine"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
|
|
||||||
const group = deriveGroup(address)
|
const group = deriveGroup(address)
|
||||||
|
const meta = deriveGroupMeta(address)
|
||||||
|
const initialValues = {...$meta, members: $group?.members || []}
|
||||||
|
|
||||||
const initialValues = {
|
const onSubmit = async ({
|
||||||
id: getGroupId($group),
|
kind,
|
||||||
type: address.startsWith("34550:") ? "open" : "closed",
|
identifier,
|
||||||
feeds: $group.feeds || [],
|
listing_is_public,
|
||||||
relays: $group.relays || [],
|
...meta
|
||||||
list_publicly: $group.listing_is_public,
|
}: GroupMeta & {members: string[]}) => {
|
||||||
meta: {
|
|
||||||
name: getGroupName($group),
|
|
||||||
about: $group.meta?.about || "",
|
|
||||||
picture: $group.meta?.picture || "",
|
|
||||||
banner: $group.meta?.banner || "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async ({id, type, list_publicly, feeds, relays, meta}: Values) => {
|
|
||||||
// If we're switching group listing visibility, delete the old listing
|
// If we're switching group listing visibility, delete the old listing
|
||||||
if ($group.listing_is_public && !list_publicly) {
|
if (listing_is_public && !initialValues.listing_is_public) {
|
||||||
await prop("result", await deleteGroupMeta($group.address))
|
await prop("result", await deleteGroupMeta(address))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "open") {
|
if (kind === COMMUNITY) {
|
||||||
await publishCommunityMeta(address, id, feeds, relays, meta)
|
await publishCommunityMeta(address, identifier, meta)
|
||||||
} else {
|
} else {
|
||||||
await publishGroupMeta(address, id, feeds, relays, meta, list_publicly)
|
await publishGroupMeta(address, identifier, meta, listing_is_public)
|
||||||
}
|
}
|
||||||
|
|
||||||
showInfo("Your group has been updated!")
|
showInfo("Your group has been updated!")
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {toNostrURI} from "@welshman/util"
|
import {nth} from "@welshman/lib"
|
||||||
|
import {toNostrURI, Address} from "@welshman/util"
|
||||||
import {nsecEncode} from "src/util/nostr"
|
import {nsecEncode} from "src/util/nostr"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Popover from "src/partials/Popover.svelte"
|
import Popover from "src/partials/Popover.svelte"
|
||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
import CopyValue from "src/partials/CopyValue.svelte"
|
import CopyValue from "src/partials/CopyValue.svelte"
|
||||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||||
import {groups, deriveAdminKeyForGroup, getGroupNaddr} from "src/engine"
|
import {deriveGroupMeta, deriveAdminKeyForGroup} from "src/engine"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
|
|
||||||
const group = groups.key(address)
|
const meta = deriveGroupMeta(address)
|
||||||
const adminKey = deriveAdminKeyForGroup(address)
|
const adminKey = deriveAdminKeyForGroup(address)
|
||||||
|
const naddr = Address.from(address, $meta?.relays.map(nth(1)) || []).toNaddr()
|
||||||
|
|
||||||
const shareAdminKey = () => {
|
const shareAdminKey = () => {
|
||||||
popover?.hide()
|
popover?.hide()
|
||||||
@ -24,7 +26,7 @@
|
|||||||
|
|
||||||
<h1 class="staatliches text-2xl">Details</h1>
|
<h1 class="staatliches text-2xl">Details</h1>
|
||||||
<CopyValue label="Group ID" value={address} />
|
<CopyValue label="Group ID" value={address} />
|
||||||
<CopyValue label="Link" value={toNostrURI(getGroupNaddr($group))} />
|
<CopyValue label="Link" value={toNostrURI(naddr)} />
|
||||||
{#if $adminKey}
|
{#if $adminKey}
|
||||||
<CopyValue isPassword label="Admin key" value={$adminKey.privkey} encode={nsecEncode}>
|
<CopyValue isPassword label="Admin key" value={$adminKey.privkey} encode={nsecEncode}>
|
||||||
<div slot="label" class="flex gap-2">
|
<div slot="label" class="flex gap-2">
|
||||||
@ -46,12 +48,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</CopyValue>
|
</CopyValue>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $group.relays?.length > 0}
|
{#if $meta?.relays.length > 0}
|
||||||
<h1 class="staatliches text-2xl">Relays</h1>
|
<h1 class="staatliches text-2xl">Relays</h1>
|
||||||
<p>This group uses the following relays:</p>
|
<p>This group uses the following relays:</p>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#each $group.relays as url}
|
{#each $meta.relays as tag}
|
||||||
<RelayCard {url} />
|
<RelayCard url={tag[1]} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {filter, assoc} from "ramda"
|
import {filter, reject, assoc} from "ramda"
|
||||||
|
import {derived} from "svelte/store"
|
||||||
import {now, shuffle} from "@welshman/lib"
|
import {now, shuffle} from "@welshman/lib"
|
||||||
import {GROUP, COMMUNITY, getIdFilters} from "@welshman/util"
|
import {GROUP, COMMUNITY, getAddress, getIdFilters} from "@welshman/util"
|
||||||
import {createScroller} from "src/util/misc"
|
import {createScroller} from "src/util/misc"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
@ -12,40 +13,37 @@
|
|||||||
load,
|
load,
|
||||||
hints,
|
hints,
|
||||||
groups,
|
groups,
|
||||||
repository,
|
|
||||||
loadGiftWraps,
|
loadGiftWraps,
|
||||||
loadGroupMessages,
|
loadGroupMessages,
|
||||||
deriveIsGroupMember,
|
deriveIsGroupMember,
|
||||||
updateCurrentSession,
|
updateCurrentSession,
|
||||||
communityListsByAddress,
|
communityListsByAddress,
|
||||||
searchGroups,
|
searchGroupMeta,
|
||||||
|
groupMeta,
|
||||||
} from "src/engine"
|
} from "src/engine"
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
limit += 20
|
limit += 20
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIsMember = g => deriveIsGroupMember(g.address, true).get()
|
const userIsMember = meta => deriveIsGroupMember(getAddress(meta.event), true).get()
|
||||||
|
|
||||||
const userGroups = groups.derived(
|
const userGroupMeta = derived(groupMeta, filter(userIsMember))
|
||||||
filter(g => !repository.deletes.has(g.address) && userIsMember(g)),
|
|
||||||
)
|
|
||||||
|
|
||||||
let q = ""
|
let q = ""
|
||||||
let limit = 20
|
let limit = 20
|
||||||
let element = null
|
let element = null
|
||||||
|
|
||||||
$: otherGroups = $searchGroups(q)
|
$: otherGroupMeta = reject(userIsMember, $searchGroupMeta(q)).slice(0, limit)
|
||||||
.filter(g => !userIsMember(g))
|
|
||||||
.slice(0, limit)
|
|
||||||
|
|
||||||
document.title = "Groups"
|
document.title = "Groups"
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const loader = loadGiftWraps()
|
const loader = loadGiftWraps()
|
||||||
const scroller = createScroller(loadMore, {element})
|
const scroller = createScroller(loadMore, {element})
|
||||||
const communityAddrs = Array.from($communityListsByAddress.keys())
|
const communityAddrs = Array.from($communityListsByAddress.keys()).filter(
|
||||||
.filter(a => !groups.key(a).get()?.meta)
|
a => !groups.key(a).get()?.meta,
|
||||||
|
)
|
||||||
|
|
||||||
updateCurrentSession(assoc("groups_last_synced", now()))
|
updateCurrentSession(assoc("groups_last_synced", now()))
|
||||||
|
|
||||||
@ -81,8 +79,8 @@
|
|||||||
<i class="fa-solid fa-plus" /> Create
|
<i class="fa-solid fa-plus" /> Create
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</div>
|
</div>
|
||||||
{#each $userGroups as group (group.address)}
|
{#each $userGroupMeta as meta (meta.event.id)}
|
||||||
<GroupListItem address={group.address} />
|
<GroupListItem address={getAddress(meta.event)} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-center py-8">You haven't yet joined any groups.</p>
|
<p class="text-center py-8">You haven't yet joined any groups.</p>
|
||||||
{/each}
|
{/each}
|
||||||
@ -90,7 +88,7 @@
|
|||||||
<Input bind:value={q} type="text" class="flex-grow" placeholder="Search groups">
|
<Input bind:value={q} type="text" class="flex-grow" placeholder="Search groups">
|
||||||
<i slot="before" class="fa-solid fa-search" />
|
<i slot="before" class="fa-solid fa-search" />
|
||||||
</Input>
|
</Input>
|
||||||
{#each otherGroups as group (group.address)}
|
{#each otherGroupMeta as meta (meta.event.id)}
|
||||||
<GroupListItem address={group.address} />
|
<GroupListItem address={getAddress(meta.event)} />
|
||||||
{/each}
|
{/each}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {without} from "ramda"
|
import {without} from "@welshman/lib"
|
||||||
import {difference} from "hurdak"
|
import {difference} from "hurdak"
|
||||||
import {showInfo} from "src/partials/Toast.svelte"
|
import {showInfo} from "src/partials/Toast.svelte"
|
||||||
import Field from "src/partials/Field.svelte"
|
import Field from "src/partials/Field.svelte"
|
||||||
@ -11,9 +11,11 @@
|
|||||||
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
||||||
import type {GroupRequest} from "src/engine"
|
import type {GroupRequest} from "src/engine"
|
||||||
import {
|
import {
|
||||||
|
hints,
|
||||||
groups,
|
groups,
|
||||||
groupRequests,
|
groupRequests,
|
||||||
initSharedKey,
|
initSharedKey,
|
||||||
|
deriveGroupMeta,
|
||||||
deriveSharedKeyForGroup,
|
deriveSharedKeyForGroup,
|
||||||
publishGroupInvites,
|
publishGroupInvites,
|
||||||
publishGroupEvictions,
|
publishGroupEvictions,
|
||||||
@ -27,6 +29,7 @@
|
|||||||
export let removeMembers = []
|
export let removeMembers = []
|
||||||
|
|
||||||
const group = groups.key(address)
|
const group = groups.key(address)
|
||||||
|
const meta = deriveGroupMeta(address)
|
||||||
const sharedKey = deriveSharedKeyForGroup(address)
|
const sharedKey = deriveSharedKeyForGroup(address)
|
||||||
const initialMembers = new Set(
|
const initialMembers = new Set(
|
||||||
without(removeMembers, [...($group?.members || []), ...addMembers]),
|
without(removeMembers, [...($group?.members || []), ...addMembers]),
|
||||||
@ -34,7 +37,7 @@
|
|||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (!soft || !$sharedKey) {
|
if (!soft || !$sharedKey) {
|
||||||
initSharedKey(address)
|
initSharedKey(address, hints.WithinContext(address).getUrls())
|
||||||
}
|
}
|
||||||
|
|
||||||
const allMembers = new Set(members)
|
const allMembers = new Set(members)
|
||||||
@ -81,8 +84,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-publish group info
|
// Re-publish group info
|
||||||
if (!soft && !$group.listing_is_public) {
|
if (!soft && !$meta.listing_is_public) {
|
||||||
publishGroupMeta(address, $group.id, $group.feeds, $group.relays, $group.meta, false)
|
publishGroupMeta(address, $meta.identifier, $meta, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-send invites. This could be optimized further, but it's useful to re-send to different relays.
|
// Re-send invites. This could be optimized further, but it's useful to re-send to different relays.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {zipObj, uniq, pluck} from "ramda"
|
import {zipObj, pluck} from "ramda"
|
||||||
import {normalizeRelayUrl, Address} from "@welshman/util"
|
import {normalizeRelayUrl} from "@welshman/util"
|
||||||
import {updateIn} from "src/util/misc"
|
import {updateIn} from "src/util/misc"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
import Heading from "src/partials/Heading.svelte"
|
import Heading from "src/partials/Heading.svelte"
|
||||||
@ -12,7 +12,7 @@
|
|||||||
import GroupActions from "src/app/shared/GroupActions.svelte"
|
import GroupActions from "src/app/shared/GroupActions.svelte"
|
||||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||||
import Onboarding from "src/app/views/Onboarding.svelte"
|
import Onboarding from "src/app/views/Onboarding.svelte"
|
||||||
import {session, loadGroups, groups as allGroups} from "src/engine"
|
import {session, loadGroups, groupHints} from "src/engine"
|
||||||
|
|
||||||
export let people = []
|
export let people = []
|
||||||
export let relays = []
|
export let relays = []
|
||||||
@ -30,24 +30,9 @@
|
|||||||
|
|
||||||
loadGroups(pluck("address", parsedGroups) as string[], pluck("relay", parsedGroups) as string[])
|
loadGroups(pluck("address", parsedGroups) as string[], pluck("relay", parsedGroups) as string[])
|
||||||
|
|
||||||
// Add relay hints to groups so we can use them deep in the call stack
|
|
||||||
for (const {address, relay} of parsedGroups) {
|
for (const {address, relay} of parsedGroups) {
|
||||||
const group = allGroups.key(address)
|
|
||||||
|
|
||||||
if (relay) {
|
if (relay) {
|
||||||
const {identifier, pubkey} = Address.from(address)
|
groupHints.update($gh => ({...$gh, [address]: [...$gh[address], relay]}))
|
||||||
|
|
||||||
group.update($g => {
|
|
||||||
const {relays = []} = $g
|
|
||||||
|
|
||||||
return {
|
|
||||||
...$g,
|
|
||||||
address,
|
|
||||||
id: identifier,
|
|
||||||
pubkey: pubkey,
|
|
||||||
relays: uniq([...relays, relay]),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {prop} from "ramda"
|
|
||||||
import {without, identity} from "@welshman/lib"
|
import {without, identity} from "@welshman/lib"
|
||||||
|
import {getAddress} from "@welshman/util"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {pickVals, toSpliced} from "src/util/misc"
|
import {pickVals, toSpliced} from "src/util/misc"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
@ -15,8 +15,8 @@
|
|||||||
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
||||||
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
import {displayRelayUrl} from "src/domain"
|
import {displayRelayUrl, displayGroupMeta} from "src/domain"
|
||||||
import {hints, relaySearch, searchGroups, displayGroup, deriveGroup} from "src/engine"
|
import {hints, relaySearch, searchGroupMeta, groupMetaByAddress} from "src/engine"
|
||||||
|
|
||||||
export let initialPubkey = null
|
export let initialPubkey = null
|
||||||
export let initialGroupAddress = null
|
export let initialGroupAddress = null
|
||||||
@ -70,7 +70,7 @@
|
|||||||
groups = toSpliced(groups, i, 1)
|
groups = toSpliced(groups, i, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayGroupFromAddress = a => displayGroup(deriveGroup(a).get())
|
const displayGroupFromAddress = a => displayGroupMeta($groupMetaByAddress.get(a))
|
||||||
|
|
||||||
let relayInput, groupInput
|
let relayInput, groupInput
|
||||||
let sections = []
|
let sections = []
|
||||||
@ -194,14 +194,14 @@
|
|||||||
<SearchSelect
|
<SearchSelect
|
||||||
value={null}
|
value={null}
|
||||||
bind:this={groupInput}
|
bind:this={groupInput}
|
||||||
search={$searchGroups}
|
search={$searchGroupMeta}
|
||||||
getKey={prop("address")}
|
displayItem={displayGroupMeta}
|
||||||
onChange={g => g && addGroup(g.address)}
|
getKey={groupMeta => getAddress(groupMeta.event)}
|
||||||
displayItem={g => (g ? displayGroup(g) : "")}>
|
onChange={groupMeta => groupMeta && addGroup(getAddress(groupMeta.event))}>
|
||||||
<i slot="before" class="fa fa-search" />
|
<i slot="before" class="fa fa-search" />
|
||||||
<div slot="item" let:item class="flex items-center gap-4 text-neutral-100">
|
<div slot="item" let:item class="flex items-center gap-4 text-neutral-100">
|
||||||
<GroupCircle address={item.address} class="h-5 w-5" />
|
<GroupCircle address={getAddress(item.event)} class="h-5 w-5" />
|
||||||
<GroupName address={item.address} />
|
<GroupName address={getAddress(item.event)} />
|
||||||
</div>
|
</div>
|
||||||
</SearchSelect>
|
</SearchSelect>
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<i class="fa fa-list fa-lg" />
|
<i class="fa fa-list fa-lg" />
|
||||||
<h2 class="staatliches text-2xl">Your feeds</h2>
|
<h2 class="staatliches text-2xl">Your lists</h2>
|
||||||
</div>
|
</div>
|
||||||
<Anchor button accent on:click={createList}>
|
<Anchor button accent on:click={createList}>
|
||||||
<i class="fa fa-plus" /> List
|
<i class="fa fa-plus" /> List
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import {join, whereEq, identity} from "ramda"
|
import {join, whereEq, identity} from "ramda"
|
||||||
import {throttle, commaFormat, toTitle, switcherFn} from "hurdak"
|
import {throttle, commaFormat, toTitle, switcherFn} from "hurdak"
|
||||||
import {now, writable} from "@welshman/lib"
|
import {now, writable} from "@welshman/lib"
|
||||||
import {createEvent, Tags} from "@welshman/util"
|
import {createEvent} from "@welshman/util"
|
||||||
import {currencyOptions} from "src/util/i18n"
|
import {currencyOptions} from "src/util/i18n"
|
||||||
import {dateToSeconds} from "src/util/misc"
|
import {dateToSeconds} from "src/util/misc"
|
||||||
import {showWarning, showPublishInfo} from "src/partials/Toast.svelte"
|
import {showWarning, showPublishInfo} from "src/partials/Toast.svelte"
|
||||||
@ -44,11 +44,7 @@
|
|||||||
export let group = null
|
export let group = null
|
||||||
export let initialValues = {}
|
export let initialValues = {}
|
||||||
|
|
||||||
const defaultGroups = $env.FORCE_GROUP
|
const defaultGroups = $env.FORCE_GROUP ? [$env.FORCE_GROUP] : [group].filter(identity)
|
||||||
? [$env.FORCE_GROUP]
|
|
||||||
: quote
|
|
||||||
? Tags.fromEvent(quote).context().values().valueOf()
|
|
||||||
: [group].filter(identity)
|
|
||||||
|
|
||||||
let images, compose
|
let images, compose
|
||||||
let charCount = 0
|
let charCount = 0
|
||||||
|
41
src/domain/group.ts
Normal file
41
src/domain/group.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {fromPairs, nthEq} from '@welshman/lib'
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {isSignedEvent} from "@welshman/util"
|
||||||
|
|
||||||
|
export type GroupMeta = {
|
||||||
|
kind: number
|
||||||
|
feeds: string[][]
|
||||||
|
relays: string[][]
|
||||||
|
moderators: string[][]
|
||||||
|
identifier: string
|
||||||
|
name: string
|
||||||
|
about: string
|
||||||
|
banner: string
|
||||||
|
image: string
|
||||||
|
listing_is_public: boolean
|
||||||
|
event?: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PublishedGroupMeta = Omit<GroupMeta, "event"> & {
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readGroupMeta = (event: TrustedEvent) => {
|
||||||
|
const meta = fromPairs(event.tags)
|
||||||
|
|
||||||
|
return {
|
||||||
|
event,
|
||||||
|
kind: event.kind,
|
||||||
|
feeds: event.tags.filter(nthEq(0, 'feed')),
|
||||||
|
relays: event.tags.filter(nthEq(0, 'relay')),
|
||||||
|
moderators: event.tags.filter(nthEq(0, 'p')),
|
||||||
|
identifier: meta.d,
|
||||||
|
name: meta.name,
|
||||||
|
about: meta.about,
|
||||||
|
banner: meta.banner,
|
||||||
|
image: meta.image || meta.picture,
|
||||||
|
listing_is_public: isSignedEvent(event),
|
||||||
|
} as PublishedGroupMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayGroupMeta = (meta: GroupMeta) => meta?.name || meta?.identifier || "[no name]"
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./collection"
|
export * from "./collection"
|
||||||
export * from "./feed"
|
export * from "./feed"
|
||||||
|
export * from "./group"
|
||||||
export * from "./handle"
|
export * from "./handle"
|
||||||
export * from "./handler"
|
export * from "./handler"
|
||||||
export * from "./kind"
|
export * from "./kind"
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
Address,
|
Address,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
|
FEEDS,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
@ -51,7 +52,6 @@ import {
|
|||||||
loadOne,
|
loadOne,
|
||||||
createAndPublish,
|
createAndPublish,
|
||||||
deriveAdminKeyForGroup,
|
deriveAdminKeyForGroup,
|
||||||
deriveGroup,
|
|
||||||
deriveIsGroupMember,
|
deriveIsGroupMember,
|
||||||
deriveSharedKeyForGroup,
|
deriveSharedKeyForGroup,
|
||||||
displayProfileByPubkey,
|
displayProfileByPubkey,
|
||||||
@ -250,7 +250,7 @@ export const updateZapper = async ({pubkey, created_at}, {lud16, lud06}) => {
|
|||||||
|
|
||||||
// Key state management
|
// Key state management
|
||||||
|
|
||||||
export const initSharedKey = address => {
|
export const initSharedKey = (address: string, relays: string[]) => {
|
||||||
const privkey = generatePrivateKey()
|
const privkey = generatePrivateKey()
|
||||||
const pubkey = getPublicKey(privkey)
|
const pubkey = getPublicKey(privkey)
|
||||||
const key = {
|
const key = {
|
||||||
@ -258,6 +258,7 @@ export const initSharedKey = address => {
|
|||||||
pubkey: pubkey,
|
pubkey: pubkey,
|
||||||
privkey: privkey,
|
privkey: privkey,
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
|
hints: relays,
|
||||||
}
|
}
|
||||||
|
|
||||||
groupSharedKeys.key(pubkey).set(key)
|
groupSharedKeys.key(pubkey).set(key)
|
||||||
@ -266,24 +267,24 @@ export const initSharedKey = address => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const initGroup = (kind, relays) => {
|
export const initGroup = (kind, relays) => {
|
||||||
const id = randomId()
|
const identifier = randomId()
|
||||||
const privkey = generatePrivateKey()
|
const privkey = generatePrivateKey()
|
||||||
const pubkey = getPublicKey(privkey)
|
const pubkey = getPublicKey(privkey)
|
||||||
const address = `${kind}:${pubkey}:${id}`
|
const address = `${kind}:${pubkey}:${identifier}`
|
||||||
const sharedKey = kind === 35834 ? initSharedKey(address) : null
|
const sharedKey = kind === 35834 ? initSharedKey(address, relays) : null
|
||||||
const adminKey = {
|
const adminKey = {
|
||||||
group: address,
|
group: address,
|
||||||
pubkey: pubkey,
|
pubkey: pubkey,
|
||||||
privkey: privkey,
|
privkey: privkey,
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
relays,
|
hints: relays,
|
||||||
}
|
}
|
||||||
|
|
||||||
groupAdminKeys.key(pubkey).set(adminKey)
|
groupAdminKeys.key(pubkey).set(adminKey)
|
||||||
|
|
||||||
groups.key(address).set({id, pubkey, address, relays})
|
groups.key(address).set({id: identifier, pubkey, address})
|
||||||
|
|
||||||
return {id, address, adminKey, sharedKey}
|
return {identifier, address, adminKey, sharedKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most people don't have access to nip44 yet, send nip04-encrypted fallbacks for:
|
// Most people don't have access to nip44 yet, send nip04-encrypted fallbacks for:
|
||||||
@ -485,7 +486,7 @@ export const publishKeyShares = async (address, pubkeys, template) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const publishAdminKeyShares = async (address, pubkeys) => {
|
export const publishAdminKeyShares = async (address, pubkeys) => {
|
||||||
const {relays} = deriveGroup(address).get()
|
const relays = hints.WithinContext(address).getUrls()
|
||||||
const {privkey} = deriveAdminKeyForGroup(address).get()
|
const {privkey} = deriveAdminKeyForGroup(address).get()
|
||||||
const template = createEvent(24, {
|
const template = createEvent(24, {
|
||||||
tags: [
|
tags: [
|
||||||
@ -501,7 +502,7 @@ export const publishAdminKeyShares = async (address, pubkeys) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const publishGroupInvites = async (address, pubkeys, gracePeriod = 0) => {
|
export const publishGroupInvites = async (address, pubkeys, gracePeriod = 0) => {
|
||||||
const {relays} = deriveGroup(address).get()
|
const relays = hints.WithinContext(address).getUrls()
|
||||||
const adminKey = deriveAdminKeyForGroup(address).get()
|
const adminKey = deriveAdminKeyForGroup(address).get()
|
||||||
const {privkey} = deriveSharedKeyForGroup(address).get()
|
const {privkey} = deriveSharedKeyForGroup(address).get()
|
||||||
const template = createEvent(24, {
|
const template = createEvent(24, {
|
||||||
@ -535,40 +536,44 @@ export const publishGroupMembers = async (address, op, pubkeys) => {
|
|||||||
return publishAsGroupAdminPrivately(address, template)
|
return publishAsGroupAdminPrivately(address, template)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishCommunityMeta = (address, id, feeds, relays, meta) => {
|
export const publishCommunityMeta = (address, identifier, meta) => {
|
||||||
const template = createEvent(34550, {
|
const template = createEvent(34550, {
|
||||||
tags: [
|
tags: [
|
||||||
["d", id],
|
["d", identifier],
|
||||||
["name", meta.name],
|
["name", meta.name],
|
||||||
["description", meta.about],
|
["description", meta.about],
|
||||||
["banner", meta.banner],
|
["banner", meta.banner],
|
||||||
["image", meta.picture],
|
["picture", meta.image],
|
||||||
|
["image", meta.image],
|
||||||
|
...meta.feeds,
|
||||||
|
...meta.relays,
|
||||||
|
...meta.moderators,
|
||||||
...getClientTags(),
|
...getClientTags(),
|
||||||
...(feeds || []),
|
|
||||||
...(relays || []).map(url => ["relay", url]),
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
return publishAsGroupAdminPublicly(address, template, relays)
|
return publishAsGroupAdminPublicly(address, template, meta.relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishGroupMeta = (address, id, feeds, relays, meta, listPublicly) => {
|
export const publishGroupMeta = (address, identifier, meta, listPublicly) => {
|
||||||
const template = createEvent(35834, {
|
const template = createEvent(35834, {
|
||||||
tags: [
|
tags: [
|
||||||
["d", id],
|
["d", identifier],
|
||||||
["name", meta.name],
|
["name", meta.name],
|
||||||
["about", meta.about],
|
["about", meta.about],
|
||||||
["banner", meta.banner],
|
["banner", meta.banner],
|
||||||
["picture", meta.picture],
|
["picture", meta.image],
|
||||||
|
["image", meta.image],
|
||||||
|
...meta.feeds,
|
||||||
|
...meta.relays,
|
||||||
|
...meta.moderators,
|
||||||
...getClientTags(),
|
...getClientTags(),
|
||||||
...(feeds || []),
|
|
||||||
...(relays || []).map(url => ["relay", url]),
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
return listPublicly
|
return listPublicly
|
||||||
? publishAsGroupAdminPublicly(address, template, relays)
|
? publishAsGroupAdminPublicly(address, template, meta.relays)
|
||||||
: publishAsGroupAdminPrivately(address, template, relays)
|
: publishAsGroupAdminPrivately(address, template, meta.relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteGroupMeta = address =>
|
export const deleteGroupMeta = address =>
|
||||||
@ -739,6 +744,12 @@ export const unmuteNote = (id: string) => updateSingleton(MUTES, tags => reject(
|
|||||||
|
|
||||||
export const muteNote = (id: string) => updateSingleton(MUTES, tags => append(tags, ["e", id]))
|
export const muteNote = (id: string) => updateSingleton(MUTES, tags => append(tags, ["e", id]))
|
||||||
|
|
||||||
|
export const removeFeedFavorite = (address: string) =>
|
||||||
|
updateSingleton(FEEDS, tags => reject(nthEq(1, address), tags))
|
||||||
|
|
||||||
|
export const addFeedFavorite = (address: string) =>
|
||||||
|
updateSingleton(FEEDS, tags => append(tags, ["a", address]))
|
||||||
|
|
||||||
// Relays
|
// Relays
|
||||||
|
|
||||||
export const requestRelayAccess = async (url: string, claim: string, sk?: string) =>
|
export const requestRelayAccess = async (url: string, claim: string, sk?: string) =>
|
||||||
|
@ -22,27 +22,12 @@ export enum GroupAccess {
|
|||||||
Revoked = "revoked",
|
Revoked = "revoked",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupMeta = {
|
|
||||||
name?: string
|
|
||||||
about?: string
|
|
||||||
banner?: string
|
|
||||||
picture?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Group = {
|
export type Group = {
|
||||||
id: string
|
id: string
|
||||||
pubkey: string
|
pubkey: string
|
||||||
address: string
|
address: string
|
||||||
meta?: GroupMeta
|
|
||||||
meta_updated_at?: number
|
|
||||||
feeds?: string[][]
|
|
||||||
feeds_updated_at?: number
|
|
||||||
relays?: string[]
|
|
||||||
relays_updated_at?: number
|
|
||||||
members?: string[]
|
members?: string[]
|
||||||
recent_member_updates?: TrustedEvent[]
|
recent_member_updates?: TrustedEvent[]
|
||||||
listing_is_public?: boolean
|
|
||||||
listing_is_public_updated?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupKey = {
|
export type GroupKey = {
|
||||||
|
@ -5,8 +5,6 @@ import type {TrustedEvent} from "@welshman/util"
|
|||||||
import {
|
import {
|
||||||
Tags,
|
Tags,
|
||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
Address,
|
|
||||||
getAddress,
|
|
||||||
getIdFilters,
|
getIdFilters,
|
||||||
MUTES,
|
MUTES,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
@ -43,7 +41,6 @@ import {
|
|||||||
modifyGroupStatus,
|
modifyGroupStatus,
|
||||||
setGroupStatus,
|
setGroupStatus,
|
||||||
updateRecord,
|
updateRecord,
|
||||||
updateStore,
|
|
||||||
updateSession,
|
updateSession,
|
||||||
setSession,
|
setSession,
|
||||||
updateZapper,
|
updateZapper,
|
||||||
@ -109,63 +106,11 @@ projections.addHandler(24, (e: TrustedEvent) => {
|
|||||||
groupAlerts.key(e.id).set({...e, group: address, type: "exit"})
|
groupAlerts.key(e.id).set({...e, group: address, type: "exit"})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relays.length > 0) {
|
|
||||||
const {pubkey, identifier} = Address.from(address)
|
|
||||||
|
|
||||||
if (!groups.key(address).get()) {
|
|
||||||
groups.key(address).set({address, pubkey, id: identifier, relays})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setGroupStatus(recipient, address, e.created_at, {
|
setGroupStatus(recipient, address, e.created_at, {
|
||||||
access: privkey ? GroupAccess.Granted : GroupAccess.Revoked,
|
access: privkey ? GroupAccess.Granted : GroupAccess.Revoked,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Group metadata
|
|
||||||
|
|
||||||
projections.addHandler(35834, (e: TrustedEvent) => {
|
|
||||||
const tags = Tags.fromEvent(e)
|
|
||||||
const meta = tags.asObject()
|
|
||||||
const address = getAddress(e)
|
|
||||||
const group = groups.key(address)
|
|
||||||
|
|
||||||
group.merge({address, id: meta.d, pubkey: e.pubkey})
|
|
||||||
|
|
||||||
updateStore(group, e.created_at, {
|
|
||||||
feeds: tags.whereKey("feed").unwrap(),
|
|
||||||
relays: tags.values("relay").valueOf(),
|
|
||||||
listing_is_public: !e.wrap,
|
|
||||||
meta: {
|
|
||||||
name: meta.name,
|
|
||||||
about: meta.about,
|
|
||||||
banner: meta.banner,
|
|
||||||
picture: meta.picture,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
projections.addHandler(34550, (e: TrustedEvent) => {
|
|
||||||
const tags = Tags.fromEvent(e)
|
|
||||||
const meta = tags.asObject()
|
|
||||||
const address = getAddress(e)
|
|
||||||
const group = groups.key(address)
|
|
||||||
|
|
||||||
group.merge({address, id: meta.d, pubkey: e.pubkey})
|
|
||||||
|
|
||||||
updateStore(group, e.created_at, {
|
|
||||||
feeds: tags.whereKey("feed").unwrap(),
|
|
||||||
relays: tags.values("relay").valueOf(),
|
|
||||||
listing_is_public: true,
|
|
||||||
meta: {
|
|
||||||
name: meta.name,
|
|
||||||
about: meta.description,
|
|
||||||
banner: meta.banner,
|
|
||||||
picture: meta.image,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
projections.addHandler(27, (e: TrustedEvent) => {
|
projections.addHandler(27, (e: TrustedEvent) => {
|
||||||
const address = Tags.fromEvent(e).groups().values().first()
|
const address = Tags.fromEvent(e).groups().values().first()
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ export const deriveEventsMapped = <T>({
|
|||||||
if (dirty) {
|
if (dirty) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
if (new Set(data.map(item => itemToEvent(item).id)).size < data.length) {
|
if (new Set(data.map(item => itemToEvent(item).id)).size < data.length) {
|
||||||
console.error(`Duplicate records found:`, copy, data, updates)
|
console.error(`Duplicate records found:`, copy, [...data], updates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {batch, Fetch, noop, tryFunc, seconds, createMapOf, sleep, switcherFn} from "hurdak"
|
import {batch, Fetch, noop, tryFunc, seconds, createMapOf, sleep, switcherFn} from "hurdak"
|
||||||
|
import {get} from "svelte/store"
|
||||||
import type {LoadOpts} from "@welshman/feeds"
|
import type {LoadOpts} from "@welshman/feeds"
|
||||||
import {
|
import {
|
||||||
FeedLoader,
|
FeedLoader,
|
||||||
@ -38,7 +39,6 @@ import {
|
|||||||
deriveUserCircles,
|
deriveUserCircles,
|
||||||
getGroupReqInfo,
|
getGroupReqInfo,
|
||||||
getCommunityReqInfo,
|
getCommunityReqInfo,
|
||||||
groups,
|
|
||||||
dvmRequest,
|
dvmRequest,
|
||||||
env,
|
env,
|
||||||
getFollows,
|
getFollows,
|
||||||
@ -60,6 +60,7 @@ import {
|
|||||||
subscribe,
|
subscribe,
|
||||||
subscribePersistent,
|
subscribePersistent,
|
||||||
dufflepud,
|
dufflepud,
|
||||||
|
deriveGroupMeta,
|
||||||
} from "src/engine/state"
|
} from "src/engine/state"
|
||||||
import {updateCurrentSession, updateSession} from "src/engine/commands"
|
import {updateCurrentSession, updateSession} from "src/engine/commands"
|
||||||
import {loadPubkeyRelays} from "src/engine/requests/pubkeys"
|
import {loadPubkeyRelays} from "src/engine/requests/pubkeys"
|
||||||
@ -140,9 +141,14 @@ export const getStaleAddrs = (addrs: string[]) => {
|
|||||||
|
|
||||||
for (const addr of addrs) {
|
for (const addr of addrs) {
|
||||||
const attempts = attemptedAddrs.get(addr) | 0
|
const attempts = attemptedAddrs.get(addr) | 0
|
||||||
const group = groups.key(addr).get()
|
|
||||||
|
|
||||||
if (!group?.meta || attempts === 0) {
|
if (attempts > 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = get(deriveGroupMeta(addr))
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
stale.add(addr)
|
stale.add(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
NAMED_BOOKMARKS,
|
NAMED_BOOKMARKS,
|
||||||
COMMUNITIES,
|
COMMUNITIES,
|
||||||
FEED,
|
FEED,
|
||||||
|
FEEDS,
|
||||||
MUTES,
|
MUTES,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
APP_DATA,
|
APP_DATA,
|
||||||
@ -55,7 +56,7 @@ const getFiltersForKey = (key: string, authors: string[]) => {
|
|||||||
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}]
|
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}]
|
||||||
case "pubkey/user":
|
case "pubkey/user":
|
||||||
return [
|
return [
|
||||||
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, APP_DATA]},
|
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, FEEDS]},
|
||||||
{authors, kinds: [APP_DATA], "#d": Object.values(appDataKeys)},
|
{authors, kinds: [APP_DATA], "#d": Object.values(appDataKeys)},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import Fuse from "fuse.js"
|
|||||||
import {openDB, deleteDB} from "idb"
|
import {openDB, deleteDB} from "idb"
|
||||||
import type {IDBPDatabase} from "idb"
|
import type {IDBPDatabase} from "idb"
|
||||||
import {throttle} from "throttle-debounce"
|
import {throttle} from "throttle-debounce"
|
||||||
import {derived, writable} from "svelte/store"
|
import {get, derived, writable} from "svelte/store"
|
||||||
import {defer, doPipe, batch, randomInt, seconds, sleep, switcher} from "hurdak"
|
import {defer, doPipe, batch, randomInt, seconds, sleep, switcher} from "hurdak"
|
||||||
import {
|
import {
|
||||||
any,
|
any,
|
||||||
@ -43,6 +43,9 @@ import {
|
|||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
WRAP,
|
WRAP,
|
||||||
|
FEEDS,
|
||||||
|
COMMUNITY,
|
||||||
|
GROUP,
|
||||||
WRAP_NIP04,
|
WRAP_NIP04,
|
||||||
COMMUNITIES,
|
COMMUNITIES,
|
||||||
READ_RECEIPT,
|
READ_RECEIPT,
|
||||||
@ -103,6 +106,7 @@ import type {
|
|||||||
PublishedListFeed,
|
PublishedListFeed,
|
||||||
PublishedSingleton,
|
PublishedSingleton,
|
||||||
PublishedList,
|
PublishedList,
|
||||||
|
PublishedGroupMeta,
|
||||||
RelayPolicy,
|
RelayPolicy,
|
||||||
Handle,
|
Handle,
|
||||||
} from "src/domain"
|
} from "src/domain"
|
||||||
@ -130,6 +134,7 @@ import {
|
|||||||
makeRelayPolicy,
|
makeRelayPolicy,
|
||||||
filterRelaysByNip,
|
filterRelaysByNip,
|
||||||
displayRelayUrl,
|
displayRelayUrl,
|
||||||
|
readGroupMeta,
|
||||||
} from "src/domain"
|
} from "src/domain"
|
||||||
import type {
|
import type {
|
||||||
Channel,
|
Channel,
|
||||||
@ -178,6 +183,7 @@ export const handles = withGetter(writable<Record<string, Handle>>({}))
|
|||||||
export const zappers = withGetter(writable<Record<string, Zapper>>({}))
|
export const zappers = withGetter(writable<Record<string, Zapper>>({}))
|
||||||
export const plaintext = withGetter(writable<Record<string, string>>({}))
|
export const plaintext = withGetter(writable<Record<string, string>>({}))
|
||||||
export const anonymous = withGetter(writable<AnonymousUserState>({follows: [], relays: []}))
|
export const anonymous = withGetter(writable<AnonymousUserState>({follows: [], relays: []}))
|
||||||
|
export const groupHints = withGetter(writable<Record<string, string[]>>({}))
|
||||||
|
|
||||||
export const groups = new CollectionStore<Group>("address")
|
export const groups = new CollectionStore<Group>("address")
|
||||||
export const relays = new CollectionStore<RelayInfo>("url")
|
export const relays = new CollectionStore<RelayInfo>("url")
|
||||||
@ -250,7 +256,15 @@ export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dufflepud = (path: string) => `${getSetting("dufflepud_url")}/${path}`
|
export const dufflepud = (path: string) => {
|
||||||
|
const base = getSetting("dufflepud_url")
|
||||||
|
|
||||||
|
if (!base) {
|
||||||
|
throw new Error("Dufflepud is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${base}/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
export const session = new Derived(
|
export const session = new Derived(
|
||||||
[pubkey, sessions],
|
[pubkey, sessions],
|
||||||
@ -719,46 +733,53 @@ export const deriveCommunities = (pk: string) =>
|
|||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
|
|
||||||
export const deriveGroup = address => {
|
export const groupMeta = deriveEventsMapped<PublishedGroupMeta>({
|
||||||
const {pubkey, identifier: id} = Address.from(address)
|
filters: [{kinds: [GROUP, COMMUNITY]}],
|
||||||
|
itemToEvent: prop("event"),
|
||||||
|
eventToItem: readGroupMeta,
|
||||||
|
})
|
||||||
|
|
||||||
return groups.key(address).derived(defaultTo({id, pubkey, address}))
|
export const groupMetaByAddress = withGetter(
|
||||||
}
|
derived(groupMeta, $metas => indexBy($meta => getAddress($meta.event), $metas)),
|
||||||
|
)
|
||||||
|
|
||||||
export const searchGroups = derived(
|
export const deriveGroupMeta = (address: string) =>
|
||||||
[groups.throttle(300), communityListsByAddress, userFollows],
|
derived(groupMetaByAddress, $m => $m.get(address))
|
||||||
([$groups, $communityListsByAddress, $userFollows]) => {
|
|
||||||
const options = $groups
|
|
||||||
.filter(group => !repository.deletes.has(group.address))
|
|
||||||
.map(group => {
|
|
||||||
const lists = $communityListsByAddress.get(group.address) || []
|
|
||||||
const members = lists.map(l => l.event.pubkey)
|
|
||||||
const followedMembers = intersection(members, $userFollows)
|
|
||||||
|
|
||||||
return {group, score: followedMembers.length}
|
export const searchGroupMeta = derived(
|
||||||
})
|
[groupMeta, communityListsByAddress, userFollows],
|
||||||
|
([$groupMeta, $communityListsByAddress, $userFollows]) => {
|
||||||
|
const options = $groupMeta.map(meta => {
|
||||||
|
const lists = $communityListsByAddress.get(getAddress(meta.event)) || []
|
||||||
|
const members = lists.map(l => l.event.pubkey)
|
||||||
|
const followedMembers = intersection(members, Array.from($userFollows))
|
||||||
|
|
||||||
|
return {...meta, score: followedMembers.length}
|
||||||
|
})
|
||||||
|
|
||||||
const fuse = new Fuse(options, {
|
const fuse = new Fuse(options, {
|
||||||
keys: [{name: "group.id", weight: 0.2}, "group.meta.name", "group.meta.about"],
|
keys: [{name: "identifier", weight: 0.2}, "name", {name: "about", weight: 0.5}],
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
shouldSort: false,
|
shouldSort: false,
|
||||||
includeScore: true,
|
includeScore: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (term: string) => {
|
const sortFn = (r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100)
|
||||||
if (!term) {
|
|
||||||
return sortBy(item => -item.score, options).map(item => item.group)
|
|
||||||
}
|
|
||||||
|
|
||||||
return doPipe(fuse.search(term), [
|
return (term: string) =>
|
||||||
$results =>
|
term
|
||||||
sortBy((r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100), $results),
|
? sortBy(sortFn, fuse.search(term)).map((r: any) => r.item.meta)
|
||||||
$results => $results.map((r: any) => r.item.group),
|
: sortBy(meta => -meta.score, options)
|
||||||
])
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Legacy
|
||||||
|
export const deriveGroup = address => {
|
||||||
|
const {pubkey, identifier: id} = Address.from(address)
|
||||||
|
|
||||||
|
return groups.key(address).derived(defaultTo({id, pubkey, address}))
|
||||||
|
}
|
||||||
|
|
||||||
export const getRecipientKey = wrap => {
|
export const getRecipientKey = wrap => {
|
||||||
const pubkey = Tags.fromEvent(wrap).values("p").first()
|
const pubkey = Tags.fromEvent(wrap).values("p").first()
|
||||||
const sharedKey = groupSharedKeys.key(pubkey).get()
|
const sharedKey = groupSharedKeys.key(pubkey).get()
|
||||||
@ -943,8 +964,7 @@ export const groupNotifications = new Derived(
|
|||||||
const $isEventMuted = isEventMuted.get()
|
const $isEventMuted = isEventMuted.get()
|
||||||
|
|
||||||
const shouldSkip = e => {
|
const shouldSkip = e => {
|
||||||
const tags = Tags.fromEvent(e)
|
const context = e.tags.filter(t => t[0] === "a")
|
||||||
const context = tags.context().values().valueOf()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!context.some(a => addresses.has(a)) ||
|
!context.some(a => addresses.has(a)) ||
|
||||||
@ -952,7 +972,7 @@ export const groupNotifications = new Derived(
|
|||||||
!noteKinds.includes(e.kind) ||
|
!noteKinds.includes(e.kind) ||
|
||||||
e.pubkey === $session.pubkey ||
|
e.pubkey === $session.pubkey ||
|
||||||
// Skip mentions since they're covered in normal notifications
|
// Skip mentions since they're covered in normal notifications
|
||||||
tags.values("p").has($session.pubkey) ||
|
e.tags.find(t => t[0] === "p" && t[1] === $session.pubkey) ||
|
||||||
$isEventMuted(e)
|
$isEventMuted(e)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1147,20 +1167,21 @@ export const deriveUserRelayPolicy = url =>
|
|||||||
// Relay selection
|
// Relay selection
|
||||||
|
|
||||||
export const getGroupRelayUrls = address => {
|
export const getGroupRelayUrls = address => {
|
||||||
const group = groups.key(address).get()
|
const meta = groupMetaByAddress.get().get(address)
|
||||||
const keys = groupSharedKeys.get()
|
|
||||||
|
|
||||||
if (group?.relays) {
|
if (meta?.relays) {
|
||||||
return group.relays
|
return meta.relays.map(nth(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestKey = last(sortBy(prop("created_at"), keys.filter(whereEq({group: address}))))
|
const latestKey = last(
|
||||||
|
sortBy(prop("created_at"), get(groupSharedKeys).filter(whereEq({group: address}))),
|
||||||
|
)
|
||||||
|
|
||||||
if (latestKey?.hints) {
|
if (latestKey?.hints) {
|
||||||
return latestKey.hints
|
return latestKey.hints
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return get(groupHints)[address] || []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const forceRelays = (relays: string[], forceRelays: string[]) =>
|
export const forceRelays = (relays: string[], forceRelays: string[]) =>
|
||||||
@ -1257,15 +1278,36 @@ export const listSearch = derived(lists, $lists => new ListSearch($lists))
|
|||||||
|
|
||||||
export const feeds = deriveEventsMapped<PublishedFeed>({
|
export const feeds = deriveEventsMapped<PublishedFeed>({
|
||||||
filters: [{kinds: [FEED]}],
|
filters: [{kinds: [FEED]}],
|
||||||
eventToItem: readFeed,
|
|
||||||
itemToEvent: prop("event"),
|
itemToEvent: prop("event"),
|
||||||
|
eventToItem: readFeed,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const userFeeds = derived([feeds, pubkey], ([$feeds, $pubkey]: [PublishedFeed[], string]) =>
|
export const userFeeds = derived([feeds, pubkey], ([$feeds, $pubkey]: [PublishedFeed[], string]) =>
|
||||||
sortBy(
|
$feeds.filter(feed => feed.event.pubkey === $pubkey),
|
||||||
f => f.title.toLowerCase(),
|
)
|
||||||
$feeds.filter(feed => feed.event.pubkey === $pubkey),
|
|
||||||
),
|
export const feedFavorites = deriveEventsMapped<PublishedSingleton>({
|
||||||
|
filters: [{kinds: [FEEDS]}],
|
||||||
|
itemToEvent: prop("event"),
|
||||||
|
eventToItem: event =>
|
||||||
|
readSingleton(
|
||||||
|
asDecryptedEvent(event, {
|
||||||
|
content: getPlaintext(event),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const userFeedFavorites = derived(
|
||||||
|
[feedFavorites, pubkey],
|
||||||
|
([$singletons, $pubkey]: [PublishedSingleton[], string]) =>
|
||||||
|
$singletons.find(singleton => singleton.event.pubkey === $pubkey),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const userFavoritedFeeds = derived(userFeedFavorites, $singleton =>
|
||||||
|
Array.from(getSingletonValues("a", $singleton))
|
||||||
|
.map(repository.getEvent)
|
||||||
|
.filter(identity)
|
||||||
|
.map(readFeed),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const feedSearch = derived(feeds, $feeds => new FeedSearch($feeds))
|
export const feedSearch = derived(feeds, $feeds => new FeedSearch($feeds))
|
||||||
@ -1495,6 +1537,7 @@ export const onAuth = async (url, challenge) => {
|
|||||||
|
|
||||||
export type MySubscribeRequest = SubscribeRequest & {
|
export type MySubscribeRequest = SubscribeRequest & {
|
||||||
onEvent?: (event: TrustedEvent) => void
|
onEvent?: (event: TrustedEvent) => void
|
||||||
|
onEose?: (url: string) => void
|
||||||
onComplete?: () => void
|
onComplete?: () => void
|
||||||
skipCache?: boolean
|
skipCache?: boolean
|
||||||
forcePlatform?: boolean
|
forcePlatform?: boolean
|
||||||
@ -1539,6 +1582,10 @@ export const subscribe = ({forcePlatform = true, ...request}: MySubscribeRequest
|
|||||||
projections.push(await ensureUnwrapped(event))
|
projections.push(await ensureUnwrapped(event))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (request.onEose) {
|
||||||
|
sub.emitter.on("eose", request.onEose)
|
||||||
|
}
|
||||||
|
|
||||||
if (request.onComplete) {
|
if (request.onComplete) {
|
||||||
sub.emitter.on("complete", request.onComplete)
|
sub.emitter.on("complete", request.onComplete)
|
||||||
}
|
}
|
||||||
@ -1577,7 +1624,14 @@ export const subscribePersistent = (request: MySubscribeRequest) => {
|
|||||||
|
|
||||||
export const LOAD_OPTS = {timeout: 3000, closeOnEose: true}
|
export const LOAD_OPTS = {timeout: 3000, closeOnEose: true}
|
||||||
|
|
||||||
export const load = (request: MySubscribeRequest) => subscribe({...request, ...LOAD_OPTS}).result
|
export const load = (request: MySubscribeRequest) =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
const events = []
|
||||||
|
const sub = subscribe({...request, ...LOAD_OPTS})
|
||||||
|
|
||||||
|
sub.emitter.on("event", (url: string, event: TrustedEvent) => events.push(event))
|
||||||
|
sub.emitter.on("complete", (url: string) => resolve(events))
|
||||||
|
})
|
||||||
|
|
||||||
export const loadOne = (request: MySubscribeRequest) =>
|
export const loadOne = (request: MySubscribeRequest) =>
|
||||||
new Promise<TrustedEvent | null>(resolve => {
|
new Promise<TrustedEvent | null>(resolve => {
|
||||||
@ -1986,16 +2040,19 @@ class IndexedDBAdapter {
|
|||||||
const removedRecords = prev.filter(r => !currentIds.has(r[key]))
|
const removedRecords = prev.filter(r => !currentIds.has(r[key]))
|
||||||
|
|
||||||
if (newRecords.length > 0) {
|
if (newRecords.length > 0) {
|
||||||
|
console.log("putting", name, newRecords.length, current.length)
|
||||||
await storage.bulkPut(name, newRecords)
|
await storage.bulkPut(name, newRecords)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedRecords.length > 0) {
|
if (removedRecords.length > 0) {
|
||||||
|
console.trace("deleting", name, removedRecords.length, current.length)
|
||||||
await storage.bulkDelete(name, removedRecords.map(prop(key)))
|
await storage.bulkDelete(name, removedRecords.map(prop(key)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have much more than our limit, prune our store. This will get persisted
|
// If we have much more than our limit, prune our store. This will get persisted
|
||||||
// the next time around.
|
// the next time around.
|
||||||
if (current.length > limit * 1.5) {
|
if (current.length > limit * 1.5) {
|
||||||
|
console.log("pruning", name, current.length)
|
||||||
set((sort ? sort(current) : current).slice(0, limit))
|
set((sort ? sort(current) : current).slice(0, limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import {ellipsize} from "hurdak"
|
|
||||||
import {Address} from "@welshman/util"
|
|
||||||
import type {Group} from "src/engine/model"
|
|
||||||
|
|
||||||
export const getGroupNaddr = (group: Group) => Address.from(group.address, group.relays).toNaddr()
|
|
||||||
|
|
||||||
export const getGroupId = (group: Group) => group.address.split(":").slice(2).join(":")
|
|
||||||
|
|
||||||
export const getGroupName = (group: Group) => group.meta?.name || group.id || ""
|
|
||||||
|
|
||||||
export const displayGroup = (group: Group) => ellipsize(group ? getGroupName(group) : "No name", 60)
|
|
@ -12,7 +12,6 @@ export * from "./nip59"
|
|||||||
export * from "./signer"
|
export * from "./signer"
|
||||||
export * from "./connect"
|
export * from "./connect"
|
||||||
export * from "./events"
|
export * from "./events"
|
||||||
export * from "./groups"
|
|
||||||
|
|
||||||
export const getConnect = memoize(session => new Connect(session))
|
export const getConnect = memoize(session => new Connect(session))
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
const loadPreview = async () => {
|
const loadPreview = async () => {
|
||||||
const json = await Fetch.postJson(dufflepud("link/preview"), {url})
|
const json = await Fetch.postJson(dufflepud("link/preview"), {url})
|
||||||
|
|
||||||
if (!json.title && !json.image) {
|
if (!json?.title && !json?.image) {
|
||||||
throw new Error("Unable to load preview")
|
throw new Error("Failed to load link preview")
|
||||||
}
|
}
|
||||||
|
|
||||||
return json
|
return json
|
||||||
@ -81,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:catch}
|
{:catch}
|
||||||
<p class="mb-1 px-12 py-24 text-center text-neutral-600">
|
<p class="mb-1 p-12 text-center text-neutral-600">
|
||||||
Unable to load a preview for {url}
|
Unable to load a preview for {url}
|
||||||
</p>
|
</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Media from "src/partials/Media.svelte"
|
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
|
||||||
import Modal from "src/partials/Modal.svelte"
|
|
||||||
|
|
||||||
export let links
|
|
||||||
|
|
||||||
let showModal = false
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
showModal = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
showModal = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="my-8 flex justify-center">
|
|
||||||
<Anchor button on:click={openModal}>
|
|
||||||
<i class="fa fa-plus" /> Show all {links.length} link previews
|
|
||||||
</Anchor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showModal}
|
|
||||||
<Modal onEscape={closeModal}>
|
|
||||||
{#each links as url}
|
|
||||||
<Media {url} />
|
|
||||||
{/each}
|
|
||||||
</Modal>
|
|
||||||
{/if}
|
|
@ -5,16 +5,18 @@
|
|||||||
export let displayOption = x => x
|
export let displayOption = x => x
|
||||||
|
|
||||||
const getClassName = active =>
|
const getClassName = active =>
|
||||||
cx("px-4 h-7 transition-all rounded-full mr-2 mb-2 inline-flex items-center", {
|
cx("px-4 h-6 transition-all rounded-full mr-2 mb-2 inline-block items-center", {
|
||||||
"bg-neutral-900 dark:bg-tinted-100 text-accent": active,
|
"bg-neutral-900 dark:bg-tinted-100 text-accent": active,
|
||||||
"bg-neutral-900 text-neutral-400": !active,
|
"bg-neutral-900 text-neutral-400": !active,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="-mb-2 inline-block">
|
<div class={cx($$props.class, "-mb-2 inline-block")}>
|
||||||
<SelectList {...$$props} class="staatliches inline-flex">
|
<SelectList {...$$props} optionClass="staatliches inline-block">
|
||||||
<div slot="item" let:i let:active let:option class={getClassName(active)}>
|
<div slot="item" let:i let:active let:option class={getClassName(active)}>
|
||||||
{displayOption(option)}
|
<slot name="item" {option} {active}>
|
||||||
|
{displayOption(option)}
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</SelectList>
|
</SelectList>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
export let onChange = null
|
export let onChange = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let multiple = false
|
export let multiple = false
|
||||||
|
export let optionClass = ""
|
||||||
|
|
||||||
const onClick = option => {
|
const onClick = option => {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
@ -24,7 +25,7 @@
|
|||||||
class:opacity-75={disabled}
|
class:opacity-75={disabled}
|
||||||
class:cursor-pointer={!disabled}>
|
class:cursor-pointer={!disabled}>
|
||||||
{#each options as option, i}
|
{#each options as option, i}
|
||||||
<div on:click={() => onClick(option)}>
|
<div class={optionClass} on:click={() => onClick(option)}>
|
||||||
<slot
|
<slot
|
||||||
name="item"
|
name="item"
|
||||||
{i}
|
{i}
|
||||||
|
Loading…
Reference in New Issue
Block a user