mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-28 16:00:52 +00:00
Compare commits
7 Commits
b92b0b71b5
...
1408f782f1
Author | SHA1 | Date | |
---|---|---|---|
|
1408f782f1 | ||
|
b1f2218e81 | ||
|
282a80cdea | ||
|
e212297396 | ||
|
c9db37c92a | ||
|
148a63d95f | ||
|
a5517f2eff |
@ -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
BIN
package-lock.json
generated
Binary file not shown.
@ -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",
|
||||
|
@ -236,3 +236,9 @@ body,
|
||||
.react-switch-bg {
|
||||
border: 1px solid var(--neutral-600);
|
||||
}
|
||||
|
||||
/* note content */
|
||||
|
||||
.note-content a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -319,6 +319,7 @@
|
||||
required: ["splits"],
|
||||
serializers: {
|
||||
eid: asNote,
|
||||
amount: asJson("amount"),
|
||||
splits: asJson("splits"),
|
||||
anonymous: asJson("anonymous"),
|
||||
},
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
}))
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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)},
|
||||
]
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user