mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 00:10:52 +00:00
Compare commits
12 Commits
e212297396
...
0acf627870
Author | SHA1 | Date | |
---|---|---|---|
|
0acf627870 | ||
|
abd306ccac | ||
|
b7f6d4b8be | ||
|
d7e808d977 | ||
|
5cbdda2438 | ||
|
01e6c663fb | ||
|
4708dc3013 | ||
|
2e3df2838d | ||
|
2a32f31aba | ||
|
1408f782f1 | ||
|
b1f2218e81 | ||
|
282a80cdea |
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
# 0.4.8
|
||||
|
||||
- [x] Add support for kind 10050 relay lists
|
||||
- [x] Toggle nip44 messages based on 10050 signaling
|
||||
- [x] Fix settings being stale until reload
|
||||
- [x] Fix WoT not being applied to mute replies
|
||||
- [x] Add developer donation prompt
|
||||
- [x] Add WoT information to feed cards and search
|
||||
- [x] Fix group updates not getting saved
|
||||
- [x] Add relay hints to group/community tags
|
||||
|
||||
# 0.4.7
|
||||
|
||||
- [x] Show toast when offline
|
||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -59,7 +59,7 @@
|
||||
"@welshman/feeds": "^0.0.12",
|
||||
"@welshman/lib": "^0.0.10",
|
||||
"@welshman/net": "^0.0.14",
|
||||
"@welshman/util": "^0.0.15",
|
||||
"@welshman/util": "^0.0.16",
|
||||
"bowser": "^2.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"compressorjs": "^1.2.1",
|
||||
|
@ -319,6 +319,7 @@
|
||||
required: ["splits"],
|
||||
serializers: {
|
||||
eid: asNote,
|
||||
amount: asJson("amount"),
|
||||
splits: asJson("splits"),
|
||||
anonymous: asJson("anonymous"),
|
||||
},
|
||||
|
@ -1,15 +1,18 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {writable} from "@welshman/lib"
|
||||
import {seconds} from "hurdak"
|
||||
import {writable, now, 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 +27,10 @@
|
||||
export let showGroup = false
|
||||
export let onEvent = null
|
||||
|
||||
const splits = [["zap", $env.PLATFORM_PUBKEY, "", "1"]]
|
||||
|
||||
const promptDismissed = synced("feed/promptDismissed", 0)
|
||||
|
||||
const shouldHideReplies = showControls ? synced("Feed.shouldHideReplies", false) : writable(false)
|
||||
|
||||
const reload = async () => {
|
||||
@ -99,6 +106,21 @@
|
||||
{anchor}
|
||||
{note} />
|
||||
</div>
|
||||
{#if i > 20 && parseInt(hash(note.id)) % 100 === 0 && $promptDismissed < now() - seconds(7, "day")}
|
||||
<Card class="group flex items-center justify-between">
|
||||
<p class="text-xl">Enjoying Coracle?</p>
|
||||
<div class="flex gap-2">
|
||||
<Anchor
|
||||
class="text-neutral-400 opacity-0 transition-all group-hover:opacity-100"
|
||||
on:click={() => promptDismissed.set(now())}>
|
||||
Dismiss
|
||||
</Anchor>
|
||||
<Anchor modal button accent href={router.at("zap").qp({splits}).toString()}>
|
||||
Zap the developer
|
||||
</Anchor>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
{/each}
|
||||
</FlexColumn>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {remove} from "@welshman/lib"
|
||||
import {NAMED_BOOKMARKS, toNostrURI, Address} from "@welshman/util"
|
||||
import {slide} from "src/util/transition"
|
||||
import {boolCtrl} from "src/partials/utils"
|
||||
@ -8,6 +9,7 @@
|
||||
import Chip from "src/partials/Chip.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import CopyValueSimple from "src/partials/CopyValueSimple.svelte"
|
||||
import PersonCircles from "src/app/shared/PersonCircles.svelte"
|
||||
import FeedSummary from "src/app/shared/FeedSummary.svelte"
|
||||
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
|
||||
import {readFeed, readList, displayFeed, mapListToFeed, getSingletonValues} from "src/domain"
|
||||
@ -18,6 +20,7 @@
|
||||
addFeedFavorite,
|
||||
removeFeedFavorite,
|
||||
userFeedFavorites,
|
||||
feedFavoritesByAddress,
|
||||
} from "src/engine"
|
||||
import {globalFeed} from "src/app/state"
|
||||
import {router} from "src/app/util"
|
||||
@ -40,6 +43,10 @@
|
||||
}
|
||||
|
||||
$: isFavorite = getSingletonValues("a", $userFeedFavorites).has(address)
|
||||
$: favoritedPubkeys = remove(
|
||||
$pubkey,
|
||||
($feedFavoritesByAddress.get(address) || []).map(s => s.event.pubkey),
|
||||
)
|
||||
</script>
|
||||
|
||||
<Card class="flex gap-3">
|
||||
@ -70,6 +77,12 @@
|
||||
{#if feed.description}
|
||||
<p>{feed.description}</p>
|
||||
{/if}
|
||||
{#if favoritedPubkeys.length > 0}
|
||||
<div class="flex gap-2">
|
||||
<span class="text-neutral-300">Bookmarked by</span>
|
||||
<PersonCircles class="h-6 w-6" pubkeys={favoritedPubkeys.slice(0, 20)} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2 flex items-start justify-between">
|
||||
<FeedSummary feed={feed.definition} />
|
||||
<div class="flex gap-1">
|
||||
|
@ -8,8 +8,10 @@
|
||||
const meta = deriveGroupMeta(address)
|
||||
</script>
|
||||
|
||||
<NoteContentKind1
|
||||
{#key $meta?.about}
|
||||
<NoteContentKind1
|
||||
note={{content: $meta?.about || ""}}
|
||||
minLength={100}
|
||||
maxLength={140}
|
||||
showEntire={!truncate} />
|
||||
{/key}
|
||||
|
@ -10,7 +10,7 @@
|
||||
deriveGroup,
|
||||
deriveGroupStatus,
|
||||
deriveAdminKeyForGroup,
|
||||
deriveUserCommunities,
|
||||
getUserCommunities,
|
||||
} from "src/engine"
|
||||
|
||||
export let address
|
||||
@ -51,9 +51,9 @@
|
||||
})
|
||||
}
|
||||
|
||||
const join = () => publishCommunitiesList(deriveUserCommunities().get().concat(address))
|
||||
const join = () => publishCommunitiesList(getUserCommunities(session.get()).concat(address))
|
||||
|
||||
const leave = () => publishCommunitiesList(without([address], deriveUserCommunities().get()))
|
||||
const leave = () => publishCommunitiesList(without([address], getUserCommunities(session.get())))
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-3" on:click|stopPropagation>
|
||||
|
@ -114,7 +114,7 @@
|
||||
$: reply = tags.parent()
|
||||
$: root = tags.root()
|
||||
|
||||
$: muted = !showMuted && $isEventMuted(event, true)
|
||||
$: muted = !showMuted && $isEventMuted(event)
|
||||
|
||||
// Find children in our context
|
||||
$: children = context.filter(e => isChildOf(e, event))
|
||||
@ -130,7 +130,7 @@
|
||||
visibleReplies = []
|
||||
|
||||
for (const e of replies) {
|
||||
if ($isEventMuted(e)) {
|
||||
if ($isEventMuted(e, true)) {
|
||||
mutedReplies.push(e)
|
||||
} else if (collapsed) {
|
||||
hiddenReplies.push(e)
|
||||
|
@ -48,11 +48,12 @@
|
||||
mention,
|
||||
tracker,
|
||||
hints,
|
||||
mentionEvent,
|
||||
repository,
|
||||
unmuteNote,
|
||||
muteNote,
|
||||
deriveHandlersForKind,
|
||||
deriveIsGroupMember,
|
||||
userIsGroupMember,
|
||||
publishToZeroOrMoreGroups,
|
||||
deleteEvent,
|
||||
getSetting,
|
||||
@ -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
|
||||
@ -191,12 +192,12 @@
|
||||
window.open(templateTag[1].replace("<bech32>", entity))
|
||||
}
|
||||
|
||||
const groupOptions = session.derived($session => {
|
||||
const groupOptions = derived(session, $session => {
|
||||
const options = []
|
||||
|
||||
for (const addr of Object.keys($session?.groups || {})) {
|
||||
const group = groups.key(addr).get()
|
||||
const isMember = deriveIsGroupMember(addr).get()
|
||||
const isMember = $userIsGroupMember(addr)
|
||||
|
||||
if (group && isMember && addr !== address) {
|
||||
options.push(group)
|
||||
@ -211,9 +212,7 @@
|
||||
let handlersShown = false
|
||||
|
||||
$: disableActions =
|
||||
!$canSign ||
|
||||
($muted && !showMuted) ||
|
||||
(note.wrap && address && !deriveIsGroupMember(address).get())
|
||||
!$canSign || ($muted && !showMuted) || (note.wrap && address && !$userIsGroupMember(address))
|
||||
$: like = likes.find(e => e.pubkey === $session?.pubkey)
|
||||
$: $likesCount = likes.length
|
||||
$: zap = zaps.find(e => e.request.pubkey === $session?.pubkey)
|
||||
|
@ -82,7 +82,7 @@
|
||||
{#if muted}
|
||||
<p class="mb-1 py-24 text-center text-neutral-600">
|
||||
You have hidden this note.
|
||||
<Anchor class="underline" on:click={unmute}>Show</Anchor>
|
||||
<Anchor class="underline" stopPropagation on:click={unmute}>Show</Anchor>
|
||||
</p>
|
||||
{:else}
|
||||
{#if !isGroup}
|
||||
|
@ -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,12 +110,21 @@
|
||||
{#if showControls && $canSign}
|
||||
<div class="-mx-6 my-1 h-px bg-tinted-700" />
|
||||
<div>
|
||||
<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})}
|
||||
@ -108,5 +132,25 @@
|
||||
<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"
|
||||
|
||||
@ -40,40 +37,14 @@
|
||||
|
||||
const getContent = e => (e.kind === 4 ? ensureMessagePlaintext(e) : e.content) || ""
|
||||
|
||||
const send = async (content, useNip44) => {
|
||||
const send = async (content, useNip17) => {
|
||||
// If we don't have nip44 support, just send a legacy message
|
||||
if (!$nip44.isEnabled() || !useNip44) {
|
||||
if (!$nip44.isEnabled() || !useNip17) {
|
||||
return sendLegacyMessage(channelId, content)
|
||||
}
|
||||
|
||||
const [message] = sortEventsDesc($messages || [])
|
||||
|
||||
if (!message || message?.kind === 4) {
|
||||
confirmMessage = content
|
||||
} else {
|
||||
sendMessage(channelId, content)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmNip04 = () => {
|
||||
sendLegacyMessage(channelId, confirmMessage)
|
||||
confirmMessage = null
|
||||
}
|
||||
|
||||
const confirmNip44 = () => {
|
||||
sendMessage(channelId, confirmMessage)
|
||||
confirmMessage = null
|
||||
}
|
||||
|
||||
const abortMessage = () => {
|
||||
if (confirmMessage) {
|
||||
ctrl.setMessage(confirmMessage)
|
||||
}
|
||||
|
||||
confirmMessage = null
|
||||
}
|
||||
|
||||
let confirmMessage, ctrl
|
||||
|
||||
onMount(() => {
|
||||
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,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {sortBy, uniqBy} from "@welshman/lib"
|
||||
import {getAddress} from "@welshman/util"
|
||||
import {sortBy, flatten, batch, uniqBy} from "@welshman/lib"
|
||||
import {FEEDS, getAddress, getIdFilters} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {onMount} from "svelte"
|
||||
import {createScroller} from "src/util/misc"
|
||||
import {getAddressTagValues} from "src/util/nostr"
|
||||
import {fly} from "src/util/transition"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
@ -11,41 +13,64 @@
|
||||
import {router} from "src/app/util/router"
|
||||
import {displayFeed} from "src/domain"
|
||||
import {
|
||||
load,
|
||||
hints,
|
||||
userFeeds,
|
||||
repository,
|
||||
feedSearch,
|
||||
userListFeeds,
|
||||
loadPubkeyFeeds,
|
||||
feedFavorites,
|
||||
userFavoritedFeeds,
|
||||
userFollows,
|
||||
} from "src/engine"
|
||||
|
||||
const favoritedFeeds = $userFavoritedFeeds
|
||||
|
||||
const createFeed = () => router.at("feeds/create").open()
|
||||
|
||||
const editFeed = address => router.at("feeds").of(address).open()
|
||||
|
||||
const loadFeeds = batch(300, (addresseses: string[][]) => {
|
||||
const addresses = flatten(addresseses).filter(a => !repository.getEvent(a))
|
||||
|
||||
if (addresses.length > 0) {
|
||||
load({
|
||||
relays: hints.User().getUrls(),
|
||||
filters: getIdFilters(addresses),
|
||||
skipCache: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onRepositoryUpdate = ({added}: {added: TrustedEvent[]}) =>
|
||||
loadFeeds(added.filter(e => e.kind === FEEDS).flatMap(e => getAddressTagValues(e.tags)))
|
||||
|
||||
const loadMore = async () => {
|
||||
limit += 20
|
||||
}
|
||||
|
||||
let q = ""
|
||||
let limit = 20
|
||||
let initialAddrs = new Set()
|
||||
let element
|
||||
|
||||
$: allUserFeeds = [...$userFeeds, ...$userListFeeds]
|
||||
|
||||
$: feeds = uniqBy(
|
||||
feed => getAddress(feed.event),
|
||||
sortBy(displayFeed, [...allUserFeeds, ...favoritedFeeds]),
|
||||
sortBy(displayFeed, [...$userFeeds, ...$userListFeeds, ...$userFavoritedFeeds]),
|
||||
)
|
||||
|
||||
loadPubkeyFeeds(Array.from($userFollows))
|
||||
loadFeeds($feedFavorites.flatMap(s => getAddressTagValues(s.event.tags)))
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller(loadMore, {element})
|
||||
|
||||
return () => scroller.stop()
|
||||
initialAddrs = new Set(feeds.map(feed => getAddress(feed.event)))
|
||||
repository.on("update", onRepositoryUpdate)
|
||||
|
||||
return () => {
|
||||
scroller.stop()
|
||||
repository.off("update", onRepositoryUpdate)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -96,7 +121,7 @@
|
||||
</Input>
|
||||
{#each $feedSearch
|
||||
.searchValues(q)
|
||||
.filter(address => !feeds.find(feed => getAddress(feed.event) === address))
|
||||
.filter(address => !initialAddrs.has(address))
|
||||
.slice(0, limit) as address (address)}
|
||||
<FeedCard {address} />
|
||||
{/each}
|
||||
|
@ -9,6 +9,7 @@
|
||||
import {
|
||||
env,
|
||||
pubkey,
|
||||
session,
|
||||
initGroup,
|
||||
publishGroupMeta,
|
||||
publishGroupInvites,
|
||||
@ -16,7 +17,7 @@
|
||||
publishCommunityMeta,
|
||||
publishCommunitiesList,
|
||||
publishGroupMembers,
|
||||
deriveUserCommunities,
|
||||
getUserCommunities,
|
||||
} from "src/engine"
|
||||
import {router} from "src/app/util/router"
|
||||
|
||||
@ -50,7 +51,7 @@
|
||||
|
||||
if (kind === COMMUNITY) {
|
||||
await publishCommunityMeta(address, identifier, meta)
|
||||
await publishCommunitiesList(deriveUserCommunities().get().concat(address))
|
||||
await publishCommunitiesList(getUserCommunities(session.get()).concat(address))
|
||||
} else {
|
||||
await publishGroupMeta(address, identifier, meta, listing_is_public)
|
||||
await publishGroupMembers(address, "set", members)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
deriveGroupMeta,
|
||||
deriveAdminKeyForGroup,
|
||||
deriveSharedKeyForGroup,
|
||||
deriveIsGroupMember,
|
||||
userIsGroupMember,
|
||||
deriveGroupStatus,
|
||||
loadGroups,
|
||||
loadGroupMessages,
|
||||
@ -38,7 +38,6 @@
|
||||
const group = deriveGroup(address)
|
||||
const meta = deriveGroupMeta(address)
|
||||
const status = deriveGroupStatus(address)
|
||||
const isGroupMember = deriveIsGroupMember(address)
|
||||
const sharedKey = deriveSharedKeyForGroup(address)
|
||||
const adminKey = deriveAdminKeyForGroup(address)
|
||||
const requests = groupRequests.derived(requests =>
|
||||
@ -56,7 +55,7 @@
|
||||
|
||||
let tabs
|
||||
|
||||
$: key = $group && $isGroupMember
|
||||
$: key = $group && $userIsGroupMember(address)
|
||||
|
||||
$: {
|
||||
if (key) {
|
||||
|
@ -15,7 +15,7 @@
|
||||
groups,
|
||||
loadGiftWraps,
|
||||
loadGroupMessages,
|
||||
deriveIsGroupMember,
|
||||
userIsGroupMember,
|
||||
updateCurrentSession,
|
||||
communityListsByAddress,
|
||||
searchGroupMeta,
|
||||
@ -26,7 +26,7 @@
|
||||
limit += 20
|
||||
}
|
||||
|
||||
const userIsMember = meta => deriveIsGroupMember(getAddress(meta.event), true).get()
|
||||
const userIsMember = meta => $userIsGroupMember(getAddress(meta.event), true)
|
||||
|
||||
const userGroupMeta = derived(groupMeta, filter(userIsMember))
|
||||
|
||||
|
@ -6,8 +6,7 @@
|
||||
|
||||
export let topic
|
||||
|
||||
const topics = ["web-of-trust", "nip-44-dms"]
|
||||
const nip44Url = "https://github.com/nostr-protocol/nips/blob/master/44.md"
|
||||
const topics = ["web-of-trust", "nip-17-dms"]
|
||||
const nip17Url = "https://github.com/nostr-protocol/nips/blob/master/17.md"
|
||||
</script>
|
||||
|
||||
@ -29,11 +28,10 @@
|
||||
You can set a minimum web of trust score on your content settings page, which will
|
||||
automatically mute anyone with a lower score than your threshold.
|
||||
</p>
|
||||
{:else if topic === "nip-44-dms"}
|
||||
{:else if topic === "nip-17-dms"}
|
||||
<p>
|
||||
<Anchor underline external href={nip44Url}>NIP 44</Anchor> is a new encryption standard for nostr,
|
||||
which along with <Anchor underline external href={nip17Url}>NIP 17</Anchor> improves upon the old
|
||||
NIP 04 direct messages standard by adding support for group chats and better metadata hiding.
|
||||
<Anchor underline external href={nip17Url}>NIP 17</Anchor> improves upon the old NIP 04 direct
|
||||
messages standard by adding support for group chats and better metadata hiding.
|
||||
</p>
|
||||
<p>
|
||||
In the past, a significant amount of information about private messages was public, event
|
||||
|
@ -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)
|
||||
|
@ -15,7 +15,7 @@
|
||||
import PersonSelect from "src/app/shared/PersonSelect.svelte"
|
||||
import {
|
||||
mention,
|
||||
getSettings,
|
||||
settings,
|
||||
publishSettings,
|
||||
searchTopics,
|
||||
userMutes,
|
||||
@ -23,12 +23,12 @@
|
||||
updateSingleton,
|
||||
} from "src/engine"
|
||||
|
||||
const settings = getSettings()
|
||||
const values = {...$settings}
|
||||
|
||||
const searchWords = q => pluck("name", $searchTopics(q))
|
||||
|
||||
const submit = () => {
|
||||
publishSettings(settings)
|
||||
publishSettings(values)
|
||||
updateSingleton(MUTES, () => mutedPubkeys.map(mention))
|
||||
|
||||
showInfo("Your preferences have been saved!")
|
||||
@ -50,20 +50,20 @@
|
||||
</div>
|
||||
<div class="flex w-full flex-col gap-8">
|
||||
<FieldInline label="Show likes on notes">
|
||||
<Toggle bind:value={settings.enable_reactions} />
|
||||
<Toggle bind:value={values.enable_reactions} />
|
||||
<p slot="info">
|
||||
Show how many likes and reactions a note received. Disabling this can reduce how much data {appName}
|
||||
uses.
|
||||
</p>
|
||||
</FieldInline>
|
||||
<FieldInline label="Show images and link previews">
|
||||
<Toggle bind:value={settings.show_media} />
|
||||
<Toggle bind:value={values.show_media} />
|
||||
<p slot="info">
|
||||
If enabled, {appName} will automatically show images and previews for embedded links.
|
||||
</p>
|
||||
</FieldInline>
|
||||
<FieldInline label="Hide sensitive content">
|
||||
<Toggle bind:value={settings.hide_sensitive} />
|
||||
<Toggle bind:value={values.hide_sensitive} />
|
||||
<p slot="info">
|
||||
If enabled, content flagged by the author as potentially sensitive will be hidden.
|
||||
</p>
|
||||
@ -71,9 +71,9 @@
|
||||
<Field>
|
||||
<div slot="label" class="flex justify-between">
|
||||
<strong>Minimum WoT score</strong>
|
||||
<div>{settings.min_wot_score}</div>
|
||||
<div>{values.min_wot_score}</div>
|
||||
</div>
|
||||
<Input type="range" bind:value={settings.min_wot_score} min={-10} max={10} />
|
||||
<Input type="range" bind:value={values.min_wot_score} min={-10} max={10} />
|
||||
<p slot="info">
|
||||
Select a minimum <Anchor underline modal href="/help/web-of-trust">web-of-trust</Anchor>
|
||||
score. Notes from accounts with a lower score will be automatically hidden.
|
||||
@ -86,7 +86,7 @@
|
||||
<Field label="Muted words and topics">
|
||||
<SearchSelect
|
||||
multiple
|
||||
bind:value={settings.muted_words}
|
||||
bind:value={values.muted_words}
|
||||
search={searchWords}
|
||||
termToItem={identity} />
|
||||
<p slot="info">Notes containing these words will be hidden by default.</p>
|
||||
|
@ -21,7 +21,7 @@
|
||||
hints,
|
||||
groupSharedKeys,
|
||||
relaySearch,
|
||||
deriveIsGroupMember,
|
||||
userIsGroupMember,
|
||||
groupAdminKeys,
|
||||
subscribe,
|
||||
LOAD_OPTS,
|
||||
@ -104,7 +104,7 @@
|
||||
prop("group"),
|
||||
sortBy(
|
||||
k => -k.created_at,
|
||||
$groupSharedKeys.filter(k => deriveIsGroupMember(k.group).get()),
|
||||
$groupSharedKeys.filter(k => $userIsGroupMember(k.group)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -9,14 +9,14 @@
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import {env, getSettings, publishSettings} from "src/engine"
|
||||
import {env, settings, publishSettings} from "src/engine"
|
||||
import SearchSelect from "src/partials/SearchSelect.svelte"
|
||||
import {fuzzy} from "src/util/misc"
|
||||
|
||||
const settings = getSettings()
|
||||
const values = {...$settings}
|
||||
|
||||
const submit = () => {
|
||||
publishSettings(settings)
|
||||
publishSettings(values)
|
||||
|
||||
showInfo("Your settings have been saved!")
|
||||
}
|
||||
@ -26,7 +26,7 @@
|
||||
const formatPercent = d => String(Math.round(d * 100))
|
||||
const parsePercent = p => parseInt(p) / 100
|
||||
|
||||
$: settings.relay_redundancy = Math.round(Math.log10(settings.relay_limit) * 4)
|
||||
$: values.relay_redundancy = Math.round(Math.log10(values.relay_limit) * 4)
|
||||
|
||||
document.title = "Settings"
|
||||
</script>
|
||||
@ -38,7 +38,7 @@
|
||||
</div>
|
||||
<div class="flex w-full flex-col gap-8">
|
||||
<Field label="Default zap amount">
|
||||
<Input bind:value={settings.default_zap}>
|
||||
<Input bind:value={values.default_zap}>
|
||||
<i slot="before" class="fa fa-bolt" />
|
||||
</Input>
|
||||
<p slot="info">The default amount of sats to use when sending a lightning tip.</p>
|
||||
@ -46,7 +46,7 @@
|
||||
<Field label="Platform zap split">
|
||||
<Input
|
||||
type="number"
|
||||
bind:value={settings.platform_zap_split}
|
||||
bind:value={values.platform_zap_split}
|
||||
format={formatPercent}
|
||||
parse={parsePercent}>
|
||||
<i slot="before" class="fa fa-percent" />
|
||||
@ -58,9 +58,9 @@
|
||||
<Field>
|
||||
<div slot="label" class="flex justify-between">
|
||||
<strong>Max relays per request</strong>
|
||||
<div>{settings.relay_limit} relays</div>
|
||||
<div>{values.relay_limit} relays</div>
|
||||
</div>
|
||||
<Input type="range" bind:value={settings.relay_limit} min={1} max={30} parse={parseInt} />
|
||||
<Input type="range" bind:value={values.relay_limit} min={1} max={30} parse={parseInt} />
|
||||
<p slot="info">
|
||||
This controls how many relays to max out at when loading feeds and event context. More is
|
||||
faster, but will require more bandwidth and processing power.
|
||||
@ -68,7 +68,7 @@
|
||||
</Field>
|
||||
{#if !$env.FORCE_GROUP && $env.PLATFORM_RELAYS.length === 0}
|
||||
<FieldInline label="Authenticate with relays">
|
||||
<Toggle bind:value={settings.auto_authenticate} />
|
||||
<Toggle bind:value={values.auto_authenticate} />
|
||||
<p slot="info">
|
||||
Allows {appName} to authenticate with relays that have access controls automatically.
|
||||
</p>
|
||||
@ -84,7 +84,7 @@
|
||||
<SearchSelect
|
||||
multiple
|
||||
search={searchUploadProviders}
|
||||
bind:value={settings.nip96_urls}
|
||||
bind:value={values.nip96_urls}
|
||||
termToItem={identity}>
|
||||
<div slot="item" let:item>
|
||||
<strong>{item}</strong>
|
||||
@ -92,7 +92,7 @@
|
||||
</SearchSelect>
|
||||
</Field>
|
||||
<Field label="Dufflepud URL">
|
||||
<Input bind:value={settings.dufflepud_url}>
|
||||
<Input bind:value={values.dufflepud_url}>
|
||||
<i slot="before" class="fa-solid fa-server" />
|
||||
</Input>
|
||||
<p slot="info">
|
||||
@ -104,7 +104,7 @@
|
||||
</p>
|
||||
</Field>
|
||||
<Field label="Imgproxy URL">
|
||||
<Input bind:value={settings.imgproxy_url}>
|
||||
<Input bind:value={values.imgproxy_url}>
|
||||
<i slot="before" class="fa-solid fa-image" />
|
||||
</Input>
|
||||
<p slot="info">
|
||||
@ -115,7 +115,7 @@
|
||||
</p>
|
||||
</Field>
|
||||
<Field label="Multiplextr URL">
|
||||
<Input bind:value={settings.multiplextr_url}>
|
||||
<Input bind:value={values.multiplextr_url}>
|
||||
<i slot="before" class="fa-solid fa-code-merge" />
|
||||
</Input>
|
||||
<p slot="info">
|
||||
@ -128,14 +128,14 @@
|
||||
</p>
|
||||
</Field>
|
||||
<FieldInline label="Report errors and analytics">
|
||||
<Toggle bind:value={settings.report_analytics} />
|
||||
<Toggle bind:value={values.report_analytics} />
|
||||
<p slot="info">
|
||||
Keep this enabled if you would like developers to be able to know what features are used,
|
||||
and to diagnose and fix bugs.
|
||||
</p>
|
||||
</FieldInline>
|
||||
<FieldInline label="Enable client fingerprinting">
|
||||
<Toggle bind:value={settings.enable_client_tag} />
|
||||
<Toggle bind:value={values.enable_client_tag} />
|
||||
<p slot="info">
|
||||
If this is turned on, public notes you create will have a "client" tag added. This helps
|
||||
with troubleshooting, and allows other people to find out about {appName}.
|
||||
|
@ -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,5 +1,5 @@
|
||||
import {fromPairs, randomId} from "@welshman/lib"
|
||||
import {FEED, Tags, getAddress} from "@welshman/util"
|
||||
import {FEED, Tags} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
feedFromTags,
|
||||
@ -10,7 +10,6 @@ import {
|
||||
isScopeFeed,
|
||||
} from "@welshman/feeds"
|
||||
import type {Feed as IFeed} from "@welshman/feeds"
|
||||
import {SearchHelper} from "src/util/misc"
|
||||
import {tryJson} from "src/util/misc"
|
||||
import type {PublishedList} from "./list"
|
||||
|
||||
@ -81,12 +80,6 @@ export const editFeed = (feed: PublishedFeed) => ({
|
||||
|
||||
export const displayFeed = (feed?: Feed) => feed?.title || "[no name]"
|
||||
|
||||
export class FeedSearch extends SearchHelper<PublishedFeed, string> {
|
||||
config = {keys: ["title", "description"]}
|
||||
getValue = (option: PublishedFeed) => getAddress(option.event)
|
||||
displayValue = (address: string) => displayFeed(this.getOption(address))
|
||||
}
|
||||
|
||||
export const isTopicFeed = f => isTagFeed(f) && f[1] === "#t"
|
||||
|
||||
export const isMentionFeed = f => isTagFeed(f) && f[1] === "#p"
|
||||
|
@ -30,10 +30,10 @@ export const readGroupMeta = (event: TrustedEvent) => {
|
||||
relays: event.tags.filter(nthEq(0, 'relay')),
|
||||
moderators: event.tags.filter(nthEq(0, 'p')),
|
||||
identifier: meta.d,
|
||||
name: meta.name,
|
||||
about: meta.about,
|
||||
banner: meta.banner,
|
||||
image: meta.image || meta.picture,
|
||||
name: meta.name || "",
|
||||
about: meta.about || "",
|
||||
banner: meta.banner || "",
|
||||
image: meta.image || meta.picture || "",
|
||||
listing_is_public: isSignedEvent(event),
|
||||
} as PublishedGroupMeta
|
||||
}
|
||||
|
@ -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,
|
||||
}))
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
PROFILE,
|
||||
MUTES,
|
||||
WRAP_NIP04,
|
||||
INBOX_RELAYS,
|
||||
} from "@welshman/util"
|
||||
import {Fetch, chunk, createMapOf, randomId, seconds, sleep, tryFunc} from "hurdak"
|
||||
import {
|
||||
@ -40,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"
|
||||
@ -52,10 +54,9 @@ import {
|
||||
loadOne,
|
||||
createAndPublish,
|
||||
deriveAdminKeyForGroup,
|
||||
deriveIsGroupMember,
|
||||
userIsGroupMember,
|
||||
deriveSharedKeyForGroup,
|
||||
displayProfileByPubkey,
|
||||
getSettings,
|
||||
env,
|
||||
getClientTags,
|
||||
groupAdminKeys,
|
||||
@ -81,6 +82,8 @@ import {
|
||||
zappers,
|
||||
getPlaintext,
|
||||
anonymous,
|
||||
mentionGroup,
|
||||
userRelayPolicies,
|
||||
} from "src/engine/state"
|
||||
import {loadHandle, loadZapper} from "src/engine/requests"
|
||||
|
||||
@ -312,9 +315,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
|
||||
@ -377,25 +380,27 @@ 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})
|
||||
}
|
||||
|
||||
export const publishToGroupsPrivately = async (addresses, template, {anonymous = false} = {}) => {
|
||||
const $userIsGroupMember = userIsGroupMember.get()
|
||||
|
||||
const events = []
|
||||
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:")) {
|
||||
throw new Error("Attempted to publish privately to an invalid address", address)
|
||||
}
|
||||
|
||||
if (!deriveIsGroupMember(address).get()) {
|
||||
if (!$userIsGroupMember(address)) {
|
||||
throw new Error("Attempted to publish privately to a group the user is not a member of")
|
||||
}
|
||||
|
||||
@ -490,7 +495,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(),
|
||||
@ -507,7 +512,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)],
|
||||
@ -524,13 +529,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)
|
||||
@ -541,6 +546,7 @@ export const publishCommunityMeta = (address, identifier, meta) => {
|
||||
tags: [
|
||||
["d", identifier],
|
||||
["name", meta.name],
|
||||
["about", meta.about],
|
||||
["description", meta.about],
|
||||
["banner", meta.banner],
|
||||
["picture", meta.image],
|
||||
@ -561,6 +567,7 @@ export const publishGroupMeta = (address, identifier, meta, listPublicly) => {
|
||||
["d", identifier],
|
||||
["name", meta.name],
|
||||
["about", meta.about],
|
||||
["description", meta.description],
|
||||
["banner", meta.banner],
|
||||
["picture", meta.image],
|
||||
["image", meta.image],
|
||||
@ -577,7 +584,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
|
||||
|
||||
@ -600,7 +607,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])
|
||||
@ -624,7 +631,7 @@ export const publishGroupExitRequest = address => {
|
||||
address,
|
||||
createEvent(26, {
|
||||
content: `${displayProfileByPubkey(pubkey.get())} is leaving the group`,
|
||||
tags: [...getClientTags(), ["a", address]],
|
||||
tags: [...getClientTags(), mentionGroup(address)],
|
||||
}),
|
||||
)
|
||||
}
|
||||
@ -633,7 +640,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(),
|
||||
})
|
||||
|
||||
@ -708,6 +715,7 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
|
||||
} else {
|
||||
const singleton = makeSingleton({kind})
|
||||
const publicTags = modifyTags(singleton.publicTags)
|
||||
|
||||
encryptable = createSingleton({...singleton, publicTags})
|
||||
}
|
||||
|
||||
@ -761,7 +769,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 {
|
||||
@ -769,8 +777,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) {
|
||||
@ -785,7 +811,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()) {
|
||||
@ -800,7 +829,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()) {
|
||||
@ -896,7 +925,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,
|
||||
})
|
||||
}
|
||||
@ -1006,8 +1035,8 @@ export const setAppData = async (d: string, data: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const publishSettings = (updates: Record<string, any>) =>
|
||||
setAppData(appDataKeys.USER_SETTINGS, {...getSettings(), ...updates})
|
||||
export const publishSettings = ($settings: Record<string, any>) =>
|
||||
setAppData(appDataKeys.USER_SETTINGS, $settings)
|
||||
|
||||
export const setSession = (k, data) => sessions.update($s => ($s[k] ? {...$s, [k]: data} : $s))
|
||||
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
topics,
|
||||
relays,
|
||||
deriveAdminKeyForGroup,
|
||||
deriveGroupStatus,
|
||||
getGroupStatus,
|
||||
getChannelId,
|
||||
getSession,
|
||||
groupAdminKeys,
|
||||
@ -71,7 +71,7 @@ projections.addHandler(24, (e: TrustedEvent) => {
|
||||
return
|
||||
}
|
||||
|
||||
const status = deriveGroupStatus(address).get()
|
||||
const status = getGroupStatus(getSession(recipient), address)
|
||||
|
||||
if (privkey) {
|
||||
const pubkey = getPublicKey(privkey)
|
||||
|
@ -36,7 +36,7 @@ import {LIST_KINDS} from "src/domain"
|
||||
import type {Zapper} from "src/engine/model"
|
||||
import {repository} from "src/engine/repository"
|
||||
import {
|
||||
deriveUserCircles,
|
||||
getUserCircles,
|
||||
getGroupReqInfo,
|
||||
getCommunityReqInfo,
|
||||
dvmRequest,
|
||||
@ -175,7 +175,7 @@ export const loadGroups = async (rawAddrs: string[], explicitRelays: string[] =
|
||||
|
||||
export const loadGroupMessages = (addresses?: string[]) => {
|
||||
const promises = []
|
||||
const addrs = addresses || deriveUserCircles().get()
|
||||
const addrs = addresses || getUserCircles(session.get())
|
||||
const [groupAddrs, communityAddrs] = partition(isGroupAddress, addrs)
|
||||
|
||||
for (const address of groupAddrs) {
|
||||
|
@ -4,6 +4,7 @@ import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
RELAYS,
|
||||
PROFILE,
|
||||
INBOX_RELAYS,
|
||||
HANDLER_INFORMATION,
|
||||
NAMED_BOOKMARKS,
|
||||
COMMUNITIES,
|
||||
@ -49,9 +50,9 @@ const getFiltersForKey = (key: string, authors: string[]) => {
|
||||
case "pubkey/lists":
|
||||
return [{authors, kinds: LIST_KINDS}]
|
||||
case "pubkey/feeds":
|
||||
return [{authors, kinds: [NAMED_BOOKMARKS, FEED]}]
|
||||
return [{authors, kinds: [NAMED_BOOKMARKS, FEED, FEEDS]}]
|
||||
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":
|
||||
|
@ -46,6 +46,7 @@ import {
|
||||
FEEDS,
|
||||
COMMUNITY,
|
||||
GROUP,
|
||||
INBOX_RELAYS,
|
||||
WRAP_NIP04,
|
||||
COMMUNITIES,
|
||||
READ_RECEIPT,
|
||||
@ -73,6 +74,9 @@ import {
|
||||
LOCAL_RELAY_URL,
|
||||
getFilterResultCardinality,
|
||||
isShareableRelayUrl,
|
||||
isReplaceable,
|
||||
isGroupAddress,
|
||||
isCommunityAddress,
|
||||
} from "@welshman/util"
|
||||
import type {Filter, RouterScenario, TrustedEvent, SignedEvent} from "@welshman/util"
|
||||
import {
|
||||
@ -98,6 +102,8 @@ import {
|
||||
repostKinds,
|
||||
noteKinds,
|
||||
reactionKinds,
|
||||
getRelayTags,
|
||||
getRelayTagValues,
|
||||
} from "src/util/nostr"
|
||||
import logger from "src/util/logger"
|
||||
import type {
|
||||
@ -111,12 +117,12 @@ import type {
|
||||
Handle,
|
||||
} from "src/domain"
|
||||
import {
|
||||
RelayMode,
|
||||
displayFeed,
|
||||
EDITABLE_LIST_KINDS,
|
||||
getSingletonValues,
|
||||
makeSingleton,
|
||||
makeRelayPoliciesFromTags,
|
||||
ListSearch,
|
||||
FeedSearch,
|
||||
profileHasName,
|
||||
readFeed,
|
||||
readList,
|
||||
@ -211,13 +217,22 @@ export const getFreshness = (key: string, value: any) =>
|
||||
export const setFreshness = (key: string, value: any, ts: number) =>
|
||||
freshness.set(getFreshnessKey(key, value), ts)
|
||||
|
||||
// Session and settings
|
||||
// Session, signing, encryption
|
||||
|
||||
export const getSession = pubkey => sessions.get()[pubkey]
|
||||
|
||||
export const getCurrentSession = () => sessions.get()[pubkey.get()]
|
||||
export const session = withGetter(derived([pubkey, sessions], ([$pk, $sessions]) => $sessions[$pk]))
|
||||
|
||||
export const getDefaultSettings = () => ({
|
||||
export const connect = withGetter(derived(session, getConnect))
|
||||
export const signer = withGetter(derived(session, getSigner))
|
||||
export const nip04 = withGetter(derived(session, getNip04))
|
||||
export const nip44 = withGetter(derived(session, getNip44))
|
||||
export const nip59 = withGetter(derived(session, getNip59))
|
||||
export const canSign = withGetter(derived(signer, $signer => $signer.isEnabled()))
|
||||
|
||||
// Settings
|
||||
|
||||
export const defaultSettings = {
|
||||
relay_limit: 10,
|
||||
relay_redundancy: 3,
|
||||
default_zap: 21,
|
||||
@ -234,11 +249,13 @@ export const getDefaultSettings = () => ({
|
||||
dufflepud_url: env.get().DUFFLEPUD_URL,
|
||||
multiplextr_url: env.get().MULTIPLEXTR_URL,
|
||||
platform_zap_split: env.get().PLATFORM_ZAP_SPLIT,
|
||||
})
|
||||
}
|
||||
|
||||
export const getSettings = () => ({...getDefaultSettings(), ...getSession(pubkey.get())?.settings})
|
||||
export const settings = withGetter(
|
||||
derived(session, $session => ({...defaultSettings, ...$session.settings})),
|
||||
)
|
||||
|
||||
export const getSetting = k => prop(k, getSettings())
|
||||
export const getSetting = k => prop(k, settings.get())
|
||||
|
||||
export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
|
||||
const base = getSetting("imgproxy_url")
|
||||
@ -266,19 +283,6 @@ export const dufflepud = (path: string) => {
|
||||
return `${base}/${path}`
|
||||
}
|
||||
|
||||
export const session = new Derived(
|
||||
[pubkey, sessions],
|
||||
([$pk, $sessions]: [string, Record<string, Session>]) => ($pk ? $sessions[$pk] : null),
|
||||
)
|
||||
|
||||
export const connect = session.derived(getConnect)
|
||||
export const signer = session.derived(getSigner)
|
||||
export const nip04 = session.derived(getNip04)
|
||||
export const nip44 = session.derived(getNip44)
|
||||
export const nip59 = session.derived(getNip59)
|
||||
export const canSign = signer.derived($signer => $signer.isEnabled())
|
||||
export const settings = derived(pubkey, getSettings)
|
||||
|
||||
// Plaintext
|
||||
|
||||
export const getPlaintext = (e: TrustedEvent) => plaintext.get()[e.id]
|
||||
@ -613,65 +617,6 @@ export const userMuteList = derived([muteListsByPubkey, pubkey], ([$m, $pk]) =>
|
||||
|
||||
export const userMutes = derived(userMuteList, l => getSingletonValues("p", l))
|
||||
|
||||
// Events
|
||||
|
||||
export const isEventMuted = withGetter(
|
||||
derived(
|
||||
[userMutes, userFollows, settings, pubkey],
|
||||
([$userMutes, $userFollows, $settings, $pubkey]) => {
|
||||
const words = $settings.muted_words
|
||||
const minWot = $settings.min_wot_score
|
||||
const regex =
|
||||
words.length > 0 ? new RegExp(`\\b(${words.map(w => w.toLowerCase()).join("|")})\\b`) : null
|
||||
|
||||
return (e: Partial<TrustedEvent>, strict = false) => {
|
||||
if (!$pubkey || e.pubkey === $pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tags = Tags.wrap(e.tags || [])
|
||||
const {roots, replies} = tags.ancestors()
|
||||
|
||||
if (
|
||||
find(
|
||||
t => $userMutes.has(t),
|
||||
[e.id, e.pubkey, ...roots.values().valueOf(), ...replies.values().valueOf()],
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (regex && e.content?.toLowerCase().match(regex)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!strict) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isGroupMember = tags
|
||||
.groups()
|
||||
.values()
|
||||
.some(a => deriveIsGroupMember(a).get())
|
||||
const isCommunityMember = tags
|
||||
.communities()
|
||||
.values()
|
||||
.some(a => false)
|
||||
const wotAdjustment = isCommunityMember || isGroupMember ? 1 : 0
|
||||
|
||||
if (
|
||||
!$userFollows.has(e.pubkey) &&
|
||||
getWotScore($pubkey, e.pubkey) < minWot - wotAdjustment
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// Channels
|
||||
|
||||
export const sortChannels = $channels =>
|
||||
@ -849,16 +794,17 @@ export const getGroupStatus = (session, address) =>
|
||||
(session?.groups?.[address] || {}) as GroupStatus
|
||||
|
||||
export const deriveGroupStatus = address =>
|
||||
session.derived($session => getGroupStatus($session, address))
|
||||
derived(session, $session => getGroupStatus($session, address))
|
||||
|
||||
export const getIsGroupMember = (session, address, includeRequests = false) => {
|
||||
const status = getGroupStatus(session, address)
|
||||
export const userIsGroupMember = withGetter(
|
||||
derived(session, $session => (address, includeRequests = false) => {
|
||||
const status = getGroupStatus($session, address)
|
||||
|
||||
if (address.startsWith("34550:")) {
|
||||
if (isCommunityAddress(address)) {
|
||||
return status.joined
|
||||
}
|
||||
|
||||
if (address.startsWith("35834:")) {
|
||||
if (isGroupAddress(address)) {
|
||||
if (includeRequests && status.access === GroupAccess.Requested) {
|
||||
return true
|
||||
}
|
||||
@ -867,19 +813,17 @@ export const getIsGroupMember = (session, address, includeRequests = false) => {
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const deriveIsGroupMember = (address, includeRequests = false) =>
|
||||
session.derived($session => getIsGroupMember($session, address, includeRequests))
|
||||
}),
|
||||
)
|
||||
|
||||
export const deriveGroupOptions = (defaultGroups = []) =>
|
||||
session.derived($session => {
|
||||
derived([session, userIsGroupMember], ([$session, $userIsGroupMember]) => {
|
||||
const options = []
|
||||
|
||||
for (const address of Object.keys($session?.groups || {})) {
|
||||
const group = groups.key(address).get()
|
||||
|
||||
if (group && deriveIsGroupMember(address).get()) {
|
||||
if (group && $userIsGroupMember(address)) {
|
||||
options.push(group)
|
||||
}
|
||||
}
|
||||
@ -891,22 +835,73 @@ export const deriveGroupOptions = (defaultGroups = []) =>
|
||||
return uniqBy(prop("address"), options)
|
||||
})
|
||||
|
||||
export const getUserCircles = (session: Session) =>
|
||||
Object.entries(session?.groups || {})
|
||||
.filter(([a, s]) => deriveIsGroupMember(a).get())
|
||||
export const getUserCircles = (session: Session) => {
|
||||
const $userIsGroupMember = userIsGroupMember.get()
|
||||
|
||||
return Object.entries(session?.groups || {})
|
||||
.filter(([a, s]) => $userIsGroupMember(a))
|
||||
.map(([a, s]) => a)
|
||||
}
|
||||
|
||||
export const deriveUserCircles = () => session.derived(getUserCircles)
|
||||
|
||||
export const getUserGroups = (session: Session) =>
|
||||
getUserCircles(session).filter(a => a.startsWith("35834:"))
|
||||
|
||||
export const deriveUserGroups = () => session.derived(getUserGroups)
|
||||
export const getUserGroups = (session: Session) => getUserCircles(session).filter(isGroupAddress)
|
||||
|
||||
export const getUserCommunities = (session: Session) =>
|
||||
getUserCircles(session).filter(a => a.startsWith("34550:"))
|
||||
getUserCircles(session).filter(isCommunityAddress)
|
||||
|
||||
export const deriveUserCommunities = () => session.derived(getUserCommunities)
|
||||
// Events
|
||||
|
||||
export const isEventMuted = withGetter(
|
||||
derived(
|
||||
[userMutes, userFollows, settings, pubkey, userIsGroupMember],
|
||||
([$userMutes, $userFollows, $settings, $pubkey, $userIsGroupMember]) => {
|
||||
const words = $settings.muted_words
|
||||
const minWot = $settings.min_wot_score
|
||||
const regex =
|
||||
words.length > 0 ? new RegExp(`\\b(${words.map(w => w.toLowerCase()).join("|")})\\b`) : null
|
||||
|
||||
return (e: Partial<TrustedEvent>, strict = false) => {
|
||||
if (!$pubkey || e.pubkey === $pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tags = Tags.wrap(e.tags || [])
|
||||
const {roots, replies} = tags.ancestors()
|
||||
|
||||
if (
|
||||
find(
|
||||
t => $userMutes.has(t),
|
||||
[e.id, e.pubkey, ...roots.values().valueOf(), ...replies.values().valueOf()],
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (regex && e.content?.toLowerCase().match(regex)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!strict) {
|
||||
return false
|
||||
}
|
||||
const isInGroup = tags.groups().values().some($userIsGroupMember)
|
||||
const isInCommunity = tags
|
||||
.communities()
|
||||
.values()
|
||||
.some(a => false)
|
||||
const wotAdjustment = isInCommunity || isInGroup ? 1 : 0
|
||||
|
||||
if (
|
||||
!$userFollows.has(e.pubkey) &&
|
||||
getWotScore($pubkey, e.pubkey) < minWot - wotAdjustment
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// Read receipts
|
||||
|
||||
@ -931,9 +926,9 @@ export const isSeen = derived(allReadReceipts, $m => e => $m.has(e.id))
|
||||
|
||||
// Notifications
|
||||
|
||||
export const notifications = derived(events, $events => {
|
||||
const $pubkey = pubkey.get()
|
||||
const $isEventMuted = isEventMuted.get()
|
||||
export const notifications = derived(
|
||||
[pubkey, events, isEventMuted],
|
||||
([$pubkey, $events, $isEventMuted]) => {
|
||||
const kinds = [...noteKinds, ...reactionKinds]
|
||||
|
||||
return Array.from(repository.query([{"#p": [$pubkey]}])).filter(
|
||||
@ -943,7 +938,8 @@ export const notifications = derived(events, $events => {
|
||||
!$isEventMuted(e) &&
|
||||
(e.kind !== 7 || isLike(e)),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export const unreadNotifications = derived([isSeen, notifications], ([$isSeen, $notifications]) => {
|
||||
const since = now() - seconds(30, "day")
|
||||
@ -954,14 +950,13 @@ export const unreadNotifications = derived([isSeen, notifications], ([$isSeen, $
|
||||
})
|
||||
|
||||
export const groupNotifications = new Derived(
|
||||
[session, events, groupRequests, groupAlerts, groupAdminKeys],
|
||||
[session, events, groupRequests, groupAlerts, groupAdminKeys, isEventMuted],
|
||||
x => x,
|
||||
)
|
||||
.throttle(3000)
|
||||
.derived(([$session, $events, $requests, $alerts, $adminKeys, $addresses]) => {
|
||||
.derived(([$session, $events, $requests, $alerts, $adminKeys, $addresses, $isEventMuted]) => {
|
||||
const addresses = new Set(getUserCircles($session))
|
||||
const adminPubkeys = new Set($adminKeys.map(k => k.pubkey))
|
||||
const $isEventMuted = isEventMuted.get()
|
||||
|
||||
const shouldSkip = e => {
|
||||
const context = e.tags.filter(t => t[0] === "a")
|
||||
@ -1105,6 +1100,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 +1123,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 +1134,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))
|
||||
const policiesByUrl = new Map()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 (!policies.has(event.pubkey)) {
|
||||
policies.set(event.pubkey, policy)
|
||||
if (!policiesByUrlByPubkey.has(event.pubkey)) {
|
||||
policiesByUrlByPubkey.set(event.pubkey, indexBy(prop("url"), policy))
|
||||
}
|
||||
}
|
||||
|
||||
return policies
|
||||
}),
|
||||
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 +1192,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 =>
|
||||
@ -1297,6 +1341,20 @@ export const feedFavorites = deriveEventsMapped<PublishedSingleton>({
|
||||
),
|
||||
})
|
||||
|
||||
export const feedFavoritesByAddress = withGetter(
|
||||
derived(feedFavorites, $feedFavorites => {
|
||||
const $feedFavoritesByAddress = new Map<string, PublishedSingleton[]>()
|
||||
|
||||
for (const singleton of $feedFavorites) {
|
||||
for (const address of getSingletonValues("a", singleton)) {
|
||||
pushToMapKey($feedFavoritesByAddress, address, singleton)
|
||||
}
|
||||
}
|
||||
|
||||
return $feedFavoritesByAddress
|
||||
}),
|
||||
)
|
||||
|
||||
export const userFeedFavorites = derived(
|
||||
[feedFavorites, pubkey],
|
||||
([$singletons, $pubkey]: [PublishedSingleton[], string]) =>
|
||||
@ -1310,6 +1368,35 @@ export const userFavoritedFeeds = derived(userFeedFavorites, $singleton =>
|
||||
.map(readFeed),
|
||||
)
|
||||
|
||||
export class FeedSearch extends SearchHelper<PublishedFeed, string> {
|
||||
getSearch = () => {
|
||||
const $feedFavoritesByAddress = feedFavoritesByAddress.get()
|
||||
const getScore = feed => $feedFavoritesByAddress.get(getAddress(feed.event))?.length || 0
|
||||
const options = this.options.map(feed => ({feed, score: getScore(feed)}))
|
||||
const fuse = new Fuse(options, {
|
||||
keys: ["feed.title", "feed.description"],
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
})
|
||||
|
||||
return (term: string) => {
|
||||
if (!term) {
|
||||
return sortBy(item => -item.score, options).map(item => item.feed)
|
||||
}
|
||||
|
||||
return doPipe(fuse.search(term), [
|
||||
results =>
|
||||
sortBy((r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100), results),
|
||||
results => results.map((r: any) => r.item.feed),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
getValue = (option: PublishedFeed) => getAddress(option.event)
|
||||
|
||||
displayValue = (address: string) => displayFeed(this.getOption(address))
|
||||
}
|
||||
|
||||
export const feedSearch = derived(feeds, $feeds => new FeedSearch($feeds))
|
||||
|
||||
export const listFeeds = deriveEventsMapped<PublishedListFeed>({
|
||||
@ -1622,7 +1709,7 @@ export const subscribePersistent = (request: MySubscribeRequest) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const LOAD_OPTS = {timeout: 3000, closeOnEose: true}
|
||||
export const LOAD_OPTS = {timeout: 5000, closeOnEose: true}
|
||||
|
||||
export const load = (request: MySubscribeRequest) =>
|
||||
new Promise(resolve => {
|
||||
@ -1743,8 +1830,29 @@ 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",
|
||||
address,
|
||||
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 = []
|
||||
@ -1819,7 +1927,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())
|
||||
}
|
||||
|
||||
@ -1835,7 +1943,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())
|
||||
}
|
||||
|
||||
|
@ -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 useNip17 =
|
||||
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, useNip17)
|
||||
|
||||
stickToBottom()
|
||||
}
|
||||
@ -159,18 +161,18 @@
|
||||
</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={useNip17} />
|
||||
<small>
|
||||
Send messages using
|
||||
<Popover class="inline">
|
||||
<span slot="trigger" class="cursor-pointer underline">NIP 44</span>
|
||||
<span slot="trigger" class="cursor-pointer underline">NIP 17</span>
|
||||
<div slot="tooltip" class="flex flex-col gap-2">
|
||||
<p>
|
||||
When enabled, Coracle will use 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
|
||||
href="/help/nip-17-dms">here</Anchor
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
|
Loading…
Reference in New Issue
Block a user