Compare commits

...

7 Commits

Author SHA1 Message Date
Jon Staab
1408f782f1 Toggle nip44 messages based on 10050 signaling 2024-06-20 16:59:19 -07:00
Jon Staab
b1f2218e81 Add support for kind 10050 relay lists 2024-06-20 16:34:43 -07:00
Jon Staab
282a80cdea Add zap the developer 2024-06-20 14:10:39 -07:00
Jon Staab
e212297396 Bump welshman/net 2024-06-20 12:12:48 -07:00
Jon Staab
c9db37c92a Fix profile loading, improve feed list item updating 2024-06-20 11:08:13 -07:00
Jon Staab
148a63d95f Bump versions 2024-06-20 10:25:48 -07:00
Jon Staab
a5517f2eff Add feed favorites 2024-06-20 10:15:28 -07:00
25 changed files with 428 additions and 224 deletions

View File

@ -1,5 +1,10 @@
# Changelog
# 0.4.8
- [x] Add support for kind 10050 relay lists
- [x] Toggle nip44 messages based on 10050 signaling
# 0.4.7
- [x] Show toast when offline
@ -11,6 +16,7 @@
- [x] Add reports using tagr-bot
- [x] Open links to coracle in same tab
- [x] Add global feeds
- [x] Add feed favorites
# 0.4.6

BIN
package-lock.json generated

Binary file not shown.

View File

@ -57,9 +57,9 @@
"@scure/base": "^1.1.6",
"@welshman/content": "^0.0.5",
"@welshman/feeds": "^0.0.12",
"@welshman/lib": "^0.0.9",
"@welshman/net": "^0.0.13",
"@welshman/util": "^0.0.14",
"@welshman/lib": "^0.0.10",
"@welshman/net": "^0.0.14",
"@welshman/util": "^0.0.15",
"bowser": "^2.11.0",
"classnames": "^2.5.1",
"compressorjs": "^1.2.1",

View File

@ -236,3 +236,9 @@ body,
.react-switch-bg {
border: 1px solid var(--neutral-600);
}
/* note content */
.note-content a {
text-decoration: underline;
}

View File

@ -319,6 +319,7 @@
required: ["splits"],
serializers: {
eid: asNote,
amount: asJson("amount"),
splits: asJson("splits"),
anonymous: asJson("anonymous"),
},

View File

@ -1,15 +1,17 @@
<script lang="ts">
import {onMount} from "svelte"
import {writable} from "@welshman/lib"
import {writable, hash} from "@welshman/lib"
import {createScroller, synced} from "src/util/misc"
import {fly, fade} from "src/util/transition"
import Anchor from "src/partials/Anchor.svelte"
import Card from "src/partials/Card.svelte"
import Spinner from "src/partials/Spinner.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import Note from "src/app/shared/Note.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 {env} from "src/engine"
export let feed: Feed
export let anchor = null
@ -24,6 +26,8 @@
export let showGroup = false
export let onEvent = null
const splits = [["zap", $env.PLATFORM_PUBKEY, "", "1"]]
const shouldHideReplies = showControls ? synced("Feed.shouldHideReplies", false) : writable(false)
const reload = async () => {
@ -99,6 +103,14 @@
{anchor}
{note} />
</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}
</FlexColumn>

View File

@ -1,4 +1,5 @@
<script lang="ts">
import cx from "classnames"
import {NAMED_BOOKMARKS, toNostrURI, Address} from "@welshman/util"
import {slide} from "src/util/transition"
import {boolCtrl} from "src/partials/utils"
@ -9,8 +10,15 @@
import CopyValueSimple from "src/partials/CopyValueSimple.svelte"
import FeedSummary from "src/app/shared/FeedSummary.svelte"
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
import {readFeed, readList, displayFeed, mapListToFeed} from "src/domain"
import {repository} from "src/engine"
import {readFeed, readList, displayFeed, mapListToFeed, getSingletonValues} from "src/domain"
import {
hints,
pubkey,
repository,
addFeedFavorite,
removeFeedFavorite,
userFeedFavorites,
} from "src/engine"
import {globalFeed} from "src/app/state"
import {router} from "src/app/util"
@ -19,14 +27,19 @@
const expandDefinition = boolCtrl()
const event = repository.getEvent(address)
const deleted = repository.isDeleted(event)
const naddr = Address.from(address, hints.Event(event).getUrls()).toNaddr()
const feed = address.startsWith(NAMED_BOOKMARKS)
? mapListToFeed(readList(event))
: readFeed(event)
const toggleFavorite = () => (isFavorite ? removeFeedFavorite(address) : addFeedFavorite(address))
const loadFeed = () => {
globalFeed.set(feed)
router.at("notes").push()
}
$: isFavorite = getSingletonValues("a", $userFeedFavorites).has(address)
</script>
<Card class="flex gap-3">
@ -69,7 +82,15 @@
<i class="fa fa-angle-right" />
{/if}
</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>
{#if $expandDefinition.enabled}

View File

@ -1,6 +1,8 @@
<script lang="ts">
import {debounce} from "throttle-debounce"
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 {toSpliced} from "src/util/misc"
import {boolCtrl} from "src/partials/utils"
@ -13,8 +15,8 @@
import FeedForm from "src/app/shared/FeedForm.svelte"
import {router} from "src/app/util"
import {globalFeed} from "src/app/state"
import {normalizeFeedDefinition, displayList, readFeed, makeFeed, displayFeed} from "src/domain"
import {userListFeeds, canSign, deleteEvent, userFeeds} from "src/engine"
import {normalizeFeedDefinition, readFeed, makeFeed, displayFeed} from "src/domain"
import {userListFeeds, canSign, deleteEvent, userFeeds, userFavoritedFeeds} from "src/engine"
export let feed
export let updateFeed
@ -25,6 +27,10 @@
const listMenu = boolCtrl()
const followsFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Follows))})
const networkFeed = makeFeed({definition: normalizeFeedDefinition(makeScopeFeed(Scope.Network))})
const allFeeds = uniqBy(
feed => getAddress(feed.event),
sortBy(displayFeed, [...$userFeeds, ...$userListFeeds, ...$userFavoritedFeeds]),
)
const openForm = () => {
savePoint = {...feed}
@ -89,8 +95,6 @@
let search = getSearch(feed.definition)
$: subFeeds = getFeedArgs(feed.definition as any)
$: console.log(feed)
</script>
<div class="flex flex-grow items-center justify-end gap-2">
@ -132,20 +136,13 @@
on:click={() => setFeed(networkFeed)}>
Network
</MenuItem>
{#each $userFeeds as feed}
{#each allFeeds as feed}
<MenuItem
active={equals(feed.definition, $globalFeed.definition)}
on:click={() => setFeed(feed)}>
{displayFeed(feed)}
</MenuItem>
{/each}
{#each $userListFeeds as feed}
<MenuItem
active={equals(feed.definition, $globalFeed.definition)}
on:click={() => setFeed(feed)}>
{displayList(feed.list)}
</MenuItem>
{/each}
</div>
{#if $canSign}
<div class="bg-neutral-900">

View File

@ -11,7 +11,15 @@
import Anchor from "src/partials/Anchor.svelte"
import FeedField from "src/app/shared/FeedField.svelte"
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 exit
@ -98,9 +106,18 @@
<Anchor underline on:click={openSave} class="text-neutral-400">Save this feed</Anchor>
</Card>
{:else if draft.event || draft.list}
{@const event = draft.event || draft.list.event}
<Card class="flex flex-col justify-between sm:flex-row">
<p>You are currently editing your {displayFeed(draft)} feed.</p>
<Anchor underline on:click={startClone} class="text-neutral-400">
{#if event.pubkey === $pubkey}
<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
</Anchor>
</Card>
@ -108,7 +125,7 @@
<Card class="flex flex-col justify-between sm:flex-row">
<p>You are currently creating a new feed.</p>
<Anchor underline on:click={stopClone} class="text-neutral-400">
Edit your {displayFeed(feed)} feed instead
Edit "{displayFeed(feed)}" instead
</Anchor>
</Card>
{/if}

View File

@ -48,6 +48,7 @@
mention,
tracker,
hints,
mentionEvent,
repository,
unmuteNote,
muteNote,
@ -130,7 +131,7 @@
const crossPost = async (address = null) => {
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
if (note.kind === 1) {
@ -148,7 +149,7 @@
const startZap = () => {
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]
router

View File

@ -48,7 +48,9 @@
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
? fullContent
@ -65,7 +67,7 @@
</script>
<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)"}>
<div>
{#each shortContent as parsed, i}
@ -84,7 +86,7 @@
<QRCode copyOnClick code={parsed.value} />
</div>
{:else if isLink(parsed)}
<NoteContentLink value={parsed.value} showMedia={showMedia && isStartOrEnd(i)} />
<NoteContentLink value={parsed.value} showMedia={showMedia && isStartAndEnd(i)} />
{:else if isProfile(parsed)}
<PersonLink pubkey={parsed.value.pubkey} />
{:else if (isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 2}
@ -93,8 +95,6 @@
<slot name="note-content" {quote} />
</div>
</NoteContentQuote>
{:else if !expandable && isEllipsis(parsed)}
{@html renderParsed(parsed)}
{:else}
{@html renderParsed(parsed)}
{/if}

View File

@ -1,11 +1,12 @@
<script lang="ts">
import {derived} from "svelte/store"
import {uniq} from "@welshman/lib"
import {parseAnything} from "src/util/nostr"
import Anchor from "src/partials/Anchor.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
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 multiple = false
@ -16,27 +17,33 @@
const {loading, load} = createPeopleLoader()
const search = term => {
load(term)
const search = derived(profileSearch, $profileSearch => {
return term => {
load(term)
parseAnything(term).then(result => {
if (result?.type === "npub") {
value = uniq(value.concat(result.data))
input.clearTerm()
}
parseAnything(term).then(result => {
if (result?.type === "npub") {
loadPubkeyProfiles([result.data])
value = uniq(value.concat(result.data))
input.clearTerm()
onChange(value)
}
if (result?.type === "nprofile") {
value = uniq(value.concat(result.data.pubkey))
input.clearTerm()
}
})
if (result?.type === "nprofile") {
loadPubkeyProfiles([result.data.pubkey])
value = uniq(value.concat(result.data.pubkey))
input.clearTerm()
onChange(value)
}
})
return $profileSearch.searchValues(term)
}
return $profileSearch.searchValues(term)
}
})
</script>
<SearchSelect
{search}
search={$search}
{onChange}
{multiple}
{autofocus}

View File

@ -12,7 +12,14 @@
import RelayCardActions from "src/app/shared/RelayCardActions.svelte"
import {router} from "src/app/util/router"
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 claim = null
@ -27,7 +34,15 @@
const relay = deriveRelay(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>
<div
@ -95,18 +110,47 @@
{#if showControls && $canSign}
<div class="-mx-6 my-1 h-px bg-tinted-700" />
<div>
<Chip
pad
class={cx("cursor-pointer transition-opacity", {"opacity-50": !$policy.read})}
on:click={policySetter(RelayMode.Read)}>
<i class="fa fa-book-open text-neutral-300" /> Read
</Chip>
<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>
<Popover triggerType="mouseenter" class="inline-block">
<div slot="trigger">
<Chip
pad
class={cx("cursor-pointer transition-opacity", {"opacity-50": !$policy.read})}
on:click={policySetter(RelayMode.Read)}>
<i class="fa fa-book-open text-neutral-300" /> Read
</Chip>
</div>
<div slot="tooltip">
Notes intended for you will {$policy.read ? "" : "not"} be delivered to this relay.
</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>
{/if}
</div>

View File

@ -5,9 +5,7 @@
import {DIRECT_MESSAGE} from "@welshman/util"
import {formatTimestamp} from "src/util/misc"
import Channel from "src/partials/Channel.svelte"
import Content from "src/partials/Content.svelte"
import Popover from "src/partials/Popover.svelte"
import Modal from "src/partials/Modal.svelte"
import Anchor from "src/partials/Anchor.svelte"
import PersonCircles from "src/app/shared/PersonCircles.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"
@ -23,7 +21,6 @@
markChannelRead,
getChannelIdFromEvent,
listenForMessages,
sortEventsDesc,
ensureMessagePlaintext,
} from "src/engine"
@ -46,35 +43,9 @@
return sendLegacyMessage(channelId, content)
}
const [message] = sortEventsDesc($messages || [])
if (!message || message?.kind === 4) {
confirmMessage = content
} else {
sendMessage(channelId, content)
}
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(() => {
markChannelRead(channelId)
@ -88,7 +59,7 @@
document.title = `Direct Messages`
</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 class="flex items-center gap-4 pt-1">
<Anchor class="fa fa-arrow-left cursor-pointer text-2xl" href="/channels" />
@ -134,20 +105,22 @@
class:text-neutral-100={message.pubkey !== $session.pubkey}>
{formatTimestamp(message.created_at)}
{#if message.kind === 4}
<Popover>
<i slot="trigger" class="fa fa-unlock cursor-pointer text-neutral-200" />
<Popover triggerType="mouseenter">
<i slot="trigger" class="fa fa-unlock cursor-pointer text-neutral-400" />
<p slot="tooltip">
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>.
</p>
</Popover>
{:else}
<Popover>
<i slot="trigger" class="fa fa-lock cursor-pointer text-neutral-200" />
<Popover triggerType="mouseenter">
<i slot="trigger" class="fa fa-lock cursor-pointer text-neutral-400" />
<div slot="tooltip" class="flex flex-col gap-2">
<p>
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>
Note that these messages are not yet universally supported. Make sure the person
@ -159,29 +132,3 @@
</small>
</div>
</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}

View File

@ -1,4 +1,5 @@
<script lang="ts">
import {sortBy, uniqBy} from "@welshman/lib"
import {getAddress} from "@welshman/util"
import {onMount} from "svelte"
import {createScroller} from "src/util/misc"
@ -8,15 +9,18 @@
import Input from "src/partials/Input.svelte"
import FeedCard from "src/app/shared/FeedCard.svelte"
import {router} from "src/app/util/router"
import {displayFeed} from "src/domain"
import {
pubkey,
userFeeds,
feedSearch,
userListFeeds,
loadPubkeyFeeds,
userFavoritedFeeds,
userFollows,
} from "src/engine"
const favoritedFeeds = $userFavoritedFeeds
const createFeed = () => router.at("feeds/create").open()
const editFeed = address => router.at("feeds").of(address).open()
@ -29,6 +33,13 @@
let limit = 20
let element
$: allUserFeeds = [...$userFeeds, ...$userListFeeds]
$: feeds = uniqBy(
feed => getAddress(feed.event),
sortBy(displayFeed, [...allUserFeeds, ...favoritedFeeds]),
)
loadPubkeyFeeds(Array.from($userFollows))
onMount(() => {
@ -48,7 +59,7 @@
<i class="fa fa-plus" /> Feed
</Anchor>
</div>
{#each $userFeeds as feed (getAddress(feed.event))}
{#each feeds as feed (feed.event.id)}
{@const address = getAddress(feed.event)}
<div in:fly={{y: 20}}>
<FeedCard {address}>
@ -60,7 +71,7 @@
</FeedCard>
</div>
{/each}
{#each $userListFeeds as feed (getAddress(feed.list.event))}
{#each $userListFeeds as feed (feed.list.event.id)}
{@const address = getAddress(feed.list.event)}
<div in:fly={{y: 20}}>
<FeedCard {address}>
@ -85,7 +96,7 @@
</Input>
{#each $feedSearch
.searchValues(q)
.filter(address => !address.includes($pubkey))
.filter(address => !feeds.find(feed => getAddress(feed.event) === address))
.slice(0, limit) as address (address)}
<FeedCard {address} />
{/each}

View File

@ -3,7 +3,7 @@
import Anchor from "src/partials/Anchor.svelte"
import Subheading from "src/partials/Subheading.svelte"
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"
export let address
@ -14,14 +14,7 @@
const abort = () => router.pop()
const confirm = () => {
createAndPublish({
kind: 5,
tags: [["a", address]],
relays: hints.WithinContext(address).getUrls(),
sk: $adminKey.privkey,
forcePlatform: false,
})
deleteGroupMeta(address)
showInfo("Group deleted!")
router.pop()
}

View File

@ -33,7 +33,7 @@
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<i class="fa fa-list fa-lg" />
<h2 class="staatliches text-2xl">Your feeds</h2>
<h2 class="staatliches text-2xl">Your lists</h2>
</div>
<Anchor button accent on:click={createList}>
<i class="fa fa-plus" /> List

View File

@ -5,7 +5,7 @@
import {join, whereEq, identity} from "ramda"
import {throttle, commaFormat, toTitle, switcherFn} from "hurdak"
import {now, writable} from "@welshman/lib"
import {createEvent, Tags} from "@welshman/util"
import {createEvent} from "@welshman/util"
import {currencyOptions} from "src/util/i18n"
import {dateToSeconds} from "src/util/misc"
import {showWarning, showPublishInfo} from "src/partials/Toast.svelte"
@ -44,11 +44,7 @@
export let group = null
export let initialValues = {}
const defaultGroups = $env.FORCE_GROUP
? [$env.FORCE_GROUP]
: quote
? Tags.fromEvent(quote).context().values().valueOf()
: [group].filter(identity)
const defaultGroups = $env.FORCE_GROUP ? [$env.FORCE_GROUP] : [group].filter(identity)
let images, compose
let charCount = 0

View File

@ -19,7 +19,7 @@
createAndPublish,
updateSingleton,
publishProfile,
setRelayPolicies,
setOutboxPolicies,
tagsFromContent,
requestRelayAccess,
loginWithPrivateKey,
@ -76,7 +76,7 @@
}
// 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
publishProfile(profile)

View File

@ -17,13 +17,13 @@
export let eid = null
export let anonymous = false
export let callback = null
export let amount = getSetting("default_zap")
let zaps = []
let message = ""
let loading = false
let totalAmount = getSetting("default_zap")
const updateZaps = (message, totalAmount) => {
const updateZaps = (message, amount) => {
let totalWeight = 0
zaps = doPipe(splits, [
@ -39,7 +39,7 @@
map(([pubkey, relay, weight]: string[]) => ({
relay,
pubkey,
amount: Math.round(totalAmount * (parseFloat(weight) / totalWeight)),
amount: Math.round(amount * (parseFloat(weight) / totalWeight)),
status: "pending",
})),
sortBy((split: any) => -split.amount),
@ -137,7 +137,7 @@
}
// Watch inputs and update zaps
$: updateZaps(message, totalAmount)
$: updateZaps(message, amount)
// Initialize bitcoin connect
init({appName: import.meta.env.VITE_APP_NAME})
@ -146,7 +146,7 @@
{#if zaps.length > 0}
<h1 class="staatliches text-2xl">Send a zap</h1>
<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" />
<span slot="after" class="-mt-1">sats</span>
</Input>

View File

@ -1,6 +1,5 @@
import {last} from "@welshman/lib"
import {LOCAL_RELAY_URL, normalizeRelayUrl as _normalizeRelayUrl} from "@welshman/util"
import {getRelayTags} from "src/util/nostr"
// Utils related to bare urls
@ -42,25 +41,23 @@ export const filterRelaysByNip = (nip: number, relays) =>
export enum RelayMode {
Read = "read",
Write = "write",
Inbox = "inbox",
}
export type RelayPolicy = {
url: string
read: boolean
write: boolean
inbox: boolean
}
export const makeRelayPolicy = (
relayPolicy: Partial<RelayPolicy> & {url: string},
): RelayPolicy => ({
export const makeRelayPolicy = ({
url,
...relayPolicy
}: Partial<RelayPolicy> & {url: string}): RelayPolicy => ({
url: normalizeRelayUrl(url),
read: false,
write: false,
inbox: false,
...relayPolicy,
})
export const makeRelayPoliciesFromTags = (tags: string[][]) =>
getRelayTags(tags).map(([_, url, mode]) => ({
url: normalizeRelayUrl(url),
write: !mode || mode === RelayMode.Write,
read: !mode || mode === RelayMode.Read,
}))

View File

@ -10,11 +10,13 @@ import {
Address,
isSignedEvent,
normalizeRelayUrl,
FEEDS,
FOLLOWS,
RELAYS,
PROFILE,
MUTES,
WRAP_NIP04,
INBOX_RELAYS,
} from "@welshman/util"
import {Fetch, chunk, createMapOf, randomId, seconds, sleep, tryFunc} from "hurdak"
import {
@ -39,6 +41,7 @@ import {
editSingleton,
createSingleton,
readSingleton,
makeRelayPolicy,
} from "src/domain"
import type {RelayPolicy} from "src/domain"
import type {Session, NostrConnectHandler} from "src/engine/model"
@ -80,6 +83,8 @@ import {
zappers,
getPlaintext,
anonymous,
mentionGroup,
userRelayPolicies,
} from "src/engine/state"
import {loadHandle, loadZapper} from "src/engine/requests"
@ -311,9 +316,9 @@ export const wrapWithFallback = async (template, {author = null, wrap}) => {
return events
}
const addATags = (template, addresses) => ({
const addGroupATags = (template, addresses) => ({
...template,
tags: [...template.tags, ...addresses.map(a => ["a", a])],
tags: [...template.tags, ...addresses.map(mentionGroup)],
})
// 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()
return publish({event, relays, forcePlatform: false})
@ -387,7 +392,7 @@ export const publishToGroupsPrivately = async (addresses, template, {anonymous =
const pubs = []
for (const address of addresses) {
const relays = hints.WithinContext(address).getUrls()
const thisTemplate = addATags(template, [address])
const thisTemplate = addGroupATags(template, [address])
const sharedKey = deriveSharedKeyForGroup(address).get()
if (!address.startsWith("35834:")) {
@ -489,7 +494,7 @@ export const publishAdminKeyShares = async (address, pubkeys) => {
const {privkey} = deriveAdminKeyForGroup(address).get()
const template = createEvent(24, {
tags: [
["a", address],
mentionGroup(address),
["role", "admin"],
["privkey", privkey],
...getClientTags(),
@ -506,7 +511,7 @@ export const publishGroupInvites = async (address, pubkeys, gracePeriod = 0) =>
const {privkey} = deriveSharedKeyForGroup(address).get()
const template = createEvent(24, {
tags: [
["a", address],
mentionGroup(address),
["role", "member"],
["privkey", privkey],
["grace_period", String(gracePeriod)],
@ -523,13 +528,13 @@ export const publishGroupEvictions = async (address, pubkeys) =>
address,
pubkeys,
createEvent(24, {
tags: [["a", address], ...getClientTags()],
tags: [mentionGroup(address), ...getClientTags()],
}),
)
export const publishGroupMembers = async (address, op, pubkeys) => {
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)
@ -576,7 +581,7 @@ export const publishGroupMeta = (address, identifier, meta, listPublicly) => {
}
export const deleteGroupMeta = address =>
publishAsGroupAdminPublicly(address, createEvent(5, {tags: [["a", address]]}))
publishAsGroupAdminPublicly(address, createEvent(5, {tags: [mentionGroup(address)]}))
// Member functions
@ -599,7 +604,7 @@ export const publishGroupEntryRequest = (address, claim = null) => {
} else {
setGroupStatus(pubkey.get(), address, now(), {access: GroupAccess.Requested})
const tags = [...getClientTags(), ["a", address]]
const tags = [...getClientTags(), mentionGroup(address)]
if (claim) {
tags.push(["claim", claim])
@ -623,7 +628,7 @@ export const publishGroupExitRequest = address => {
address,
createEvent(26, {
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 =>
createAndPublish({
kind: 10004,
tags: [...addresses.map(a => ["a", a]), ...getClientTags()],
tags: [...addresses.map(mentionGroup), ...getClientTags()],
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 ((event?.created_at || 0) < now() - seconds(5, "minute")) {
console.log("loading")
const loadedEvent = await loadOne({relays: hints.User().getUrls(), filters})
console.log("loaded", loadedEvent)
if ((loadedEvent?.created_at || 0) > (event?.created_at || 0)) {
event = loadedEvent
@ -710,8 +717,12 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
encryptable = createSingleton({...singleton, publicTags})
}
console.log(1)
const template = await encryptable.reconcile(encrypt)
console.log(2, template)
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 removeFeedFavorite = (address: string) =>
updateSingleton(FEEDS, tags => reject(nthEq(1, address), tags))
export const addFeedFavorite = (address: string) =>
updateSingleton(FEEDS, tags => append(tags, ["a", address]))
// Relays
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,
})
export const setRelayPolicies = async (modifyTags: ModifyTags) => {
export const setOutboxPolicies = async (modifyTags: ModifyTags) => {
if (canSign.get()) {
updateSingleton(RELAYS, modifyTags)
} else {
@ -762,8 +779,26 @@ export const setRelayPolicies = async (modifyTags: ModifyTags) => {
}
}
export const setRelayPolicy = ({url, read, write}: RelayPolicy) =>
setRelayPolicies($tags => {
export const setInboxPolicies = async (modifyTags: ModifyTags) =>
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)
if (read && write) {
@ -778,7 +813,10 @@ export const setRelayPolicy = ({url, read, write}: RelayPolicy) =>
})
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
if (pubkey.get()) {
@ -793,7 +831,7 @@ export const joinRelay = async (url: string, claim?: string) => {
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
if (pubkey.get()) {
@ -889,7 +927,7 @@ export const sendMessage = async (channelId: string, content: string) => {
publish({
event: rumor.wrap,
relays: hints.merge(recipients.map(hints.PublishMessage)).getUrls(),
relays: hints.PublishMessage(recipient).getUrls(),
forcePlatform: false,
})
}

View File

@ -4,10 +4,12 @@ import type {TrustedEvent} from "@welshman/util"
import {
RELAYS,
PROFILE,
INBOX_RELAYS,
HANDLER_INFORMATION,
NAMED_BOOKMARKS,
COMMUNITIES,
FEED,
FEEDS,
MUTES,
FOLLOWS,
APP_DATA,
@ -50,12 +52,12 @@ const getFiltersForKey = (key: string, authors: string[]) => {
case "pubkey/feeds":
return [{authors, kinds: [NAMED_BOOKMARKS, FEED]}]
case "pubkey/relays":
return [{authors, kinds: [RELAYS]}]
return [{authors, kinds: [RELAYS, INBOX_RELAYS]}]
case "pubkey/profile":
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}]
case "pubkey/user":
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)},
]
}

View File

@ -43,8 +43,10 @@ import {
} from "@welshman/lib"
import {
WRAP,
FEEDS,
COMMUNITY,
GROUP,
INBOX_RELAYS,
WRAP_NIP04,
COMMUNITIES,
READ_RECEIPT,
@ -72,6 +74,7 @@ import {
LOCAL_RELAY_URL,
getFilterResultCardinality,
isShareableRelayUrl,
isReplaceable,
} from "@welshman/util"
import type {Filter, RouterScenario, TrustedEvent, SignedEvent} from "@welshman/util"
import {
@ -97,6 +100,8 @@ import {
repostKinds,
noteKinds,
reactionKinds,
getRelayTags,
getRelayTagValues,
} from "src/util/nostr"
import logger from "src/util/logger"
import type {
@ -110,10 +115,10 @@ import type {
Handle,
} from "src/domain"
import {
RelayMode,
EDITABLE_LIST_KINDS,
getSingletonValues,
makeSingleton,
makeRelayPoliciesFromTags,
ListSearch,
FeedSearch,
profileHasName,
@ -963,8 +968,7 @@ export const groupNotifications = new Derived(
const $isEventMuted = isEventMuted.get()
const shouldSkip = e => {
const tags = Tags.fromEvent(e)
const context = tags.context().values().valueOf()
const context = e.tags.filter(t => t[0] === "a")
return (
!context.some(a => addresses.has(a)) ||
@ -972,7 +976,7 @@ export const groupNotifications = new Derived(
!noteKinds.includes(e.kind) ||
e.pubkey === $session.pubkey ||
// 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)
)
}
@ -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(
deriveEventsMapped<{event: TrustedEvent; policy: RelayPolicy[]}>({
filters: [{kinds: [FOLLOWS]}],
@ -1115,11 +1132,7 @@ export const legacyRelayLists = withGetter(
JSON.parse(event.content) as Record<string, {write: boolean; read: boolean}>,
)
.filter(([url]) => isShareableRelayUrl(url))
.map(([url, {write = true, read = true}]) => ({
url: normalizeRelayUrl(url),
write,
read,
}))
.map(([url, {write = true, read = true}]) => makeRelayPolicy({url, read, write}))
return {event, policy}
} catch (e) {
@ -1130,21 +1143,54 @@ export const legacyRelayLists = withGetter(
)
export const relayPoliciesByPubkey = withGetter(
derived([relayLists, legacyRelayLists], ([$relayLists, $legacyRelayLists]) => {
const policies = new Map<string, RelayPolicy[]>()
derived(
[relayLists, inboxRelayLists, legacyRelayLists],
([$relayLists, $inboxRelayLists, $legacyRelayLists]) => {
const policiesByUrlByPubkey = new Map<string, Map<string, RelayPolicy>>()
for (const {event, publicTags} of $relayLists) {
policies.set(event.pubkey, makeRelayPoliciesFromTags(publicTags))
}
for (const {event, publicTags} of $relayLists) {
const policiesByUrl = new Map()
for (const {event, policy} of $legacyRelayLists) {
if (!policies.has(event.pubkey)) {
policies.set(event.pubkey, policy)
for (const [_, url, mode] of getRelayTags(publicTags)) {
const read = !mode || mode === RelayMode.Read
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) => {
@ -1155,7 +1201,14 @@ export const getPubkeyRelayPolicies = (pubkey: string, mode: string = null) => {
export const userRelayPolicies = derived(
[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 =>
@ -1278,15 +1331,36 @@ export const listSearch = derived(lists, $lists => new ListSearch($lists))
export const feeds = deriveEventsMapped<PublishedFeed>({
filters: [{kinds: [FEED]}],
eventToItem: readFeed,
itemToEvent: prop("event"),
eventToItem: readFeed,
})
export const userFeeds = derived([feeds, pubkey], ([$feeds, $pubkey]: [PublishedFeed[], string]) =>
sortBy(
f => f.title.toLowerCase(),
$feeds.filter(feed => feed.event.pubkey === $pubkey),
),
$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))
@ -1516,6 +1590,7 @@ export const onAuth = async (url, challenge) => {
export type MySubscribeRequest = SubscribeRequest & {
onEvent?: (event: TrustedEvent) => void
onEose?: (url: string) => void
onComplete?: () => void
skipCache?: boolean
forcePlatform?: boolean
@ -1560,6 +1635,10 @@ export const subscribe = ({forcePlatform = true, ...request}: MySubscribeRequest
projections.push(await ensureUnwrapped(event))
})
if (request.onEose) {
sub.emitter.on("eose", request.onEose)
}
if (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 = (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) =>
new Promise<TrustedEvent | null>(resolve => {
@ -1710,8 +1796,28 @@ Object.assign(NetworkContext, {
export const uniqTags = tags =>
uniqBy((t: string[]) => (t[0] === "param" ? t.join(":") : t.slice(0, 2).join(":")), tags)
export const mention = (pubkey: string, ...args: unknown[]) =>
hints.tagPubkey(pubkey).append(displayProfileByPubkey(pubkey)).valueOf()
export const mention = (pubkey: string, ...args: unknown[]) => [
"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) => {
const tags = []
@ -1786,7 +1892,7 @@ export const getReplyTags = (parent: TrustedEvent) => {
// Add a/e-tags for the parent event
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())
}
@ -1802,7 +1908,7 @@ export const getReactionTags = (parent: TrustedEvent) => {
}
// 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())
}
@ -2007,19 +2113,19 @@ class IndexedDBAdapter {
const removedRecords = prev.filter(r => !currentIds.has(r[key]))
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)
}
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)))
}
// If we have much more than our limit, prune our store. This will get persisted
// the next time around.
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))
}

View File

@ -1,28 +1,26 @@
<script lang="ts">
import {onMount} from "svelte"
import {sleep} from "hurdak"
import {sleep} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {INBOX_RELAYS} from "@welshman/util"
import {prop, max, reverse, pluck, sortBy, last} from "ramda"
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 Anchor from "src/partials/Anchor.svelte"
import Popover from "src/partials/Popover.svelte"
import Toggle from "src/partials/Toggle.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import ImageInput from "src/partials/ImageInput.svelte"
import {nip44} from "src/engine"
import {nip44, repository} from "src/engine"
export let pubkeys
export let channelId
export let messages: TrustedEvent[]
export let sendMessage
export let initialMessage = ""
export let messages: TrustedEvent[]
const loading = sleep(30_000)
const useNip44 = synced(`useNip44/${channelId}`, true)
const startScroller = () => {
scroller?.stop()
scroller = createScroller(loadMore, {element, reverse: true})
@ -36,6 +34,10 @@
let limit = 10
let showNewMessages = false
let groupedMessages = []
let useNip44 =
pubkeys.length > 2 ||
($nip44.isEnabled() &&
repository.query([{kinds: [INBOX_RELAYS], authors: pubkeys}]).length === pubkeys.length)
onMount(() => {
startScroller()
@ -76,7 +78,7 @@
if (content) {
textarea.value = ""
await sendMessage(content, $useNip44)
await sendMessage(content, useNip44)
stickToBottom()
}
@ -159,7 +161,7 @@
</div>
{#if $nip44.isEnabled()}
<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>
Send messages using
<Popover class="inline">