Compare commits

...

7 Commits

Author SHA1 Message Date
Jon Staab
abd306ccac Fix stale settings getting used 2024-06-21 15:47:01 -07:00
Jon Staab
b7f6d4b8be Hide low wot replies 2024-06-21 14:20:22 -07:00
Jon Staab
d7e808d977 Bump welshman/util 2024-06-21 14:02:28 -07:00
Jon Staab
5cbdda2438 Increase timeout so gleasonator works 2024-06-21 13:54:23 -07:00
Jon Staab
01e6c663fb refer to nip 17 rather than 44 2024-06-21 13:46:35 -07:00
Jon Staab
4708dc3013 Fix group-related bugs 2024-06-21 10:38:10 -07:00
Jon Staab
2e3df2838d Allow dismissing zap prompt 2024-06-21 10:25:50 -07:00
22 changed files with 210 additions and 212 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -59,7 +59,7 @@
"@welshman/feeds": "^0.0.12", "@welshman/feeds": "^0.0.12",
"@welshman/lib": "^0.0.10", "@welshman/lib": "^0.0.10",
"@welshman/net": "^0.0.14", "@welshman/net": "^0.0.14",
"@welshman/util": "^0.0.15", "@welshman/util": "^0.0.16",
"bowser": "^2.11.0", "bowser": "^2.11.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {writable, hash} from "@welshman/lib" import {seconds} from "hurdak"
import {writable, now, hash} from "@welshman/lib"
import {createScroller, synced} from "src/util/misc" import {createScroller, synced} from "src/util/misc"
import {fly, fade} from "src/util/transition" import {fly, fade} from "src/util/transition"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
@ -28,6 +29,8 @@
const splits = [["zap", $env.PLATFORM_PUBKEY, "", "1"]] const splits = [["zap", $env.PLATFORM_PUBKEY, "", "1"]]
const promptDismissed = synced("feed/promptDismissed", 0)
const shouldHideReplies = showControls ? synced("Feed.shouldHideReplies", false) : writable(false) const shouldHideReplies = showControls ? synced("Feed.shouldHideReplies", false) : writable(false)
const reload = async () => { const reload = async () => {
@ -103,12 +106,19 @@
{anchor} {anchor}
{note} /> {note} />
</div> </div>
{#if i > 20 && parseInt(hash(note.id)) % 100 === 0} {#if i > 20 && parseInt(hash(note.id)) % 100 === 0 && $promptDismissed < now() - seconds(7, "day")}
<Card class="flex items-center justify-between"> <Card class="group flex items-center justify-between">
<p class="text-xl">Enjoying Coracle?</p> <p class="text-xl">Enjoying Coracle?</p>
<Anchor modal button accent href={router.at("zap").qp({splits}).toString()}> <div class="flex gap-2">
Zap the developer <Anchor
</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> </Card>
{/if} {/if}
{/each} {/each}

View File

@ -8,8 +8,10 @@
const meta = deriveGroupMeta(address) const meta = deriveGroupMeta(address)
</script> </script>
<NoteContentKind1 {#key $meta?.about}
note={{content: $meta?.about || ""}} <NoteContentKind1
minLength={100} note={{content: $meta?.about || ""}}
maxLength={140} minLength={100}
showEntire={!truncate} /> maxLength={140}
showEntire={!truncate} />
{/key}

View File

@ -10,7 +10,7 @@
deriveGroup, deriveGroup,
deriveGroupStatus, deriveGroupStatus,
deriveAdminKeyForGroup, deriveAdminKeyForGroup,
deriveUserCommunities, getUserCommunities,
} from "src/engine" } from "src/engine"
export let address 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> </script>
<div class="flex items-center gap-3" on:click|stopPropagation> <div class="flex items-center gap-3" on:click|stopPropagation>

View File

@ -114,7 +114,7 @@
$: reply = tags.parent() $: reply = tags.parent()
$: root = tags.root() $: root = tags.root()
$: muted = !showMuted && $isEventMuted(event, true) $: muted = !showMuted && $isEventMuted(event)
// Find children in our context // Find children in our context
$: children = context.filter(e => isChildOf(e, event)) $: children = context.filter(e => isChildOf(e, event))
@ -130,7 +130,7 @@
visibleReplies = [] visibleReplies = []
for (const e of replies) { for (const e of replies) {
if ($isEventMuted(e)) { if ($isEventMuted(e, true)) {
mutedReplies.push(e) mutedReplies.push(e)
} else if (collapsed) { } else if (collapsed) {
hiddenReplies.push(e) hiddenReplies.push(e)

View File

@ -53,7 +53,7 @@
unmuteNote, unmuteNote,
muteNote, muteNote,
deriveHandlersForKind, deriveHandlersForKind,
deriveIsGroupMember, userIsGroupMember,
publishToZeroOrMoreGroups, publishToZeroOrMoreGroups,
deleteEvent, deleteEvent,
getSetting, getSetting,
@ -192,12 +192,12 @@
window.open(templateTag[1].replace("<bech32>", entity)) window.open(templateTag[1].replace("<bech32>", entity))
} }
const groupOptions = session.derived($session => { const groupOptions = derived(session, $session => {
const options = [] const options = []
for (const addr of Object.keys($session?.groups || {})) { for (const addr of Object.keys($session?.groups || {})) {
const group = groups.key(addr).get() const group = groups.key(addr).get()
const isMember = deriveIsGroupMember(addr).get() const isMember = $userIsGroupMember(addr)
if (group && isMember && addr !== address) { if (group && isMember && addr !== address) {
options.push(group) options.push(group)
@ -212,9 +212,7 @@
let handlersShown = false let handlersShown = false
$: disableActions = $: disableActions =
!$canSign || !$canSign || ($muted && !showMuted) || (note.wrap && address && !$userIsGroupMember(address))
($muted && !showMuted) ||
(note.wrap && address && !deriveIsGroupMember(address).get())
$: like = likes.find(e => e.pubkey === $session?.pubkey) $: like = likes.find(e => e.pubkey === $session?.pubkey)
$: $likesCount = likes.length $: $likesCount = likes.length
$: zap = zaps.find(e => e.request.pubkey === $session?.pubkey) $: zap = zaps.find(e => e.request.pubkey === $session?.pubkey)

View File

@ -82,7 +82,7 @@
{#if muted} {#if muted}
<p class="mb-1 py-24 text-center text-neutral-600"> <p class="mb-1 py-24 text-center text-neutral-600">
You have hidden this note. You have hidden this note.
<Anchor class="underline" on:click={unmute}>Show</Anchor> <Anchor class="underline" stopPropagation on:click={unmute}>Show</Anchor>
</p> </p>
{:else} {:else}
{#if !isGroup} {#if !isGroup}

View File

@ -37,9 +37,9 @@
const getContent = e => (e.kind === 4 ? ensureMessagePlaintext(e) : e.content) || "" 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 we don't have nip44 support, just send a legacy message
if (!$nip44.isEnabled() || !useNip44) { if (!$nip44.isEnabled() || !useNip17) {
return sendLegacyMessage(channelId, content) return sendLegacyMessage(channelId, content)
} }

View File

@ -9,6 +9,7 @@
import { import {
env, env,
pubkey, pubkey,
session,
initGroup, initGroup,
publishGroupMeta, publishGroupMeta,
publishGroupInvites, publishGroupInvites,
@ -16,7 +17,7 @@
publishCommunityMeta, publishCommunityMeta,
publishCommunitiesList, publishCommunitiesList,
publishGroupMembers, publishGroupMembers,
deriveUserCommunities, getUserCommunities,
} from "src/engine" } from "src/engine"
import {router} from "src/app/util/router" import {router} from "src/app/util/router"
@ -50,7 +51,7 @@
if (kind === COMMUNITY) { if (kind === COMMUNITY) {
await publishCommunityMeta(address, identifier, meta) await publishCommunityMeta(address, identifier, meta)
await publishCommunitiesList(deriveUserCommunities().get().concat(address)) await publishCommunitiesList(getUserCommunities(session.get()).concat(address))
} else { } else {
await publishGroupMeta(address, identifier, meta, listing_is_public) await publishGroupMeta(address, identifier, meta, listing_is_public)
await publishGroupMembers(address, "set", members) await publishGroupMembers(address, "set", members)

View File

@ -23,7 +23,7 @@
deriveGroupMeta, deriveGroupMeta,
deriveAdminKeyForGroup, deriveAdminKeyForGroup,
deriveSharedKeyForGroup, deriveSharedKeyForGroup,
deriveIsGroupMember, userIsGroupMember,
deriveGroupStatus, deriveGroupStatus,
loadGroups, loadGroups,
loadGroupMessages, loadGroupMessages,
@ -38,7 +38,6 @@
const group = deriveGroup(address) const group = deriveGroup(address)
const meta = deriveGroupMeta(address) const meta = deriveGroupMeta(address)
const status = deriveGroupStatus(address) const status = deriveGroupStatus(address)
const isGroupMember = deriveIsGroupMember(address)
const sharedKey = deriveSharedKeyForGroup(address) const sharedKey = deriveSharedKeyForGroup(address)
const adminKey = deriveAdminKeyForGroup(address) const adminKey = deriveAdminKeyForGroup(address)
const requests = groupRequests.derived(requests => const requests = groupRequests.derived(requests =>
@ -56,7 +55,7 @@
let tabs let tabs
$: key = $group && $isGroupMember $: key = $group && $userIsGroupMember(address)
$: { $: {
if (key) { if (key) {

View File

@ -15,7 +15,7 @@
groups, groups,
loadGiftWraps, loadGiftWraps,
loadGroupMessages, loadGroupMessages,
deriveIsGroupMember, userIsGroupMember,
updateCurrentSession, updateCurrentSession,
communityListsByAddress, communityListsByAddress,
searchGroupMeta, searchGroupMeta,
@ -26,7 +26,7 @@
limit += 20 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)) const userGroupMeta = derived(groupMeta, filter(userIsMember))

View File

@ -6,8 +6,7 @@
export let topic export let topic
const topics = ["web-of-trust", "nip-44-dms"] const topics = ["web-of-trust", "nip-17-dms"]
const nip44Url = "https://github.com/nostr-protocol/nips/blob/master/44.md"
const nip17Url = "https://github.com/nostr-protocol/nips/blob/master/17.md" const nip17Url = "https://github.com/nostr-protocol/nips/blob/master/17.md"
</script> </script>
@ -29,11 +28,10 @@
You can set a minimum web of trust score on your content settings page, which will 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. automatically mute anyone with a lower score than your threshold.
</p> </p>
{:else if topic === "nip-44-dms"} {:else if topic === "nip-17-dms"}
<p> <p>
<Anchor underline external href={nip44Url}>NIP 44</Anchor> is a new encryption standard for nostr, <Anchor underline external href={nip17Url}>NIP 17</Anchor> improves upon the old NIP 04 direct
which along with <Anchor underline external href={nip17Url}>NIP 17</Anchor> improves upon the old messages standard by adding support for group chats and better metadata hiding.
NIP 04 direct messages standard by adding support for group chats and better metadata hiding.
</p> </p>
<p> <p>
In the past, a significant amount of information about private messages was public, event In the past, a significant amount of information about private messages was public, event

View File

@ -15,7 +15,7 @@
import PersonSelect from "src/app/shared/PersonSelect.svelte" import PersonSelect from "src/app/shared/PersonSelect.svelte"
import { import {
mention, mention,
getSettings, settings,
publishSettings, publishSettings,
searchTopics, searchTopics,
userMutes, userMutes,
@ -23,12 +23,12 @@
updateSingleton, updateSingleton,
} from "src/engine" } from "src/engine"
const settings = getSettings() const values = {...$settings}
const searchWords = q => pluck("name", $searchTopics(q)) const searchWords = q => pluck("name", $searchTopics(q))
const submit = () => { const submit = () => {
publishSettings(settings) publishSettings(values)
updateSingleton(MUTES, () => mutedPubkeys.map(mention)) updateSingleton(MUTES, () => mutedPubkeys.map(mention))
showInfo("Your preferences have been saved!") showInfo("Your preferences have been saved!")
@ -50,20 +50,20 @@
</div> </div>
<div class="flex w-full flex-col gap-8"> <div class="flex w-full flex-col gap-8">
<FieldInline label="Show likes on notes"> <FieldInline label="Show likes on notes">
<Toggle bind:value={settings.enable_reactions} /> <Toggle bind:value={values.enable_reactions} />
<p slot="info"> <p slot="info">
Show how many likes and reactions a note received. Disabling this can reduce how much data {appName} Show how many likes and reactions a note received. Disabling this can reduce how much data {appName}
uses. uses.
</p> </p>
</FieldInline> </FieldInline>
<FieldInline label="Show images and link previews"> <FieldInline label="Show images and link previews">
<Toggle bind:value={settings.show_media} /> <Toggle bind:value={values.show_media} />
<p slot="info"> <p slot="info">
If enabled, {appName} will automatically show images and previews for embedded links. If enabled, {appName} will automatically show images and previews for embedded links.
</p> </p>
</FieldInline> </FieldInline>
<FieldInline label="Hide sensitive content"> <FieldInline label="Hide sensitive content">
<Toggle bind:value={settings.hide_sensitive} /> <Toggle bind:value={values.hide_sensitive} />
<p slot="info"> <p slot="info">
If enabled, content flagged by the author as potentially sensitive will be hidden. If enabled, content flagged by the author as potentially sensitive will be hidden.
</p> </p>
@ -71,9 +71,9 @@
<Field> <Field>
<div slot="label" class="flex justify-between"> <div slot="label" class="flex justify-between">
<strong>Minimum WoT score</strong> <strong>Minimum WoT score</strong>
<div>{settings.min_wot_score}</div> <div>{values.min_wot_score}</div>
</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"> <p slot="info">
Select a minimum <Anchor underline modal href="/help/web-of-trust">web-of-trust</Anchor> 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. score. Notes from accounts with a lower score will be automatically hidden.
@ -86,7 +86,7 @@
<Field label="Muted words and topics"> <Field label="Muted words and topics">
<SearchSelect <SearchSelect
multiple multiple
bind:value={settings.muted_words} bind:value={values.muted_words}
search={searchWords} search={searchWords}
termToItem={identity} /> termToItem={identity} />
<p slot="info">Notes containing these words will be hidden by default.</p> <p slot="info">Notes containing these words will be hidden by default.</p>

View File

@ -21,7 +21,7 @@
hints, hints,
groupSharedKeys, groupSharedKeys,
relaySearch, relaySearch,
deriveIsGroupMember, userIsGroupMember,
groupAdminKeys, groupAdminKeys,
subscribe, subscribe,
LOAD_OPTS, LOAD_OPTS,
@ -104,7 +104,7 @@
prop("group"), prop("group"),
sortBy( sortBy(
k => -k.created_at, k => -k.created_at,
$groupSharedKeys.filter(k => deriveIsGroupMember(k.group).get()), $groupSharedKeys.filter(k => $userIsGroupMember(k.group)),
), ),
), ),
) )

View File

@ -9,14 +9,14 @@
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.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 SearchSelect from "src/partials/SearchSelect.svelte"
import {fuzzy} from "src/util/misc" import {fuzzy} from "src/util/misc"
const settings = getSettings() const values = {...$settings}
const submit = () => { const submit = () => {
publishSettings(settings) publishSettings(values)
showInfo("Your settings have been saved!") showInfo("Your settings have been saved!")
} }
@ -26,7 +26,7 @@
const formatPercent = d => String(Math.round(d * 100)) const formatPercent = d => String(Math.round(d * 100))
const parsePercent = p => parseInt(p) / 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" document.title = "Settings"
</script> </script>
@ -38,7 +38,7 @@
</div> </div>
<div class="flex w-full flex-col gap-8"> <div class="flex w-full flex-col gap-8">
<Field label="Default zap amount"> <Field label="Default zap amount">
<Input bind:value={settings.default_zap}> <Input bind:value={values.default_zap}>
<i slot="before" class="fa fa-bolt" /> <i slot="before" class="fa fa-bolt" />
</Input> </Input>
<p slot="info">The default amount of sats to use when sending a lightning tip.</p> <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"> <Field label="Platform zap split">
<Input <Input
type="number" type="number"
bind:value={settings.platform_zap_split} bind:value={values.platform_zap_split}
format={formatPercent} format={formatPercent}
parse={parsePercent}> parse={parsePercent}>
<i slot="before" class="fa fa-percent" /> <i slot="before" class="fa fa-percent" />
@ -58,9 +58,9 @@
<Field> <Field>
<div slot="label" class="flex justify-between"> <div slot="label" class="flex justify-between">
<strong>Max relays per request</strong> <strong>Max relays per request</strong>
<div>{settings.relay_limit} relays</div> <div>{values.relay_limit} relays</div>
</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"> <p slot="info">
This controls how many relays to max out at when loading feeds and event context. More is 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. faster, but will require more bandwidth and processing power.
@ -68,7 +68,7 @@
</Field> </Field>
{#if !$env.FORCE_GROUP && $env.PLATFORM_RELAYS.length === 0} {#if !$env.FORCE_GROUP && $env.PLATFORM_RELAYS.length === 0}
<FieldInline label="Authenticate with relays"> <FieldInline label="Authenticate with relays">
<Toggle bind:value={settings.auto_authenticate} /> <Toggle bind:value={values.auto_authenticate} />
<p slot="info"> <p slot="info">
Allows {appName} to authenticate with relays that have access controls automatically. Allows {appName} to authenticate with relays that have access controls automatically.
</p> </p>
@ -84,7 +84,7 @@
<SearchSelect <SearchSelect
multiple multiple
search={searchUploadProviders} search={searchUploadProviders}
bind:value={settings.nip96_urls} bind:value={values.nip96_urls}
termToItem={identity}> termToItem={identity}>
<div slot="item" let:item> <div slot="item" let:item>
<strong>{item}</strong> <strong>{item}</strong>
@ -92,7 +92,7 @@
</SearchSelect> </SearchSelect>
</Field> </Field>
<Field label="Dufflepud URL"> <Field label="Dufflepud URL">
<Input bind:value={settings.dufflepud_url}> <Input bind:value={values.dufflepud_url}>
<i slot="before" class="fa-solid fa-server" /> <i slot="before" class="fa-solid fa-server" />
</Input> </Input>
<p slot="info"> <p slot="info">
@ -104,7 +104,7 @@
</p> </p>
</Field> </Field>
<Field label="Imgproxy URL"> <Field label="Imgproxy URL">
<Input bind:value={settings.imgproxy_url}> <Input bind:value={values.imgproxy_url}>
<i slot="before" class="fa-solid fa-image" /> <i slot="before" class="fa-solid fa-image" />
</Input> </Input>
<p slot="info"> <p slot="info">
@ -115,7 +115,7 @@
</p> </p>
</Field> </Field>
<Field label="Multiplextr URL"> <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" /> <i slot="before" class="fa-solid fa-code-merge" />
</Input> </Input>
<p slot="info"> <p slot="info">
@ -128,14 +128,14 @@
</p> </p>
</Field> </Field>
<FieldInline label="Report errors and analytics"> <FieldInline label="Report errors and analytics">
<Toggle bind:value={settings.report_analytics} /> <Toggle bind:value={values.report_analytics} />
<p slot="info"> <p slot="info">
Keep this enabled if you would like developers to be able to know what features are used, Keep this enabled if you would like developers to be able to know what features are used,
and to diagnose and fix bugs. and to diagnose and fix bugs.
</p> </p>
</FieldInline> </FieldInline>
<FieldInline label="Enable client fingerprinting"> <FieldInline label="Enable client fingerprinting">
<Toggle bind:value={settings.enable_client_tag} /> <Toggle bind:value={values.enable_client_tag} />
<p slot="info"> <p slot="info">
If this is turned on, public notes you create will have a "client" tag added. This helps 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}. with troubleshooting, and allows other people to find out about {appName}.

View File

@ -30,10 +30,10 @@ export const readGroupMeta = (event: TrustedEvent) => {
relays: event.tags.filter(nthEq(0, 'relay')), relays: event.tags.filter(nthEq(0, 'relay')),
moderators: event.tags.filter(nthEq(0, 'p')), moderators: event.tags.filter(nthEq(0, 'p')),
identifier: meta.d, identifier: meta.d,
name: meta.name, name: meta.name || "",
about: meta.about, about: meta.about || "",
banner: meta.banner, banner: meta.banner || "",
image: meta.image || meta.picture, image: meta.image || meta.picture || "",
listing_is_public: isSignedEvent(event), listing_is_public: isSignedEvent(event),
} as PublishedGroupMeta } as PublishedGroupMeta
} }

View File

@ -54,10 +54,9 @@ import {
loadOne, loadOne,
createAndPublish, createAndPublish,
deriveAdminKeyForGroup, deriveAdminKeyForGroup,
deriveIsGroupMember, userIsGroupMember,
deriveSharedKeyForGroup, deriveSharedKeyForGroup,
displayProfileByPubkey, displayProfileByPubkey,
getSettings,
env, env,
getClientTags, getClientTags,
groupAdminKeys, groupAdminKeys,
@ -388,6 +387,8 @@ export const publishToGroupsPublicly = async (addresses, template, {anonymous =
} }
export const publishToGroupsPrivately = async (addresses, template, {anonymous = false} = {}) => { export const publishToGroupsPrivately = async (addresses, template, {anonymous = false} = {}) => {
const $userIsGroupMember = userIsGroupMember.get()
const events = [] const events = []
const pubs = [] const pubs = []
for (const address of addresses) { for (const address of addresses) {
@ -399,7 +400,7 @@ export const publishToGroupsPrivately = async (addresses, template, {anonymous =
throw new Error("Attempted to publish privately to an invalid address", address) 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") throw new Error("Attempted to publish privately to a group the user is not a member of")
} }
@ -545,6 +546,7 @@ export const publishCommunityMeta = (address, identifier, meta) => {
tags: [ tags: [
["d", identifier], ["d", identifier],
["name", meta.name], ["name", meta.name],
["about", meta.about],
["description", meta.about], ["description", meta.about],
["banner", meta.banner], ["banner", meta.banner],
["picture", meta.image], ["picture", meta.image],
@ -565,6 +567,7 @@ export const publishGroupMeta = (address, identifier, meta, listPublicly) => {
["d", identifier], ["d", identifier],
["name", meta.name], ["name", meta.name],
["about", meta.about], ["about", meta.about],
["description", meta.description],
["banner", meta.banner], ["banner", meta.banner],
["picture", meta.image], ["picture", meta.image],
["image", meta.image], ["image", meta.image],
@ -690,9 +693,7 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
// If we don't have a recent version loaded, re-fetch to avoid dropping updates // If we don't have a recent version loaded, re-fetch to avoid dropping updates
if ((event?.created_at || 0) < now() - seconds(5, "minute")) { if ((event?.created_at || 0) < now() - seconds(5, "minute")) {
console.log("loading")
const loadedEvent = await loadOne({relays: hints.User().getUrls(), filters}) const loadedEvent = await loadOne({relays: hints.User().getUrls(), filters})
console.log("loaded", loadedEvent)
if ((loadedEvent?.created_at || 0) > (event?.created_at || 0)) { if ((loadedEvent?.created_at || 0) > (event?.created_at || 0)) {
event = loadedEvent event = loadedEvent
@ -714,15 +715,12 @@ export const updateSingleton = async (kind: number, modifyTags: ModifyTags) => {
} else { } else {
const singleton = makeSingleton({kind}) const singleton = makeSingleton({kind})
const publicTags = modifyTags(singleton.publicTags) const publicTags = modifyTags(singleton.publicTags)
encryptable = createSingleton({...singleton, publicTags}) encryptable = createSingleton({...singleton, publicTags})
} }
console.log(1)
const template = await encryptable.reconcile(encrypt) const template = await encryptable.reconcile(encrypt)
console.log(2, template)
await createAndPublish({...template, content, relays}) await createAndPublish({...template, content, relays})
} }
@ -1037,8 +1035,8 @@ export const setAppData = async (d: string, data: any) => {
} }
} }
export const publishSettings = (updates: Record<string, any>) => export const publishSettings = ($settings: Record<string, any>) =>
setAppData(appDataKeys.USER_SETTINGS, {...getSettings(), ...updates}) setAppData(appDataKeys.USER_SETTINGS, $settings)
export const setSession = (k, data) => sessions.update($s => ($s[k] ? {...$s, [k]: data} : $s)) export const setSession = (k, data) => sessions.update($s => ($s[k] ? {...$s, [k]: data} : $s))

View File

@ -22,7 +22,7 @@ import {
topics, topics,
relays, relays,
deriveAdminKeyForGroup, deriveAdminKeyForGroup,
deriveGroupStatus, getGroupStatus,
getChannelId, getChannelId,
getSession, getSession,
groupAdminKeys, groupAdminKeys,
@ -71,7 +71,7 @@ projections.addHandler(24, (e: TrustedEvent) => {
return return
} }
const status = deriveGroupStatus(address).get() const status = getGroupStatus(getSession(recipient), address)
if (privkey) { if (privkey) {
const pubkey = getPublicKey(privkey) const pubkey = getPublicKey(privkey)

View File

@ -36,7 +36,7 @@ import {LIST_KINDS} from "src/domain"
import type {Zapper} from "src/engine/model" import type {Zapper} from "src/engine/model"
import {repository} from "src/engine/repository" import {repository} from "src/engine/repository"
import { import {
deriveUserCircles, getUserCircles,
getGroupReqInfo, getGroupReqInfo,
getCommunityReqInfo, getCommunityReqInfo,
dvmRequest, dvmRequest,
@ -175,7 +175,7 @@ export const loadGroups = async (rawAddrs: string[], explicitRelays: string[] =
export const loadGroupMessages = (addresses?: string[]) => { export const loadGroupMessages = (addresses?: string[]) => {
const promises = [] const promises = []
const addrs = addresses || deriveUserCircles().get() const addrs = addresses || getUserCircles(session.get())
const [groupAddrs, communityAddrs] = partition(isGroupAddress, addrs) const [groupAddrs, communityAddrs] = partition(isGroupAddress, addrs)
for (const address of groupAddrs) { for (const address of groupAddrs) {

View File

@ -75,6 +75,8 @@ import {
getFilterResultCardinality, getFilterResultCardinality,
isShareableRelayUrl, isShareableRelayUrl,
isReplaceable, isReplaceable,
isGroupAddress,
isCommunityAddress,
} from "@welshman/util" } from "@welshman/util"
import type {Filter, RouterScenario, TrustedEvent, SignedEvent} from "@welshman/util" import type {Filter, RouterScenario, TrustedEvent, SignedEvent} from "@welshman/util"
import { import {
@ -215,13 +217,22 @@ export const getFreshness = (key: string, value: any) =>
export const setFreshness = (key: string, value: any, ts: number) => export const setFreshness = (key: string, value: any, ts: number) =>
freshness.set(getFreshnessKey(key, value), ts) freshness.set(getFreshnessKey(key, value), ts)
// Session and settings // Session, signing, encryption
export const getSession = pubkey => sessions.get()[pubkey] 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_limit: 10,
relay_redundancy: 3, relay_redundancy: 3,
default_zap: 21, default_zap: 21,
@ -238,11 +249,13 @@ export const getDefaultSettings = () => ({
dufflepud_url: env.get().DUFFLEPUD_URL, dufflepud_url: env.get().DUFFLEPUD_URL,
multiplextr_url: env.get().MULTIPLEXTR_URL, multiplextr_url: env.get().MULTIPLEXTR_URL,
platform_zap_split: env.get().PLATFORM_ZAP_SPLIT, 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} = {}) => { export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
const base = getSetting("imgproxy_url") const base = getSetting("imgproxy_url")
@ -270,19 +283,6 @@ export const dufflepud = (path: string) => {
return `${base}/${path}` 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 // Plaintext
export const getPlaintext = (e: TrustedEvent) => plaintext.get()[e.id] export const getPlaintext = (e: TrustedEvent) => plaintext.get()[e.id]
@ -617,65 +617,6 @@ export const userMuteList = derived([muteListsByPubkey, pubkey], ([$m, $pk]) =>
export const userMutes = derived(userMuteList, l => getSingletonValues("p", l)) 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 // Channels
export const sortChannels = $channels => export const sortChannels = $channels =>
@ -853,37 +794,36 @@ export const getGroupStatus = (session, address) =>
(session?.groups?.[address] || {}) as GroupStatus (session?.groups?.[address] || {}) as GroupStatus
export const deriveGroupStatus = address => export const deriveGroupStatus = address =>
session.derived($session => getGroupStatus($session, address)) derived(session, $session => getGroupStatus($session, address))
export const getIsGroupMember = (session, address, includeRequests = false) => { export const userIsGroupMember = withGetter(
const status = getGroupStatus(session, address) derived(session, $session => (address, includeRequests = false) => {
const status = getGroupStatus($session, address)
if (address.startsWith("34550:")) { if (isCommunityAddress(address)) {
return status.joined return status.joined
}
if (address.startsWith("35834:")) {
if (includeRequests && status.access === GroupAccess.Requested) {
return true
} }
return status.access === GroupAccess.Granted if (isGroupAddress(address)) {
} if (includeRequests && status.access === GroupAccess.Requested) {
return true
}
return false return status.access === GroupAccess.Granted
} }
export const deriveIsGroupMember = (address, includeRequests = false) => return false
session.derived($session => getIsGroupMember($session, address, includeRequests)) }),
)
export const deriveGroupOptions = (defaultGroups = []) => export const deriveGroupOptions = (defaultGroups = []) =>
session.derived($session => { derived([session, userIsGroupMember], ([$session, $userIsGroupMember]) => {
const options = [] const options = []
for (const address of Object.keys($session?.groups || {})) { for (const address of Object.keys($session?.groups || {})) {
const group = groups.key(address).get() const group = groups.key(address).get()
if (group && deriveIsGroupMember(address).get()) { if (group && $userIsGroupMember(address)) {
options.push(group) options.push(group)
} }
} }
@ -895,22 +835,73 @@ export const deriveGroupOptions = (defaultGroups = []) =>
return uniqBy(prop("address"), options) return uniqBy(prop("address"), options)
}) })
export const getUserCircles = (session: Session) => export const getUserCircles = (session: Session) => {
Object.entries(session?.groups || {}) const $userIsGroupMember = userIsGroupMember.get()
.filter(([a, s]) => deriveIsGroupMember(a).get())
return Object.entries(session?.groups || {})
.filter(([a, s]) => $userIsGroupMember(a))
.map(([a, s]) => a) .map(([a, s]) => a)
}
export const deriveUserCircles = () => session.derived(getUserCircles) export const getUserGroups = (session: Session) => getUserCircles(session).filter(isGroupAddress)
export const getUserGroups = (session: Session) =>
getUserCircles(session).filter(a => a.startsWith("35834:"))
export const deriveUserGroups = () => session.derived(getUserGroups)
export const getUserCommunities = (session: Session) => 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 // Read receipts
@ -935,19 +926,20 @@ export const isSeen = derived(allReadReceipts, $m => e => $m.has(e.id))
// Notifications // Notifications
export const notifications = derived(events, $events => { export const notifications = derived(
const $pubkey = pubkey.get() [pubkey, events, isEventMuted],
const $isEventMuted = isEventMuted.get() ([$pubkey, $events, $isEventMuted]) => {
const kinds = [...noteKinds, ...reactionKinds] const kinds = [...noteKinds, ...reactionKinds]
return Array.from(repository.query([{"#p": [$pubkey]}])).filter( return Array.from(repository.query([{"#p": [$pubkey]}])).filter(
e => e =>
kinds.includes(e.kind) && kinds.includes(e.kind) &&
e.pubkey !== $pubkey && e.pubkey !== $pubkey &&
!$isEventMuted(e) && !$isEventMuted(e) &&
(e.kind !== 7 || isLike(e)), (e.kind !== 7 || isLike(e)),
) )
}) },
)
export const unreadNotifications = derived([isSeen, notifications], ([$isSeen, $notifications]) => { export const unreadNotifications = derived([isSeen, notifications], ([$isSeen, $notifications]) => {
const since = now() - seconds(30, "day") const since = now() - seconds(30, "day")
@ -958,14 +950,13 @@ export const unreadNotifications = derived([isSeen, notifications], ([$isSeen, $
}) })
export const groupNotifications = new Derived( export const groupNotifications = new Derived(
[session, events, groupRequests, groupAlerts, groupAdminKeys], [session, events, groupRequests, groupAlerts, groupAdminKeys, isEventMuted],
x => x, x => x,
) )
.throttle(3000) .throttle(3000)
.derived(([$session, $events, $requests, $alerts, $adminKeys, $addresses]) => { .derived(([$session, $events, $requests, $alerts, $adminKeys, $addresses, $isEventMuted]) => {
const addresses = new Set(getUserCircles($session)) const addresses = new Set(getUserCircles($session))
const adminPubkeys = new Set($adminKeys.map(k => k.pubkey)) const adminPubkeys = new Set($adminKeys.map(k => k.pubkey))
const $isEventMuted = isEventMuted.get()
const shouldSkip = e => { const shouldSkip = e => {
const context = e.tags.filter(t => t[0] === "a") const context = e.tags.filter(t => t[0] === "a")
@ -1718,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) => export const load = (request: MySubscribeRequest) =>
new Promise(resolve => { new Promise(resolve => {
@ -1848,6 +1839,7 @@ export const mention = (pubkey: string, ...args: unknown[]) => [
export const mentionGroup = (address: string, ...args: unknown[]) => [ export const mentionGroup = (address: string, ...args: unknown[]) => [
"a", "a",
address,
hints.WithinContext(address).getUrl(), hints.WithinContext(address).getUrl(),
] ]

View File

@ -34,7 +34,7 @@
let limit = 10 let limit = 10
let showNewMessages = false let showNewMessages = false
let groupedMessages = [] let groupedMessages = []
let useNip44 = let useNip17 =
pubkeys.length > 2 || pubkeys.length > 2 ||
($nip44.isEnabled() && ($nip44.isEnabled() &&
repository.query([{kinds: [INBOX_RELAYS], authors: pubkeys}]).length === pubkeys.length) repository.query([{kinds: [INBOX_RELAYS], authors: pubkeys}]).length === pubkeys.length)
@ -78,7 +78,7 @@
if (content) { if (content) {
textarea.value = "" textarea.value = ""
await sendMessage(content, useNip44) await sendMessage(content, useNip17)
stickToBottom() stickToBottom()
} }
@ -161,18 +161,18 @@
</div> </div>
{#if $nip44.isEnabled()} {#if $nip44.isEnabled()}
<div class="fixed bottom-0 right-12 flex items-center justify-end gap-2 p-2"> <div class="fixed bottom-0 right-12 flex items-center justify-end gap-2 p-2">
<Toggle scale={0.7} bind:value={useNip44} /> <Toggle scale={0.7} bind:value={useNip17} />
<small> <small>
Send messages using Send messages using
<Popover class="inline"> <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"> <div slot="tooltip" class="flex flex-col gap-2">
<p> <p>
When enabled, Coracle will use nostr's new group chat specification, which solves When enabled, Coracle will use nostr's new group chat specification, which solves
several problems with legacy DMs. Read more <Anchor several problems with legacy DMs. Read more <Anchor
underline underline
modal modal
href="/help/nip-44-dms">here</Anchor href="/help/nip-17-dms">here</Anchor
>. >.
</p> </p>
<p> <p>