diff --git a/src/app/components/Chat.svelte b/src/app/components/Chat.svelte
index e5589549..1d079b5d 100644
--- a/src/app/components/Chat.svelte
+++ b/src/app/components/Chat.svelte
@@ -49,7 +49,7 @@
import ThunkToast from "@app/components/ThunkToast.svelte"
import {
INDEXER_RELAYS,
- userSettingValues,
+ userSettingsValues,
deriveChat,
splitChatId,
PLATFORM_NAME,
@@ -130,7 +130,7 @@
const template = templates[i]
thunks.push(
- await sendWrapped({pubkeys, template, delay: $userSettingValues.send_delay + ms(i)}),
+ await sendWrapped({pubkeys, template, delay: $userSettingsValues.send_delay + ms(i)}),
)
}
diff --git a/src/app/components/Content.svelte b/src/app/components/Content.svelte
index 53bc125e..8788562e 100644
--- a/src/app/components/Content.svelte
+++ b/src/app/components/Content.svelte
@@ -31,7 +31,7 @@
import ContentQuote from "@app/components/ContentQuote.svelte"
import ContentTopic from "@app/components/ContentTopic.svelte"
import ContentMention from "@app/components/ContentMention.svelte"
- import {entityLink, userSettingValues} from "@app/core/state"
+ import {entityLink, userSettingsValues} from "@app/core/state"
interface Props {
event: any
@@ -68,7 +68,7 @@
if (!parsed || hideMediaAtDepth <= depth) return false
- if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
+ if (isLink(parsed) && $userSettingsValues.show_media && isStartOrEnd(i)) {
return true
}
@@ -101,7 +101,7 @@
}
let warning = $state(
- $userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
+ $userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
)
const shortContent = $derived(
diff --git a/src/app/components/InfoSignatures.svelte b/src/app/components/InfoSignatures.svelte
new file mode 100644
index 00000000..12c48964
--- /dev/null
+++ b/src/app/components/InfoSignatures.svelte
@@ -0,0 +1,32 @@
+
+
+
+
+ {#snippet title()}
+ What are digital signatures?
+ {/snippet}
+
+
+ Most online services ask their users to trust them that they're being honest, and they usually
+ are. However, traditional social media platforms have the ability to create forged content that can appear to be genuinely authored, but which are actually
+ counterfeit.
+
+
+ On Nostr, all your content is authenticated
+ using digital signatures , which cryptographically tie a particular person to a
+ given post or message.
+
+
+ The result is that you don't normally have to trust service providers not to tamper with the
+ information flowing through the network — instead, your client software can prove that a given
+ piece of data is authentic.
+
+
Got it
+
diff --git a/src/app/components/SpaceExit.svelte b/src/app/components/SpaceExit.svelte
index c7f29ac4..78839380 100644
--- a/src/app/components/SpaceExit.svelte
+++ b/src/app/components/SpaceExit.svelte
@@ -7,7 +7,7 @@
import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
- import {removeSpaceMembership} from "@app/core/commands"
+ import {removeSpaceMembership, removeTrustedRelay} from "@app/core/commands"
const {url} = $props()
@@ -18,6 +18,7 @@
try {
await removeSpaceMembership(url)
+ await removeTrustedRelay(url)
} finally {
loading = false
}
diff --git a/src/app/components/SpaceTrustRelay.svelte b/src/app/components/SpaceTrustRelay.svelte
new file mode 100644
index 00000000..efac21f4
--- /dev/null
+++ b/src/app/components/SpaceTrustRelay.svelte
@@ -0,0 +1,86 @@
+
+
+
diff --git a/src/app/core/commands.ts b/src/app/core/commands.ts
index 8ca54082..6ea4b0f5 100644
--- a/src/app/core/commands.ts
+++ b/src/app/core/commands.ts
@@ -1,7 +1,17 @@
import {nwc} from "@getalby/sdk"
import * as nip19 from "nostr-tools/nip19"
import {get} from "svelte/store"
-import {randomId, flatten, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib"
+import {
+ randomId,
+ append,
+ remove,
+ flatten,
+ poll,
+ uniq,
+ equals,
+ TIMEZONE,
+ LOCALE,
+} from "@welshman/lib"
import type {Feed} from "@welshman/feeds"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {
@@ -19,6 +29,7 @@ import {
ALERT_WEB,
ALERT_IOS,
ALERT_ANDROID,
+ APP_DATA,
isSignedEvent,
makeEvent,
displayProfile,
@@ -56,13 +67,16 @@ import {
tagEventForQuote,
getThunkError,
} from "@welshman/app"
+import type {SettingsValues} from "@app/core/state"
import {
+ SETTINGS,
PROTECTED,
userMembership,
INDEXER_RELAYS,
NOTIFIER_PUBKEY,
NOTIFIER_RELAY,
userRoomsByUrl,
+ userSettingsValues,
} from "@app/core/state"
// Utils
@@ -461,6 +475,25 @@ export const makeAlert = async (params: AlertParams) => {
export const publishAlert = async (params: AlertParams) =>
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
+// Settings
+
+export const makeSettings = async (params: Partial) => {
+ const json = JSON.stringify({...userSettingsValues.get(), ...params})
+ const content = await signer.get().nip44.encrypt(pubkey.get()!, json)
+ const tags = [["d", SETTINGS]]
+
+ return makeEvent(APP_DATA, {content, tags})
+}
+
+export const publishSettings = async (params: Partial) =>
+ publishThunk({event: await makeSettings(params), relays: Router.get().FromUser().getUrls()})
+
+export const addTrustedRelay = async (url: string) =>
+ publishSettings({trusted_relays: append(url, userSettingsValues.get().trusted_relays)})
+
+export const removeTrustedRelay = async (url: string) =>
+ publishSettings({trusted_relays: remove(url, userSettingsValues.get().trusted_relays)})
+
// Lightning
export const getWebLn = () => (window as any).webln
diff --git a/src/app/core/state.ts b/src/app/core/state.ts
index cb9b9bee..40bc7a30 100644
--- a/src/app/core/state.ts
+++ b/src/app/core/state.ts
@@ -1,6 +1,6 @@
import twColors from "tailwindcss/colors"
import {Capacitor} from "@capacitor/core"
-import {get, derived} from "svelte/store"
+import {get, derived, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {
on,
@@ -23,7 +23,7 @@ import {
always,
} from "@welshman/lib"
import type {Socket} from "@welshman/net"
-import {Pool, load, AuthStateEvent, SocketEvent} from "@welshman/net"
+import {Pool, load, AuthStateEvent, SocketEvent, netContext} from "@welshman/net"
import {
collection,
custom,
@@ -56,6 +56,7 @@ import {
ALERT_IOS,
ALERT_ANDROID,
ALERT_STATUS,
+ APP_DATA,
getGroupTags,
getRelayTagValues,
getPubkeyTagValues,
@@ -68,6 +69,7 @@ import {
getTag,
getTagValue,
getTagValues,
+ verifyEvent,
} from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
import {Nip59, decrypt} from "@welshman/signer"
@@ -304,6 +306,9 @@ appContext.dufflepudUrl = DUFFLEPUD_URL
routerContext.getIndexerRelays = always(INDEXER_RELAYS)
+netContext.isEventValid = (event: TrustedEvent, url: string) =>
+ getSetting("trusted_relays").includes(url) || verifyEvent(event)
+
// Settings
export const canDecrypt = synced({
@@ -312,23 +317,27 @@ export const canDecrypt = synced({
storage: localStorageProvider,
})
-export const SETTINGS = 38489
+export const SETTINGS = "flotilla/settings"
+
+export type SettingsValues = {
+ show_media: boolean
+ hide_sensitive: boolean
+ trusted_relays: string[]
+ report_usage: boolean
+ report_errors: boolean
+ send_delay: number
+ font_size: number
+}
export type Settings = {
event: TrustedEvent
- values: {
- show_media: boolean
- hide_sensitive: boolean
- report_usage: boolean
- report_errors: boolean
- send_delay: number
- font_size: number
- }
+ values: SettingsValues
}
export const defaultSettings = {
show_media: true,
hide_sensitive: true,
+ trusted_relays: [],
report_usage: true,
report_errors: true,
send_delay: 3000,
@@ -336,7 +345,7 @@ export const defaultSettings = {
}
export const settings = deriveEventsMapped(repository, {
- filters: [{kinds: [SETTINGS]}],
+ filters: [{kinds: [APP_DATA], "#d": [SETTINGS]}],
itemToEvent: item => item.event,
eventToItem: async (event: TrustedEvent) => ({
event,
@@ -352,9 +361,13 @@ export const {
name: "settings",
store: settings,
getKey: settings => settings.event.pubkey,
- load: makeOutboxLoader(SETTINGS),
+ load: makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
})
+// Relays sending events with empty signatures that the user has to choose to trust
+
+export const relaysPendingTrust = writable([])
+
// Alerts
export type Alert = {
@@ -610,11 +623,11 @@ export const userSettings = withGetter(
}),
)
-export const userSettingValues = withGetter(
+export const userSettingsValues = withGetter(
derived(userSettings, $s => $s?.values || defaultSettings),
)
-export const getSetting = (key: keyof Settings["values"]) => userSettingValues.get()[key] as T
+export const getSetting = (key: keyof Settings["values"]) => userSettingsValues.get()[key] as T
export const userMembership = withGetter(
derived([pubkey, membershipsByPubkey], ([$pubkey, $membershipsByPubkey]) => {
diff --git a/src/lib/components/ModalFooter.svelte b/src/lib/components/ModalFooter.svelte
index 92860932..a2852f3e 100644
--- a/src/lib/components/ModalFooter.svelte
+++ b/src/lib/components/ModalFooter.svelte
@@ -1,6 +1,8 @@
diff --git a/src/routes/spaces/[relay]/+layout.svelte b/src/routes/spaces/[relay]/+layout.svelte
index be60fe64..96e4226b 100644
--- a/src/routes/spaces/[relay]/+layout.svelte
+++ b/src/routes/spaces/[relay]/+layout.svelte
@@ -5,14 +5,16 @@
import {ago, MONTH} from "@welshman/lib"
import {ROOM_META, EVENT_TIME, THREAD, COMMENT, MESSAGE} from "@welshman/util"
import Page from "@lib/components/Page.svelte"
+ import Dialog from "@lib/components/Dialog.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import MenuSpace from "@app/components/MenuSpace.svelte"
import SpaceAuthError from "@app/components/SpaceAuthError.svelte"
+ import SpaceTrustRelay from "@app/components/SpaceTrustRelay.svelte"
import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
import {setChecked} from "@app/util/notifications"
import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/core/commands"
- import {decodeRelay, userRoomsByUrl} from "@app/core/state"
+ import {decodeRelay, userRoomsByUrl, relaysPendingTrust} from "@app/core/state"
import {pullConservatively} from "@app/core/requests"
import {notifications} from "@app/util/notifications"
@@ -82,3 +84,9 @@
{@render children?.()}
{/key}
+
+{#if $relaysPendingTrust.includes(url)}
+
+
+
+{/if}
diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte
index dd36481d..f88c5d0a 100644
--- a/src/routes/spaces/[relay]/[room]/+page.svelte
+++ b/src/routes/spaces/[relay]/[room]/+page.svelte
@@ -31,7 +31,7 @@
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {
userRoomsByUrl,
- userSettingValues,
+ userSettingsValues,
decodeRelay,
getEventsForUrl,
deriveUserMembershipStatus,
@@ -128,7 +128,7 @@
const thunk = publishThunk({
relays: [url],
event: makeEvent(MESSAGE, template),
- delay: $userSettingValues.send_delay,
+ delay: $userSettingsValues.send_delay,
})
pushToast({
diff --git a/src/routes/spaces/[relay]/chat/+page.svelte b/src/routes/spaces/[relay]/chat/+page.svelte
index 1c978ddf..a7473627 100644
--- a/src/routes/spaces/[relay]/chat/+page.svelte
+++ b/src/routes/spaces/[relay]/chat/+page.svelte
@@ -20,7 +20,7 @@
import ChannelCompose from "@app/components/ChannelCompose.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {
- userSettingValues,
+ userSettingsValues,
decodeRelay,
getEventsForUrl,
PROTECTED,
@@ -69,7 +69,7 @@
const thunk = publishThunk({
relays: [url],
event: makeEvent(MESSAGE, template),
- delay: $userSettingValues.send_delay,
+ delay: $userSettingsValues.send_delay,
})
pushToast({