mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 00:10:52 +00:00
Compare commits
7 Commits
b92b0b71b5
...
1408f782f1
Author | SHA1 | Date | |
---|---|---|---|
|
1408f782f1 | ||
|
b1f2218e81 | ||
|
282a80cdea | ||
|
e212297396 | ||
|
c9db37c92a | ||
|
148a63d95f | ||
|
a5517f2eff |
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 0.4.8
|
||||||
|
|
||||||
|
- [x] Add support for kind 10050 relay lists
|
||||||
|
- [x] Toggle nip44 messages based on 10050 signaling
|
||||||
|
|
||||||
# 0.4.7
|
# 0.4.7
|
||||||
|
|
||||||
- [x] Show toast when offline
|
- [x] Show toast when offline
|
||||||
@ -11,6 +16,7 @@
|
|||||||
- [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 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.
@ -57,9 +57,9 @@
|
|||||||
"@scure/base": "^1.1.6",
|
"@scure/base": "^1.1.6",
|
||||||
"@welshman/content": "^0.0.5",
|
"@welshman/content": "^0.0.5",
|
||||||
"@welshman/feeds": "^0.0.12",
|
"@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;
|
||||||
|
}
|
||||||
|
@ -319,6 +319,7 @@
|
|||||||
required: ["splits"],
|
required: ["splits"],
|
||||||
serializers: {
|
serializers: {
|
||||||
eid: asNote,
|
eid: asNote,
|
||||||
|
amount: asJson("amount"),
|
||||||
splits: asJson("splits"),
|
splits: asJson("splits"),
|
||||||
anonymous: asJson("anonymous"),
|
anonymous: asJson("anonymous"),
|
||||||
},
|
},
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {writable} from "@welshman/lib"
|
import {writable, hash} from "@welshman/lib"
|
||||||
import {createScroller, synced} from "src/util/misc"
|
import {createScroller, synced} from "src/util/misc"
|
||||||
import {fly, fade} from "src/util/transition"
|
import {fly, fade} from "src/util/transition"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
import Card from "src/partials/Card.svelte"
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
import Note from "src/app/shared/Note.svelte"
|
import Note from "src/app/shared/Note.svelte"
|
||||||
import FeedControls from "src/app/shared/FeedControls.svelte"
|
import FeedControls from "src/app/shared/FeedControls.svelte"
|
||||||
import {createFeed} from "src/app/util"
|
import {createFeed, router} from "src/app/util"
|
||||||
import type {Feed} from "src/domain"
|
import type {Feed} from "src/domain"
|
||||||
|
import {env} from "src/engine"
|
||||||
|
|
||||||
export let feed: Feed
|
export let feed: Feed
|
||||||
export let anchor = null
|
export let anchor = null
|
||||||
@ -24,6 +26,8 @@
|
|||||||
export let showGroup = false
|
export let showGroup = false
|
||||||
export let onEvent = null
|
export let onEvent = null
|
||||||
|
|
||||||
|
const splits = [["zap", $env.PLATFORM_PUBKEY, "", "1"]]
|
||||||
|
|
||||||
const shouldHideReplies = showControls ? synced("Feed.shouldHideReplies", false) : writable(false)
|
const shouldHideReplies = showControls ? synced("Feed.shouldHideReplies", false) : writable(false)
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
@ -99,6 +103,14 @@
|
|||||||
{anchor}
|
{anchor}
|
||||||
{note} />
|
{note} />
|
||||||
</div>
|
</div>
|
||||||
|
{#if i > 20 && parseInt(hash(note.id)) % 100 === 0}
|
||||||
|
<Card class="flex items-center justify-between">
|
||||||
|
<p class="text-xl">Enjoying Coracle?</p>
|
||||||
|
<Anchor modal button accent href={router.at("zap").qp({splits}).toString()}>
|
||||||
|
Zap the developer
|
||||||
|
</Anchor>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
|
|
||||||
|
@ -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}
|
||||||
@ -89,8 +95,6 @@
|
|||||||
let search = getSearch(feed.definition)
|
let search = getSearch(feed.definition)
|
||||||
|
|
||||||
$: subFeeds = getFeedArgs(feed.definition as any)
|
$: subFeeds = getFeedArgs(feed.definition as any)
|
||||||
|
|
||||||
$: console.log(feed)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-grow items-center justify-end gap-2">
|
<div class="flex flex-grow items-center justify-end gap-2">
|
||||||
@ -132,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,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}
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
mention,
|
mention,
|
||||||
tracker,
|
tracker,
|
||||||
hints,
|
hints,
|
||||||
|
mentionEvent,
|
||||||
repository,
|
repository,
|
||||||
unmuteNote,
|
unmuteNote,
|
||||||
muteNote,
|
muteNote,
|
||||||
@ -130,7 +131,7 @@
|
|||||||
|
|
||||||
const crossPost = async (address = null) => {
|
const crossPost = async (address = null) => {
|
||||||
const content = JSON.stringify(note as SignedEvent)
|
const content = JSON.stringify(note as SignedEvent)
|
||||||
const tags = [...hints.tagEvent(note).unwrap(), mention(note.pubkey), ...getClientTags()]
|
const tags = [...mentionEvent(note), mention(note.pubkey), ...getClientTags()]
|
||||||
|
|
||||||
let template
|
let template
|
||||||
if (note.kind === 1) {
|
if (note.kind === 1) {
|
||||||
@ -148,7 +149,7 @@
|
|||||||
|
|
||||||
const startZap = () => {
|
const startZap = () => {
|
||||||
const zapTags = tags.whereKey("zap")
|
const zapTags = tags.whereKey("zap")
|
||||||
const defaultSplit = hints.tagPubkey(note.pubkey).setKey("zap").append("1").valueOf()
|
const defaultSplit = ["zap", ...mention(note.pubkey).slice(1), "1"]
|
||||||
const splits = zapTags.exists() ? zapTags.unwrap() : [defaultSplit]
|
const splits = zapTags.exists() ? zapTags.unwrap() : [defaultSplit]
|
||||||
|
|
||||||
router
|
router
|
||||||
|
@ -48,7 +48,9 @@
|
|||||||
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 isStartOrEnd = i => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
|
||||||
|
|
||||||
$: shortContent = showEntire
|
$: shortContent = showEntire
|
||||||
? fullContent
|
? fullContent
|
||||||
@ -65,7 +67,7 @@
|
|||||||
</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}
|
||||||
@ -84,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}
|
||||||
@ -93,8 +95,6 @@
|
|||||||
<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}
|
||||||
|
@ -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}
|
||||||
|
@ -12,7 +12,14 @@
|
|||||||
import RelayCardActions from "src/app/shared/RelayCardActions.svelte"
|
import RelayCardActions from "src/app/shared/RelayCardActions.svelte"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
import {displayRelayUrl, RelayMode} from "src/domain"
|
import {displayRelayUrl, RelayMode} from "src/domain"
|
||||||
import {deriveRelay, canSign, getSetting, setRelayPolicy, deriveUserRelayPolicy} from "src/engine"
|
import {
|
||||||
|
deriveRelay,
|
||||||
|
canSign,
|
||||||
|
getSetting,
|
||||||
|
setInboxPolicy,
|
||||||
|
setOutboxPolicy,
|
||||||
|
deriveUserRelayPolicy,
|
||||||
|
} from "src/engine"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let claim = null
|
export let claim = null
|
||||||
@ -27,7 +34,15 @@
|
|||||||
const relay = deriveRelay(url)
|
const relay = deriveRelay(url)
|
||||||
const policy = deriveUserRelayPolicy(url)
|
const policy = deriveUserRelayPolicy(url)
|
||||||
|
|
||||||
const policySetter = mode => () => setRelayPolicy({...$policy, [mode]: !$policy[mode]})
|
const policySetter = mode => () => {
|
||||||
|
const newPolicy = {...$policy, [mode]: !$policy[mode]}
|
||||||
|
|
||||||
|
if (mode === RelayMode.Inbox) {
|
||||||
|
setInboxPolicy(newPolicy)
|
||||||
|
} else {
|
||||||
|
setOutboxPolicy(newPolicy)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -95,18 +110,47 @@
|
|||||||
{#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" />
|
||||||
<div>
|
<div>
|
||||||
<Chip
|
<Popover triggerType="mouseenter" class="inline-block">
|
||||||
pad
|
<div slot="trigger">
|
||||||
class={cx("cursor-pointer transition-opacity", {"opacity-50": !$policy.read})}
|
<Chip
|
||||||
on:click={policySetter(RelayMode.Read)}>
|
pad
|
||||||
<i class="fa fa-book-open text-neutral-300" /> Read
|
class={cx("cursor-pointer transition-opacity", {"opacity-50": !$policy.read})}
|
||||||
</Chip>
|
on:click={policySetter(RelayMode.Read)}>
|
||||||
<Chip
|
<i class="fa fa-book-open text-neutral-300" /> Read
|
||||||
pad
|
</Chip>
|
||||||
class={cx("cursor-pointer transition-opacity", {"opacity-50": !$policy.write})}
|
</div>
|
||||||
on:click={policySetter(RelayMode.Write)}>
|
<div slot="tooltip">
|
||||||
<i class="fa fa-feather text-neutral-300" /> Write
|
Notes intended for you will {$policy.read ? "" : "not"} be delivered to this relay.
|
||||||
</Chip>
|
</div>
|
||||||
|
</Popover>
|
||||||
|
<Popover triggerType="mouseenter" class="inline-block">
|
||||||
|
<div slot="trigger">
|
||||||
|
<Chip
|
||||||
|
pad
|
||||||
|
class={cx("cursor-pointer transition-opacity", {"opacity-50": !$policy.write})}
|
||||||
|
on:click={policySetter(RelayMode.Write)}>
|
||||||
|
<i class="fa fa-feather text-neutral-300" /> Write
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
<div slot="tooltip">
|
||||||
|
Notes you publish will {$policy.write ? "" : "not"} be sent to this relay.
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
{#if $canSign}
|
||||||
|
<Popover triggerType="mouseenter" class="inline-block">
|
||||||
|
<div slot="trigger">
|
||||||
|
<Chip
|
||||||
|
pad
|
||||||
|
class={cx("cursor-pointer transition-opacity", {"opacity-50": !$policy.inbox})}
|
||||||
|
on:click={policySetter(RelayMode.Inbox)}>
|
||||||
|
<i class="fa fa-inbox text-neutral-300" /> Inbox
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
<div slot="tooltip">
|
||||||
|
Encrypted messages will {$policy.inbox ? "" : "not"} be delivered to this relay.
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,9 +5,7 @@
|
|||||||
import {DIRECT_MESSAGE} from "@welshman/util"
|
import {DIRECT_MESSAGE} from "@welshman/util"
|
||||||
import {formatTimestamp} from "src/util/misc"
|
import {formatTimestamp} from "src/util/misc"
|
||||||
import Channel from "src/partials/Channel.svelte"
|
import Channel from "src/partials/Channel.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
|
||||||
import Popover from "src/partials/Popover.svelte"
|
import Popover from "src/partials/Popover.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import PersonCircles from "src/app/shared/PersonCircles.svelte"
|
import PersonCircles from "src/app/shared/PersonCircles.svelte"
|
||||||
import PersonAbout from "src/app/shared/PersonAbout.svelte"
|
import PersonAbout from "src/app/shared/PersonAbout.svelte"
|
||||||
@ -23,7 +21,6 @@
|
|||||||
markChannelRead,
|
markChannelRead,
|
||||||
getChannelIdFromEvent,
|
getChannelIdFromEvent,
|
||||||
listenForMessages,
|
listenForMessages,
|
||||||
sortEventsDesc,
|
|
||||||
ensureMessagePlaintext,
|
ensureMessagePlaintext,
|
||||||
} from "src/engine"
|
} from "src/engine"
|
||||||
|
|
||||||
@ -46,35 +43,9 @@
|
|||||||
return sendLegacyMessage(channelId, content)
|
return sendLegacyMessage(channelId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [message] = sortEventsDesc($messages || [])
|
sendMessage(channelId, content)
|
||||||
|
|
||||||
if (!message || message?.kind === 4) {
|
|
||||||
confirmMessage = content
|
|
||||||
} else {
|
|
||||||
sendMessage(channelId, content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmNip04 = () => {
|
|
||||||
sendLegacyMessage(channelId, confirmMessage)
|
|
||||||
confirmMessage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmNip44 = () => {
|
|
||||||
sendMessage(channelId, confirmMessage)
|
|
||||||
confirmMessage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortMessage = () => {
|
|
||||||
if (confirmMessage) {
|
|
||||||
ctrl.setMessage(confirmMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmMessage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
let confirmMessage, ctrl
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
markChannelRead(channelId)
|
markChannelRead(channelId)
|
||||||
|
|
||||||
@ -88,7 +59,7 @@
|
|||||||
document.title = `Direct Messages`
|
document.title = `Direct Messages`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Channel {channelId} {pubkeys} bind:this={ctrl} messages={$messages} sendMessage={send} {initialMessage}>
|
<Channel {pubkeys} messages={$messages} sendMessage={send} {initialMessage}>
|
||||||
<div slot="header" class="flex h-16 items-start gap-4 overflow-hidden p-1 px-4">
|
<div slot="header" class="flex h-16 items-start gap-4 overflow-hidden p-1 px-4">
|
||||||
<div class="flex items-center gap-4 pt-1">
|
<div class="flex items-center gap-4 pt-1">
|
||||||
<Anchor class="fa fa-arrow-left cursor-pointer text-2xl" href="/channels" />
|
<Anchor class="fa fa-arrow-left cursor-pointer text-2xl" href="/channels" />
|
||||||
@ -134,20 +105,22 @@
|
|||||||
class:text-neutral-100={message.pubkey !== $session.pubkey}>
|
class:text-neutral-100={message.pubkey !== $session.pubkey}>
|
||||||
{formatTimestamp(message.created_at)}
|
{formatTimestamp(message.created_at)}
|
||||||
{#if message.kind === 4}
|
{#if message.kind === 4}
|
||||||
<Popover>
|
<Popover triggerType="mouseenter">
|
||||||
<i slot="trigger" class="fa fa-unlock cursor-pointer text-neutral-200" />
|
<i slot="trigger" class="fa fa-unlock cursor-pointer text-neutral-400" />
|
||||||
<p slot="tooltip">
|
<p slot="tooltip">
|
||||||
This message was sent using nostr's legacy DMs, which have a number of shortcomings.
|
This message was sent using nostr's legacy DMs, which have a number of shortcomings.
|
||||||
Read more <Anchor underline modal href="/help/nip-44-dms">here</Anchor>.
|
Read more <Anchor underline modal href="/help/nip-44-dms">here</Anchor>.
|
||||||
</p>
|
</p>
|
||||||
</Popover>
|
</Popover>
|
||||||
{:else}
|
{:else}
|
||||||
<Popover>
|
<Popover triggerType="mouseenter">
|
||||||
<i slot="trigger" class="fa fa-lock cursor-pointer text-neutral-200" />
|
<i slot="trigger" class="fa fa-lock cursor-pointer text-neutral-400" />
|
||||||
<div slot="tooltip" class="flex flex-col gap-2">
|
<div slot="tooltip" class="flex flex-col gap-2">
|
||||||
<p>
|
<p>
|
||||||
This message was sent using nostr's new group chat specification, which solves several
|
This message was sent using nostr's new group chat specification, which solves several
|
||||||
problems with legacy DMs. Read more <Anchor underline modal href="/help/nip-44-dms">here</Anchor>.
|
problems with legacy DMs. Read more <Anchor underline modal href="/help/nip-44-dms"
|
||||||
|
>here</Anchor
|
||||||
|
>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Note that these messages are not yet universally supported. Make sure the person
|
Note that these messages are not yet universally supported. Make sure the person
|
||||||
@ -159,29 +132,3 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</Channel>
|
</Channel>
|
||||||
|
|
||||||
{#if confirmMessage}
|
|
||||||
<Modal onEscape={abortMessage}>
|
|
||||||
<Content size="lg">
|
|
||||||
<p class="flex items-center gap-4 text-xl">
|
|
||||||
<i class="fa fa-info-circle" /> Auto-upgrade notice
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
This conversation has not yet been upgraded to use <Anchor
|
|
||||||
underline
|
|
||||||
modal
|
|
||||||
href="/help/nip-44-dms">new-style DMs</Anchor
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You should make sure @{displayProfileByPubkey(pubkeys[0])} is using a compatible nostr client,
|
|
||||||
or you can choose to send an old-style message instead.
|
|
||||||
</p>
|
|
||||||
<p>How would you like to send this message?</p>
|
|
||||||
<div class="flex flex-col gap-2 py-4 sm:flex-row">
|
|
||||||
<Anchor button on:click={confirmNip04}>Send using Legacy DMs</Anchor>
|
|
||||||
<Anchor button accent on:click={confirmNip44}>Send using NIP 44</Anchor>
|
|
||||||
</div>
|
|
||||||
</Content>
|
|
||||||
</Modal>
|
|
||||||
{/if}
|
|
||||||
|
@ -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}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
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 {displayGroupMeta} from "src/domain"
|
import {displayGroupMeta} from "src/domain"
|
||||||
import {deriveGroupMeta, createAndPublish, hints, deriveAdminKeyForGroup} from "src/engine"
|
import {deriveGroupMeta, deleteGroupMeta, deriveAdminKeyForGroup} from "src/engine"
|
||||||
import {router} from "src/app/util/router"
|
import {router} from "src/app/util/router"
|
||||||
|
|
||||||
export let address
|
export let address
|
||||||
@ -14,14 +14,7 @@
|
|||||||
const abort = () => router.pop()
|
const abort = () => router.pop()
|
||||||
|
|
||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
createAndPublish({
|
deleteGroupMeta(address)
|
||||||
kind: 5,
|
|
||||||
tags: [["a", address]],
|
|
||||||
relays: hints.WithinContext(address).getUrls(),
|
|
||||||
sk: $adminKey.privkey,
|
|
||||||
forcePlatform: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
showInfo("Group deleted!")
|
showInfo("Group deleted!")
|
||||||
router.pop()
|
router.pop()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
createAndPublish,
|
createAndPublish,
|
||||||
updateSingleton,
|
updateSingleton,
|
||||||
publishProfile,
|
publishProfile,
|
||||||
setRelayPolicies,
|
setOutboxPolicies,
|
||||||
tagsFromContent,
|
tagsFromContent,
|
||||||
requestRelayAccess,
|
requestRelayAccess,
|
||||||
loginWithPrivateKey,
|
loginWithPrivateKey,
|
||||||
@ -76,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do this first so we know where to publish everything else
|
// Do this first so we know where to publish everything else
|
||||||
setRelayPolicies(() => relays)
|
setOutboxPolicies(() => relays)
|
||||||
|
|
||||||
// Re-save preferences now that we have a key and relays
|
// Re-save preferences now that we have a key and relays
|
||||||
publishProfile(profile)
|
publishProfile(profile)
|
||||||
|
@ -17,13 +17,13 @@
|
|||||||
export let eid = null
|
export let eid = null
|
||||||
export let anonymous = false
|
export let anonymous = false
|
||||||
export let callback = null
|
export let callback = null
|
||||||
|
export let amount = getSetting("default_zap")
|
||||||
|
|
||||||
let zaps = []
|
let zaps = []
|
||||||
let message = ""
|
let message = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
let totalAmount = getSetting("default_zap")
|
|
||||||
|
|
||||||
const updateZaps = (message, totalAmount) => {
|
const updateZaps = (message, amount) => {
|
||||||
let totalWeight = 0
|
let totalWeight = 0
|
||||||
|
|
||||||
zaps = doPipe(splits, [
|
zaps = doPipe(splits, [
|
||||||
@ -39,7 +39,7 @@
|
|||||||
map(([pubkey, relay, weight]: string[]) => ({
|
map(([pubkey, relay, weight]: string[]) => ({
|
||||||
relay,
|
relay,
|
||||||
pubkey,
|
pubkey,
|
||||||
amount: Math.round(totalAmount * (parseFloat(weight) / totalWeight)),
|
amount: Math.round(amount * (parseFloat(weight) / totalWeight)),
|
||||||
status: "pending",
|
status: "pending",
|
||||||
})),
|
})),
|
||||||
sortBy((split: any) => -split.amount),
|
sortBy((split: any) => -split.amount),
|
||||||
@ -137,7 +137,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Watch inputs and update zaps
|
// Watch inputs and update zaps
|
||||||
$: updateZaps(message, totalAmount)
|
$: updateZaps(message, amount)
|
||||||
|
|
||||||
// Initialize bitcoin connect
|
// Initialize bitcoin connect
|
||||||
init({appName: import.meta.env.VITE_APP_NAME})
|
init({appName: import.meta.env.VITE_APP_NAME})
|
||||||
@ -146,7 +146,7 @@
|
|||||||
{#if zaps.length > 0}
|
{#if zaps.length > 0}
|
||||||
<h1 class="staatliches text-2xl">Send a zap</h1>
|
<h1 class="staatliches text-2xl">Send a zap</h1>
|
||||||
<Textarea bind:value={message} placeholder="Send a message with your zap (optional)" />
|
<Textarea bind:value={message} placeholder="Send a message with your zap (optional)" />
|
||||||
<Input bind:value={totalAmount}>
|
<Input bind:value={amount}>
|
||||||
<i slot="before" class="fa fa-bolt" />
|
<i slot="before" class="fa fa-bolt" />
|
||||||
<span slot="after" class="-mt-1">sats</span>
|
<span slot="after" class="-mt-1">sats</span>
|
||||||
</Input>
|
</Input>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {last} from "@welshman/lib"
|
import {last} from "@welshman/lib"
|
||||||
import {LOCAL_RELAY_URL, normalizeRelayUrl as _normalizeRelayUrl} from "@welshman/util"
|
import {LOCAL_RELAY_URL, normalizeRelayUrl as _normalizeRelayUrl} from "@welshman/util"
|
||||||
import {getRelayTags} from "src/util/nostr"
|
|
||||||
|
|
||||||
// Utils related to bare urls
|
// Utils related to bare urls
|
||||||
|
|
||||||
@ -42,25 +41,23 @@ export const filterRelaysByNip = (nip: number, relays) =>
|
|||||||
export enum RelayMode {
|
export enum RelayMode {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Write = "write",
|
Write = "write",
|
||||||
|
Inbox = "inbox",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelayPolicy = {
|
export type RelayPolicy = {
|
||||||
url: string
|
url: string
|
||||||
read: boolean
|
read: boolean
|
||||||
write: boolean
|
write: boolean
|
||||||
|
inbox: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeRelayPolicy = (
|
export const makeRelayPolicy = ({
|
||||||
relayPolicy: Partial<RelayPolicy> & {url: string},
|
url,
|
||||||
): RelayPolicy => ({
|
...relayPolicy
|
||||||
|
}: Partial<RelayPolicy> & {url: string}): RelayPolicy => ({
|
||||||
|
url: normalizeRelayUrl(url),
|
||||||
read: false,
|
read: false,
|
||||||
write: false,
|
write: false,
|
||||||
|
inbox: false,
|
||||||
...relayPolicy,
|
...relayPolicy,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeRelayPoliciesFromTags = (tags: string[][]) =>
|
|
||||||
getRelayTags(tags).map(([_, url, mode]) => ({
|
|
||||||
url: normalizeRelayUrl(url),
|
|
||||||
write: !mode || mode === RelayMode.Write,
|
|
||||||
read: !mode || mode === RelayMode.Read,
|
|
||||||
}))
|
|
||||||
|
@ -10,11 +10,13 @@ import {
|
|||||||
Address,
|
Address,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
|
FEEDS,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
MUTES,
|
MUTES,
|
||||||
WRAP_NIP04,
|
WRAP_NIP04,
|
||||||
|
INBOX_RELAYS,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {Fetch, chunk, createMapOf, randomId, seconds, sleep, tryFunc} from "hurdak"
|
import {Fetch, chunk, createMapOf, randomId, seconds, sleep, tryFunc} from "hurdak"
|
||||||
import {
|
import {
|
||||||
@ -39,6 +41,7 @@ import {
|
|||||||
editSingleton,
|
editSingleton,
|
||||||
createSingleton,
|
createSingleton,
|
||||||
readSingleton,
|
readSingleton,
|
||||||
|
makeRelayPolicy,
|
||||||
} from "src/domain"
|
} from "src/domain"
|
||||||
import type {RelayPolicy} from "src/domain"
|
import type {RelayPolicy} from "src/domain"
|
||||||
import type {Session, NostrConnectHandler} from "src/engine/model"
|
import type {Session, NostrConnectHandler} from "src/engine/model"
|
||||||
@ -80,6 +83,8 @@ import {
|
|||||||
zappers,
|
zappers,
|
||||||
getPlaintext,
|
getPlaintext,
|
||||||
anonymous,
|
anonymous,
|
||||||
|
mentionGroup,
|
||||||
|
userRelayPolicies,
|
||||||
} from "src/engine/state"
|
} from "src/engine/state"
|
||||||
import {loadHandle, loadZapper} from "src/engine/requests"
|
import {loadHandle, loadZapper} from "src/engine/requests"
|
||||||
|
|
||||||
@ -311,9 +316,9 @@ export const wrapWithFallback = async (template, {author = null, wrap}) => {
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
const addATags = (template, addresses) => ({
|
const addGroupATags = (template, addresses) => ({
|
||||||
...template,
|
...template,
|
||||||
tags: [...template.tags, ...addresses.map(a => ["a", a])],
|
tags: [...template.tags, ...addresses.map(mentionGroup)],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Utils for publishing group-related messages
|
// Utils for publishing group-related messages
|
||||||
@ -376,7 +381,7 @@ export const publishToGroupsPublicly = async (addresses, template, {anonymous =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await sign(addATags(template, addresses), {anonymous})
|
const event = await sign(addGroupATags(template, addresses), {anonymous})
|
||||||
const relays = hints.PublishEvent(event).getUrls()
|
const relays = hints.PublishEvent(event).getUrls()
|
||||||
|
|
||||||
return publish({event, relays, forcePlatform: false})
|
return publish({event, relays, forcePlatform: false})
|
||||||
@ -387,7 +392,7 @@ export const publishToGroupsPrivately = async (addresses, template, {anonymous =
|
|||||||
const pubs = []
|
const pubs = []
|
||||||
for (const address of addresses) {
|
for (const address of addresses) {
|
||||||
const relays = hints.WithinContext(address).getUrls()
|
const relays = hints.WithinContext(address).getUrls()
|
||||||
const thisTemplate = addATags(template, [address])
|
const thisTemplate = addGroupATags(template, [address])
|
||||||
const sharedKey = deriveSharedKeyForGroup(address).get()
|
const sharedKey = deriveSharedKeyForGroup(address).get()
|
||||||
|
|
||||||
if (!address.startsWith("35834:")) {
|
if (!address.startsWith("35834:")) {
|
||||||
@ -489,7 +494,7 @@ export const publishAdminKeyShares = async (address, pubkeys) => {
|
|||||||
const {privkey} = deriveAdminKeyForGroup(address).get()
|
const {privkey} = deriveAdminKeyForGroup(address).get()
|
||||||
const template = createEvent(24, {
|
const template = createEvent(24, {
|
||||||
tags: [
|
tags: [
|
||||||
["a", address],
|
mentionGroup(address),
|
||||||
["role", "admin"],
|
["role", "admin"],
|
||||||
["privkey", privkey],
|
["privkey", privkey],
|
||||||
...getClientTags(),
|
...getClientTags(),
|
||||||
@ -506,7 +511,7 @@ export const publishGroupInvites = async (address, pubkeys, gracePeriod = 0) =>
|
|||||||
const {privkey} = deriveSharedKeyForGroup(address).get()
|
const {privkey} = deriveSharedKeyForGroup(address).get()
|
||||||
const template = createEvent(24, {
|
const template = createEvent(24, {
|
||||||
tags: [
|
tags: [
|
||||||
["a", address],
|
mentionGroup(address),
|
||||||
["role", "member"],
|
["role", "member"],
|
||||||
["privkey", privkey],
|
["privkey", privkey],
|
||||||
["grace_period", String(gracePeriod)],
|
["grace_period", String(gracePeriod)],
|
||||||
@ -523,13 +528,13 @@ export const publishGroupEvictions = async (address, pubkeys) =>
|
|||||||
address,
|
address,
|
||||||
pubkeys,
|
pubkeys,
|
||||||
createEvent(24, {
|
createEvent(24, {
|
||||||
tags: [["a", address], ...getClientTags()],
|
tags: [mentionGroup(address), ...getClientTags()],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const publishGroupMembers = async (address, op, pubkeys) => {
|
export const publishGroupMembers = async (address, op, pubkeys) => {
|
||||||
const template = createEvent(27, {
|
const template = createEvent(27, {
|
||||||
tags: [["op", op], ["a", address], ...getClientTags(), ...pubkeys.map(mention)],
|
tags: [["op", op], mentionGroup(address), ...getClientTags(), ...pubkeys.map(mention)],
|
||||||
})
|
})
|
||||||
|
|
||||||
return publishAsGroupAdminPrivately(address, template)
|
return publishAsGroupAdminPrivately(address, template)
|
||||||
@ -576,7 +581,7 @@ export const publishGroupMeta = (address, identifier, meta, listPublicly) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const deleteGroupMeta = address =>
|
export const deleteGroupMeta = address =>
|
||||||
publishAsGroupAdminPublicly(address, createEvent(5, {tags: [["a", address]]}))
|
publishAsGroupAdminPublicly(address, createEvent(5, {tags: [mentionGroup(address)]}))
|
||||||
|
|
||||||
// Member functions
|
// Member functions
|
||||||
|
|
||||||
@ -599,7 +604,7 @@ export const publishGroupEntryRequest = (address, claim = null) => {
|
|||||||
} else {
|
} else {
|
||||||
setGroupStatus(pubkey.get(), address, now(), {access: GroupAccess.Requested})
|
setGroupStatus(pubkey.get(), address, now(), {access: GroupAccess.Requested})
|
||||||
|
|
||||||
const tags = [...getClientTags(), ["a", address]]
|
const tags = [...getClientTags(), mentionGroup(address)]
|
||||||
|
|
||||||
if (claim) {
|
if (claim) {
|
||||||
tags.push(["claim", claim])
|
tags.push(["claim", claim])
|
||||||
@ -623,7 +628,7 @@ export const publishGroupExitRequest = address => {
|
|||||||
address,
|
address,
|
||||||
createEvent(26, {
|
createEvent(26, {
|
||||||
content: `${displayProfileByPubkey(pubkey.get())} is leaving the group`,
|
content: `${displayProfileByPubkey(pubkey.get())} is leaving the group`,
|
||||||
tags: [...getClientTags(), ["a", address]],
|
tags: [...getClientTags(), mentionGroup(address)],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -632,7 +637,7 @@ export const publishGroupExitRequest = address => {
|
|||||||
export const publishCommunitiesList = addresses =>
|
export const publishCommunitiesList = addresses =>
|
||||||
createAndPublish({
|
createAndPublish({
|
||||||
kind: 10004,
|
kind: 10004,
|
||||||
tags: [...addresses.map(a => ["a", a]), ...getClientTags()],
|
tags: [...addresses.map(mentionGroup), ...getClientTags()],
|
||||||
relays: hints.WriteRelays().getUrls(),
|
relays: hints.WriteRelays().getUrls(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -685,7 +690,9 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
|
|||||||
|
|
||||||
// If we don't have a recent version loaded, re-fetch to avoid dropping updates
|
// If we don't have a recent version loaded, re-fetch to avoid dropping updates
|
||||||
if ((event?.created_at || 0) < now() - seconds(5, "minute")) {
|
if ((event?.created_at || 0) < now() - seconds(5, "minute")) {
|
||||||
|
console.log("loading")
|
||||||
const loadedEvent = await loadOne({relays: hints.User().getUrls(), filters})
|
const loadedEvent = await loadOne({relays: hints.User().getUrls(), filters})
|
||||||
|
console.log("loaded", loadedEvent)
|
||||||
|
|
||||||
if ((loadedEvent?.created_at || 0) > (event?.created_at || 0)) {
|
if ((loadedEvent?.created_at || 0) > (event?.created_at || 0)) {
|
||||||
event = loadedEvent
|
event = loadedEvent
|
||||||
@ -710,8 +717,12 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
|
|||||||
encryptable = createSingleton({...singleton, publicTags})
|
encryptable = createSingleton({...singleton, publicTags})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(1)
|
||||||
|
|
||||||
const template = await encryptable.reconcile(encrypt)
|
const template = await encryptable.reconcile(encrypt)
|
||||||
|
|
||||||
|
console.log(2, template)
|
||||||
|
|
||||||
await createAndPublish({...template, content, relays})
|
await createAndPublish({...template, content, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -743,6 +754,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) =>
|
||||||
@ -754,7 +771,7 @@ export const requestRelayAccess = async (url: string, claim: string, sk?: string
|
|||||||
sk,
|
sk,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setRelayPolicies = async (modifyTags: ModifyTags) => {
|
export const setOutboxPolicies = async (modifyTags: ModifyTags) => {
|
||||||
if (canSign.get()) {
|
if (canSign.get()) {
|
||||||
updateSingleton(RELAYS, modifyTags)
|
updateSingleton(RELAYS, modifyTags)
|
||||||
} else {
|
} else {
|
||||||
@ -762,8 +779,26 @@ export const setRelayPolicies = async (modifyTags: ModifyTags) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setRelayPolicy = ({url, read, write}: RelayPolicy) =>
|
export const setInboxPolicies = async (modifyTags: ModifyTags) =>
|
||||||
setRelayPolicies($tags => {
|
updateSingleton(INBOX_RELAYS, modifyTags)
|
||||||
|
|
||||||
|
export const setInboxPolicy = ({url, inbox}: RelayPolicy) => {
|
||||||
|
// Only update inbox policies if they already exist or we're adding them
|
||||||
|
if (inbox || get(userRelayPolicies).find(p => p.url === url && p.inbox)) {
|
||||||
|
setInboxPolicies($tags => {
|
||||||
|
$tags = $tags.filter(t => t[1] !== url)
|
||||||
|
|
||||||
|
if (inbox) {
|
||||||
|
$tags.push(["relay", url])
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tags
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setOutboxPolicy = ({url, read, write}: RelayPolicy) =>
|
||||||
|
setOutboxPolicies($tags => {
|
||||||
$tags = $tags.filter(t => t[1] !== url)
|
$tags = $tags.filter(t => t[1] !== url)
|
||||||
|
|
||||||
if (read && write) {
|
if (read && write) {
|
||||||
@ -778,7 +813,10 @@ export const setRelayPolicy = ({url, read, write}: RelayPolicy) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const leaveRelay = async (url: string) => {
|
export const leaveRelay = async (url: string) => {
|
||||||
await setRelayPolicy({url, read: false, write: false})
|
await Promise.all([
|
||||||
|
setInboxPolicy(makeRelayPolicy({url})),
|
||||||
|
setOutboxPolicy(makeRelayPolicy({url})),
|
||||||
|
])
|
||||||
|
|
||||||
// Make sure the new relay selections get to the old relay
|
// Make sure the new relay selections get to the old relay
|
||||||
if (pubkey.get()) {
|
if (pubkey.get()) {
|
||||||
@ -793,7 +831,7 @@ export const joinRelay = async (url: string, claim?: string) => {
|
|||||||
await requestRelayAccess(url, claim)
|
await requestRelayAccess(url, claim)
|
||||||
}
|
}
|
||||||
|
|
||||||
await setRelayPolicy({url, read: true, write: true})
|
await setOutboxPolicy(makeRelayPolicy({url, read: true, write: true}))
|
||||||
|
|
||||||
// Re-publish user meta to the new relay
|
// Re-publish user meta to the new relay
|
||||||
if (pubkey.get()) {
|
if (pubkey.get()) {
|
||||||
@ -889,7 +927,7 @@ export const sendMessage = async (channelId: string, content: string) => {
|
|||||||
|
|
||||||
publish({
|
publish({
|
||||||
event: rumor.wrap,
|
event: rumor.wrap,
|
||||||
relays: hints.merge(recipients.map(hints.PublishMessage)).getUrls(),
|
relays: hints.PublishMessage(recipient).getUrls(),
|
||||||
forcePlatform: false,
|
forcePlatform: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,12 @@ import type {TrustedEvent} from "@welshman/util"
|
|||||||
import {
|
import {
|
||||||
RELAYS,
|
RELAYS,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
|
INBOX_RELAYS,
|
||||||
HANDLER_INFORMATION,
|
HANDLER_INFORMATION,
|
||||||
NAMED_BOOKMARKS,
|
NAMED_BOOKMARKS,
|
||||||
COMMUNITIES,
|
COMMUNITIES,
|
||||||
FEED,
|
FEED,
|
||||||
|
FEEDS,
|
||||||
MUTES,
|
MUTES,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
APP_DATA,
|
APP_DATA,
|
||||||
@ -50,12 +52,12 @@ const getFiltersForKey = (key: string, authors: string[]) => {
|
|||||||
case "pubkey/feeds":
|
case "pubkey/feeds":
|
||||||
return [{authors, kinds: [NAMED_BOOKMARKS, FEED]}]
|
return [{authors, kinds: [NAMED_BOOKMARKS, FEED]}]
|
||||||
case "pubkey/relays":
|
case "pubkey/relays":
|
||||||
return [{authors, kinds: [RELAYS]}]
|
return [{authors, kinds: [RELAYS, INBOX_RELAYS]}]
|
||||||
case "pubkey/profile":
|
case "pubkey/profile":
|
||||||
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)},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,10 @@ import {
|
|||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
WRAP,
|
WRAP,
|
||||||
|
FEEDS,
|
||||||
COMMUNITY,
|
COMMUNITY,
|
||||||
GROUP,
|
GROUP,
|
||||||
|
INBOX_RELAYS,
|
||||||
WRAP_NIP04,
|
WRAP_NIP04,
|
||||||
COMMUNITIES,
|
COMMUNITIES,
|
||||||
READ_RECEIPT,
|
READ_RECEIPT,
|
||||||
@ -72,6 +74,7 @@ import {
|
|||||||
LOCAL_RELAY_URL,
|
LOCAL_RELAY_URL,
|
||||||
getFilterResultCardinality,
|
getFilterResultCardinality,
|
||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
|
isReplaceable,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Filter, RouterScenario, TrustedEvent, SignedEvent} from "@welshman/util"
|
import type {Filter, RouterScenario, TrustedEvent, SignedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@ -97,6 +100,8 @@ import {
|
|||||||
repostKinds,
|
repostKinds,
|
||||||
noteKinds,
|
noteKinds,
|
||||||
reactionKinds,
|
reactionKinds,
|
||||||
|
getRelayTags,
|
||||||
|
getRelayTagValues,
|
||||||
} from "src/util/nostr"
|
} from "src/util/nostr"
|
||||||
import logger from "src/util/logger"
|
import logger from "src/util/logger"
|
||||||
import type {
|
import type {
|
||||||
@ -110,10 +115,10 @@ import type {
|
|||||||
Handle,
|
Handle,
|
||||||
} from "src/domain"
|
} from "src/domain"
|
||||||
import {
|
import {
|
||||||
|
RelayMode,
|
||||||
EDITABLE_LIST_KINDS,
|
EDITABLE_LIST_KINDS,
|
||||||
getSingletonValues,
|
getSingletonValues,
|
||||||
makeSingleton,
|
makeSingleton,
|
||||||
makeRelayPoliciesFromTags,
|
|
||||||
ListSearch,
|
ListSearch,
|
||||||
FeedSearch,
|
FeedSearch,
|
||||||
profileHasName,
|
profileHasName,
|
||||||
@ -963,8 +968,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)) ||
|
||||||
@ -972,7 +976,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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1105,6 +1109,19 @@ export const relayLists = withGetter(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const inboxRelayLists = withGetter(
|
||||||
|
deriveEventsMapped<PublishedSingleton>({
|
||||||
|
filters: [{kinds: [INBOX_RELAYS]}],
|
||||||
|
itemToEvent: prop("event"),
|
||||||
|
eventToItem: event =>
|
||||||
|
readSingleton(
|
||||||
|
asDecryptedEvent(event, {
|
||||||
|
content: getPlaintext(event),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const legacyRelayLists = withGetter(
|
export const legacyRelayLists = withGetter(
|
||||||
deriveEventsMapped<{event: TrustedEvent; policy: RelayPolicy[]}>({
|
deriveEventsMapped<{event: TrustedEvent; policy: RelayPolicy[]}>({
|
||||||
filters: [{kinds: [FOLLOWS]}],
|
filters: [{kinds: [FOLLOWS]}],
|
||||||
@ -1115,11 +1132,7 @@ export const legacyRelayLists = withGetter(
|
|||||||
JSON.parse(event.content) as Record<string, {write: boolean; read: boolean}>,
|
JSON.parse(event.content) as Record<string, {write: boolean; read: boolean}>,
|
||||||
)
|
)
|
||||||
.filter(([url]) => isShareableRelayUrl(url))
|
.filter(([url]) => isShareableRelayUrl(url))
|
||||||
.map(([url, {write = true, read = true}]) => ({
|
.map(([url, {write = true, read = true}]) => makeRelayPolicy({url, read, write}))
|
||||||
url: normalizeRelayUrl(url),
|
|
||||||
write,
|
|
||||||
read,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return {event, policy}
|
return {event, policy}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -1130,21 +1143,54 @@ export const legacyRelayLists = withGetter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const relayPoliciesByPubkey = withGetter(
|
export const relayPoliciesByPubkey = withGetter(
|
||||||
derived([relayLists, legacyRelayLists], ([$relayLists, $legacyRelayLists]) => {
|
derived(
|
||||||
const policies = new Map<string, RelayPolicy[]>()
|
[relayLists, inboxRelayLists, legacyRelayLists],
|
||||||
|
([$relayLists, $inboxRelayLists, $legacyRelayLists]) => {
|
||||||
|
const policiesByUrlByPubkey = new Map<string, Map<string, RelayPolicy>>()
|
||||||
|
|
||||||
for (const {event, publicTags} of $relayLists) {
|
for (const {event, publicTags} of $relayLists) {
|
||||||
policies.set(event.pubkey, makeRelayPoliciesFromTags(publicTags))
|
const policiesByUrl = new Map()
|
||||||
}
|
|
||||||
|
|
||||||
for (const {event, policy} of $legacyRelayLists) {
|
for (const [_, url, mode] of getRelayTags(publicTags)) {
|
||||||
if (!policies.has(event.pubkey)) {
|
const read = !mode || mode === RelayMode.Read
|
||||||
policies.set(event.pubkey, policy)
|
const write = !mode || mode === RelayMode.Write
|
||||||
|
const policy = makeRelayPolicy({url, read, write})
|
||||||
|
|
||||||
|
policiesByUrl.set(policy.url, policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
policiesByUrlByPubkey.set(event.pubkey, policiesByUrl)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return policies
|
for (const {event, publicTags} of $inboxRelayLists) {
|
||||||
}),
|
const policiesByUrl = policiesByUrlByPubkey.get(event.pubkey) || new Map()
|
||||||
|
|
||||||
|
for (const url of getRelayTagValues(publicTags)) {
|
||||||
|
const normalizedUrl = normalizeRelayUrl(url)
|
||||||
|
const defaultPolicy = makeRelayPolicy({url})
|
||||||
|
const policy = policiesByUrl.get(defaultPolicy.url)
|
||||||
|
|
||||||
|
policiesByUrl.set(normalizedUrl, {...defaultPolicy, ...policy, inbox: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
policiesByUrlByPubkey.set(event.pubkey, policiesByUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const {event, policy} of $legacyRelayLists) {
|
||||||
|
if (!policiesByUrlByPubkey.has(event.pubkey)) {
|
||||||
|
policiesByUrlByPubkey.set(event.pubkey, indexBy(prop("url"), policy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new Map<string, RelayPolicy[]>()
|
||||||
|
|
||||||
|
for (const [pubkey, policiesByUrl] of policiesByUrlByPubkey.entries()) {
|
||||||
|
result.set(pubkey, Array.from(policiesByUrl.values()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const getPubkeyRelayPolicies = (pubkey: string, mode: string = null) => {
|
export const getPubkeyRelayPolicies = (pubkey: string, mode: string = null) => {
|
||||||
@ -1155,7 +1201,14 @@ export const getPubkeyRelayPolicies = (pubkey: string, mode: string = null) => {
|
|||||||
|
|
||||||
export const userRelayPolicies = derived(
|
export const userRelayPolicies = derived(
|
||||||
[relayPoliciesByPubkey, pubkey, anonymous],
|
[relayPoliciesByPubkey, pubkey, anonymous],
|
||||||
([$m, $pk, $anon]) => $m.get($pk) || makeRelayPoliciesFromTags($anon.relays),
|
([$m, $pk, $anon]) =>
|
||||||
|
$m.get($pk) ||
|
||||||
|
getRelayTags($anon.relays).map(([_, url, mode]) => {
|
||||||
|
const read = !mode || mode === RelayMode.Read
|
||||||
|
const write = !mode || mode === RelayMode.Write
|
||||||
|
|
||||||
|
return makeRelayPolicy({url, read, write})
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveUserRelayPolicy = url =>
|
export const deriveUserRelayPolicy = url =>
|
||||||
@ -1278,15 +1331,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))
|
||||||
@ -1516,6 +1590,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
|
||||||
@ -1560,6 +1635,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)
|
||||||
}
|
}
|
||||||
@ -1598,7 +1677,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 => {
|
||||||
@ -1710,8 +1796,28 @@ Object.assign(NetworkContext, {
|
|||||||
export const uniqTags = tags =>
|
export const uniqTags = tags =>
|
||||||
uniqBy((t: string[]) => (t[0] === "param" ? t.join(":") : t.slice(0, 2).join(":")), tags)
|
uniqBy((t: string[]) => (t[0] === "param" ? t.join(":") : t.slice(0, 2).join(":")), tags)
|
||||||
|
|
||||||
export const mention = (pubkey: string, ...args: unknown[]) =>
|
export const mention = (pubkey: string, ...args: unknown[]) => [
|
||||||
hints.tagPubkey(pubkey).append(displayProfileByPubkey(pubkey)).valueOf()
|
"p",
|
||||||
|
pubkey,
|
||||||
|
hints.FromPubkeys([pubkey]).getUrl(),
|
||||||
|
displayProfileByPubkey(pubkey),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mentionGroup = (address: string, ...args: unknown[]) => [
|
||||||
|
"a",
|
||||||
|
hints.WithinContext(address).getUrl(),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mentionEvent = (event: TrustedEvent, mark = "") => {
|
||||||
|
const url = hints.Event(event).getUrl()
|
||||||
|
const tags = [["e", event.id, url, mark, event.pubkey]]
|
||||||
|
|
||||||
|
if (isReplaceable(event)) {
|
||||||
|
tags.push(["a", getAddress(event), url, mark, event.pubkey])
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
export const tagsFromContent = (content: string) => {
|
export const tagsFromContent = (content: string) => {
|
||||||
const tags = []
|
const tags = []
|
||||||
@ -1786,7 +1892,7 @@ export const getReplyTags = (parent: TrustedEvent) => {
|
|||||||
|
|
||||||
// Add a/e-tags for the parent event
|
// Add a/e-tags for the parent event
|
||||||
const mark = replies.exists() ? "reply" : "root"
|
const mark = replies.exists() ? "reply" : "root"
|
||||||
for (const t of hints.tagEvent(parent, mark).valueOf()) {
|
for (const t of mentionEvent(parent, mark)) {
|
||||||
replyTags.push(t.valueOf())
|
replyTags.push(t.valueOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1802,7 +1908,7 @@ export const getReactionTags = (parent: TrustedEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a/e-tags for the parent event
|
// Add a/e-tags for the parent event
|
||||||
for (const t of hints.tagEvent(parent, "root").valueOf()) {
|
for (const t of mentionEvent(parent, "root")) {
|
||||||
replyTags.push(t.valueOf())
|
replyTags.push(t.valueOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2007,19 +2113,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)
|
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.log('deleting', name, removedRecords.length, current.length)
|
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)
|
console.log("pruning", name, current.length)
|
||||||
set((sort ? sort(current) : current).slice(0, limit))
|
set((sort ? sort(current) : current).slice(0, limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {sleep} from "hurdak"
|
import {sleep} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {INBOX_RELAYS} from "@welshman/util"
|
||||||
import {prop, max, reverse, pluck, sortBy, last} from "ramda"
|
import {prop, max, reverse, pluck, sortBy, last} from "ramda"
|
||||||
import {fly} from "src/util/transition"
|
import {fly} from "src/util/transition"
|
||||||
import {createScroller, synced} from "src/util/misc"
|
import {createScroller} from "src/util/misc"
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
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 Toggle from "src/partials/Toggle.svelte"
|
import Toggle from "src/partials/Toggle.svelte"
|
||||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||||
import ImageInput from "src/partials/ImageInput.svelte"
|
import ImageInput from "src/partials/ImageInput.svelte"
|
||||||
import {nip44} from "src/engine"
|
import {nip44, repository} from "src/engine"
|
||||||
|
|
||||||
export let pubkeys
|
export let pubkeys
|
||||||
export let channelId
|
|
||||||
export let messages: TrustedEvent[]
|
|
||||||
export let sendMessage
|
export let sendMessage
|
||||||
export let initialMessage = ""
|
export let initialMessage = ""
|
||||||
|
export let messages: TrustedEvent[]
|
||||||
|
|
||||||
const loading = sleep(30_000)
|
const loading = sleep(30_000)
|
||||||
|
|
||||||
const useNip44 = synced(`useNip44/${channelId}`, true)
|
|
||||||
|
|
||||||
const startScroller = () => {
|
const startScroller = () => {
|
||||||
scroller?.stop()
|
scroller?.stop()
|
||||||
scroller = createScroller(loadMore, {element, reverse: true})
|
scroller = createScroller(loadMore, {element, reverse: true})
|
||||||
@ -36,6 +34,10 @@
|
|||||||
let limit = 10
|
let limit = 10
|
||||||
let showNewMessages = false
|
let showNewMessages = false
|
||||||
let groupedMessages = []
|
let groupedMessages = []
|
||||||
|
let useNip44 =
|
||||||
|
pubkeys.length > 2 ||
|
||||||
|
($nip44.isEnabled() &&
|
||||||
|
repository.query([{kinds: [INBOX_RELAYS], authors: pubkeys}]).length === pubkeys.length)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
startScroller()
|
startScroller()
|
||||||
@ -76,7 +78,7 @@
|
|||||||
if (content) {
|
if (content) {
|
||||||
textarea.value = ""
|
textarea.value = ""
|
||||||
|
|
||||||
await sendMessage(content, $useNip44)
|
await sendMessage(content, useNip44)
|
||||||
|
|
||||||
stickToBottom()
|
stickToBottom()
|
||||||
}
|
}
|
||||||
@ -159,7 +161,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if $nip44.isEnabled()}
|
{#if $nip44.isEnabled()}
|
||||||
<div class="fixed bottom-0 right-12 flex items-center justify-end gap-2 p-2">
|
<div class="fixed bottom-0 right-12 flex items-center justify-end gap-2 p-2">
|
||||||
<Toggle scale={0.7} bind:value={$useNip44} />
|
<Toggle scale={0.7} bind:value={useNip44} />
|
||||||
<small>
|
<small>
|
||||||
Send messages using
|
Send messages using
|
||||||
<Popover class="inline">
|
<Popover class="inline">
|
||||||
|
Loading…
Reference in New Issue
Block a user