diff --git a/.changeset/happy-suits-mix.md b/.changeset/happy-suits-mix.md
new file mode 100644
index 000000000..932d092a4
--- /dev/null
+++ b/.changeset/happy-suits-mix.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Improve database migration
diff --git a/.changeset/lucky-beds-grin.md b/.changeset/lucky-beds-grin.md
new file mode 100644
index 000000000..567808b4e
--- /dev/null
+++ b/.changeset/lucky-beds-grin.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Store app settings in NIP-78 Arbitrary app data event with local fallback
diff --git a/src/app.tsx b/src/app.tsx
index 26d6b15c9..c4f165045 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,6 +1,6 @@
-import React, { Suspense } from "react";
+import React, { Suspense, useEffect } from "react";
import { createBrowserRouter, Navigate, Outlet, RouterProvider, useLocation } from "react-router-dom";
-import { Button, Flex, Spinner, Text } from "@chakra-ui/react";
+import { Button, Flex, Spinner, Text, useColorMode } from "@chakra-ui/react";
import { ErrorBoundary } from "./components/error-boundary";
import { Page } from "./components/page";
import { normalizeToHex } from "./helpers/nip19";
@@ -32,6 +32,7 @@ import DirectMessagesView from "./views/dm";
import DirectMessageChatView from "./views/dm/chat";
import NostrLinkView from "./views/link";
import UserReportsTab from "./views/user/reports";
+import appSettings from "./services/app-settings";
// code split search view because QrScanner library is 400kB
const SearchView = React.lazy(() => import("./views/search"));
@@ -135,10 +136,19 @@ const router = createBrowserRouter([
},
]);
-export const App = () => (
-
- }>
-
-
-
-);
+export const App = () => {
+ const { setColorMode } = useColorMode();
+ const { colorMode } = useSubject(appSettings);
+
+ useEffect(() => {
+ setColorMode(colorMode);
+ }, [colorMode]);
+
+ return (
+
+ }>
+
+
+
+ );
+};
diff --git a/src/classes/pubkey-event-requester.ts b/src/classes/pubkey-event-requester.ts
index e467c01ea..cca796ae0 100644
--- a/src/classes/pubkey-event-requester.ts
+++ b/src/classes/pubkey-event-requester.ts
@@ -3,6 +3,7 @@ import { NostrSubscription } from "./nostr-subscription";
import { SuperMap } from "./super-map";
import { NostrEvent } from "../types/nostr-event";
import Subject from "./subject";
+import { NostrQuery } from "../types/nostr-query";
type pubkey = string;
type relay = string;
@@ -10,6 +11,7 @@ type relay = string;
class PubkeyEventRequestSubscription {
private subscription: NostrSubscription;
private kind: number;
+ private dTag?: string;
private subjects = new SuperMap>(() => new Subject());
@@ -17,8 +19,9 @@ class PubkeyEventRequestSubscription {
private requestedPubkeys = new Map();
- constructor(relay: string, kind: number, name?: string) {
+ constructor(relay: string, kind: number, name?: string, dTag?: string) {
this.kind = kind;
+ this.dTag = dTag;
this.subscription = new NostrSubscription(relay, undefined, name);
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
@@ -26,7 +29,10 @@ class PubkeyEventRequestSubscription {
}
private handleEvent(event: NostrEvent) {
+ // reject the event if its the wrong kind
if (event.kind !== this.kind) return;
+ // reject the event if has the wrong d tag or is missing one
+ if (this.dTag && !event.tags.some((t) => t[0] === "d" && t[1] === this.dTag)) return;
// remove the pubkey from the waiting list
this.requestedPubkeys.delete(event.pubkey);
@@ -79,7 +85,9 @@ class PubkeyEventRequestSubscription {
// update the subscription
if (needsUpdate) {
if (this.requestedPubkeys.size > 0) {
- this.subscription.setQuery({ authors: Array.from(this.requestedPubkeys.keys()), kinds: [this.kind] });
+ const query: NostrQuery = { authors: Array.from(this.requestedPubkeys.keys()), kinds: [this.kind] };
+ if (this.dTag) query["#d"] = [this.dTag];
+ this.subscription.setQuery(query);
if (this.subscription.state !== NostrSubscription.OPEN) {
this.subscription.open();
}
@@ -93,15 +101,17 @@ class PubkeyEventRequestSubscription {
export class PubkeyEventRequester {
private kind: number;
private name?: string;
+ private dTag?: string;
private subjects = new SuperMap>(() => new Subject());
private subscriptions = new SuperMap(
- (relay) => new PubkeyEventRequestSubscription(relay, this.kind, this.name)
+ (relay) => new PubkeyEventRequestSubscription(relay, this.kind, this.name, this.dTag)
);
- constructor(kind: number, name?: string) {
+ constructor(kind: number, name?: string, dTag?: string) {
this.kind = kind;
this.name = name;
+ this.dTag = dTag;
}
getSubject(pubkey: string) {
diff --git a/src/classes/subject.ts b/src/classes/subject.ts
index 2898d200e..65b1e5774 100644
--- a/src/classes/subject.ts
+++ b/src/classes/subject.ts
@@ -59,7 +59,7 @@ export class Subject implements Connectable {
connect(connectable: Connectable) {
if (!this.upstream.has(connectable)) {
- const handler = this.next;
+ const handler = this.next.bind(this);
this.upstream.set(connectable, handler);
connectable.subscribe(handler, this);
diff --git a/src/components/embeded-note.tsx b/src/components/embeded-note.tsx
index e3fac256e..c2dd9b2bc 100644
--- a/src/components/embeded-note.tsx
+++ b/src/components/embeded-note.tsx
@@ -12,13 +12,13 @@ import { UserDnsIdentityIcon } from "./user-dns-identity";
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
import { convertTimestampToDate } from "../helpers/date";
import useSubject from "../hooks/use-subject";
-import settings from "../services/settings";
+import appSettings from "../services/app-settings";
import EventVerificationIcon from "./event-verification-icon";
import { useReadRelayUrls } from "../hooks/use-client-relays";
const EmbeddedNote = ({ note }: { note: NostrEvent }) => {
const account = useCurrentAccount();
- const showSignatureVerification = useSubject(settings.showSignatureVerification);
+ const { showSignatureVerification } = useSubject(appSettings);
const readRelays = useReadRelayUrls();
const contacts = useUserContacts(account.pubkey, readRelays);
diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx
index 75c89b270..9a5b024e9 100644
--- a/src/components/note/index.tsx
+++ b/src/components/note/index.tsx
@@ -30,7 +30,7 @@ import ReactionButton from "./buttons/reaction-button";
import NoteZapButton from "./note-zap-button";
import { ExpandProvider } from "./expanded";
import useSubject from "../../hooks/use-subject";
-import settings from "../../services/settings";
+import appSettings from "../../services/app-settings";
import EventVerificationIcon from "../event-verification-icon";
import { ReplyButton } from "./buttons/reply-button";
import { RepostButton } from "./buttons/repost-button";
@@ -45,8 +45,7 @@ export type NoteProps = {
export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteProps) => {
const isMobile = useIsMobile();
const account = useCurrentAccount();
- const showReactions = useSubject(settings.showReactions);
- const showSignatureVerification = useSubject(settings.showSignatureVerification);
+ const { showReactions, showSignatureVerification } = useSubject(appSettings);
const readRelays = useReadRelayUrls();
const contacts = useUserContacts(account.pubkey, readRelays);
diff --git a/src/components/note/note-contents.tsx b/src/components/note/note-contents.tsx
index 43c6a6cb5..9d53c7588 100644
--- a/src/components/note/note-contents.tsx
+++ b/src/components/note/note-contents.tsx
@@ -4,7 +4,7 @@ import { InlineInvoiceCard } from "../inline-invoice-card";
import { TweetEmbed } from "../tweet-embed";
import { UserLink } from "../user-link";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
-import settings from "../../services/settings";
+import appSettings from "../../services/app-settings";
import styled from "@emotion/styled";
import QuoteNote from "./quote-note";
import { useExpand } from "./expanded";
@@ -165,7 +165,7 @@ const embeds: EmbedType[] = [
regexp:
/https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,6})((?:\/[\+~%\/\.\w\-_]*)?\.(?:svg|gif|png|jpg|jpeg|webp|avif))(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/i,
render: (match, event, trusted) => {
- const ImageComponent = trusted || !settings.blurImages.value ? Image : BlurredImage;
+ const ImageComponent = trusted || !appSettings.value.blurImages ? Image : BlurredImage;
return ;
},
name: "Image",
@@ -253,7 +253,7 @@ const embeds: EmbedType[] = [
];
const MediaEmbed = ({ children, type }: { children: JSX.Element | string; type: EmbedType }) => {
- const [show, setShow] = useState(settings.autoShowMedia.value);
+ const [show, setShow] = useState(appSettings.value.autoShowMedia);
return show ? (
<>{children}>
diff --git a/src/components/user-avatar.tsx b/src/components/user-avatar.tsx
index d50141bf6..ce860d4b7 100644
--- a/src/components/user-avatar.tsx
+++ b/src/components/user-avatar.tsx
@@ -4,7 +4,7 @@ import { useUserMetadata } from "../hooks/use-user-metadata";
import { useAsync } from "react-use";
import { getIdenticon } from "../services/identicon";
import { safeUrl } from "../helpers/parse";
-import settings from "../services/settings";
+import appSettings from "../services/app-settings";
import useSubject from "../hooks/use-subject";
export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => {
@@ -17,7 +17,7 @@ export type UserAvatarProps = Omit & {
pubkey: string;
};
export const UserAvatar = React.memo(({ pubkey, ...props }: UserAvatarProps) => {
- const proxyUserMedia = useSubject(settings.proxyUserMedia);
+ const { proxyUserMedia } = useSubject(appSettings);
const metadata = useUserMetadata(pubkey);
const picture = useMemo(() => {
if (metadata?.picture) {
diff --git a/src/components/zap-modal.tsx b/src/components/zap-modal.tsx
index a69e080a9..3ba12eb05 100644
--- a/src/components/zap-modal.tsx
+++ b/src/components/zap-modal.tsx
@@ -32,7 +32,7 @@ import { useSigningContext } from "../providers/signing-provider";
import QrCodeSvg from "./qr-code-svg";
import { CopyIconButton } from "./copy-icon-button";
import { useIsMobile } from "../hooks/use-is-mobile";
-import settings from "../services/settings";
+import appSettings from "../services/app-settings";
import useSubject from "../hooks/use-subject";
type FormValues = {
@@ -63,7 +63,7 @@ export default function ZapModal({
const [promptInvoice, setPromptInvoice] = useState();
const { isOpen: showQr, onToggle: toggleQr } = useDisclosure();
const isMobile = useIsMobile();
- const zapAmounts = useSubject(settings.zapAmounts);
+ const { zapAmounts } = useSubject(appSettings);
const {
register,
@@ -173,7 +173,7 @@ export default function ZapModal({
};
const payInvoice = (invoice: string) => {
- switch (settings.lightningPayMode.value) {
+ switch (appSettings.value.lightningPayMode) {
case "webln":
payWithWebLn(invoice);
break;
diff --git a/src/services/account.ts b/src/services/account.ts
index 6a25b434d..cd73bc0b8 100644
--- a/src/services/account.ts
+++ b/src/services/account.ts
@@ -1,5 +1,6 @@
import { PersistentSubject } from "../classes/subject";
import db from "./db";
+import { AppSettings } from "./user-app-settings";
export type Account = {
pubkey: string;
@@ -8,6 +9,7 @@ export type Account = {
secKey?: ArrayBuffer;
iv?: Uint8Array;
useExtension?: boolean;
+ localSettings?: AppSettings;
};
class AccountService {
@@ -35,6 +37,11 @@ class AccountService {
if (this.hasAccount(account.pubkey)) {
// replace account
this.accounts.next(this.accounts.value.map((acc) => (acc.pubkey === account.pubkey ? account : acc)));
+
+ // if this is the current account. update it
+ if (this.current.value?.pubkey === account.pubkey) {
+ this.current.next(account);
+ }
} else {
// add account
this.accounts.next(this.accounts.value.concat(account));
@@ -48,6 +55,16 @@ class AccountService {
db.delete("accounts", pubkey);
}
+ updateAccountLocalSettings(pubkey: string, settings: AppSettings) {
+ const account = this.accounts.value.find((acc) => acc.pubkey === pubkey);
+ if (account) {
+ const updated = { ...account, localSettings: settings };
+
+ // update account
+ this.addAccount(updated);
+ }
+ }
+
switchAccount(pubkey: string) {
const account = this.accounts.value.find((acc) => acc.pubkey === pubkey);
if (account) {
diff --git a/src/services/app-settings.ts b/src/services/app-settings.ts
new file mode 100644
index 000000000..218289aa1
--- /dev/null
+++ b/src/services/app-settings.ts
@@ -0,0 +1,45 @@
+import { PersistentSubject } from "../classes/subject";
+import accountService from "./account";
+import userAppSettings, { AppSettings, defaultSettings } from "./user-app-settings";
+import clientRelaysService from "./client-relays";
+import signingService from "./signing";
+import { nostrPostAction } from "../classes/nostr-post-action";
+
+export let appSettings = new PersistentSubject(defaultSettings);
+export async function updateSettings(settings: Partial) {
+ try {
+ const account = accountService.current.value;
+ if (!account) return;
+ const json: AppSettings = { ...appSettings.value, ...settings };
+
+ if (account.readonly) {
+ accountService.updateAccountLocalSettings(account.pubkey, json);
+ appSettings.next(json);
+ } else {
+ const draft = userAppSettings.buildAppSettingsEvent({ ...appSettings.value, ...settings });
+ const event = await signingService.requestSignature(draft, account);
+ userAppSettings.receiveEvent(event);
+ await nostrPostAction(clientRelaysService.getWriteUrls(), event).onComplete;
+ }
+ } catch (e) {}
+}
+
+export async function loadSettings() {
+ const account = accountService.current.value;
+ if (!account) return;
+
+ appSettings.disconnectAll();
+
+ if (account.readonly) {
+ if (account.localSettings) {
+ appSettings.next(account.localSettings);
+ }
+ } else {
+ const subject = userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getReadUrls());
+ appSettings.connect(subject);
+ }
+}
+accountService.current.subscribe(loadSettings);
+clientRelaysService.relays.subscribe(loadSettings);
+
+export default appSettings;
diff --git a/src/services/db/index.ts b/src/services/db/index.ts
index 4f7e84704..27844abde 100644
--- a/src/services/db/index.ts
+++ b/src/services/db/index.ts
@@ -1,61 +1,56 @@
import { openDB, deleteDB } from "idb";
-import { IDBPDatabase, IDBPTransaction, StoreNames } from "idb";
-import { CustomSchema } from "./schema";
-
-type MigrationFunction = (
- database: IDBPDatabase,
- transaction: IDBPTransaction[], "versionchange">,
- event: IDBVersionChangeEvent
-) => void;
-
-const MIGRATIONS: MigrationFunction[] = [
- // 0 -> 1
- function (db, transaction, event) {
- const userMetadata = db.createObjectStore("userMetadata", {
- keyPath: "pubkey",
- });
- userMetadata.createIndex("created_at", "created_at");
-
- const userRelays = db.createObjectStore("userRelays", {
- keyPath: "pubkey",
- });
- userRelays.createIndex("created_at", "created_at");
-
- const contacts = db.createObjectStore("userContacts", {
- keyPath: "pubkey",
- });
- contacts.createIndex("created_at", "created_at");
-
- const userFollows = db.createObjectStore("userFollows", {
- keyPath: "pubkey",
- });
- userFollows.createIndex("follows", "follows", { multiEntry: true, unique: false });
-
- const dnsIdentifiers = db.createObjectStore("dnsIdentifiers");
- dnsIdentifiers.createIndex("pubkey", "pubkey", { unique: false });
- dnsIdentifiers.createIndex("name", "name", { unique: false });
- dnsIdentifiers.createIndex("domain", "domain", { unique: false });
- dnsIdentifiers.createIndex("updated", "updated", { unique: false });
-
- db.createObjectStore("settings");
- db.createObjectStore("relayInfo");
- db.createObjectStore("relayScoreboardStats", { keyPath: "relay" });
- db.createObjectStore("accounts", { keyPath: "pubkey" });
- },
-];
+import { IDBPDatabase } from "idb";
+import { SchemaV1, SchemaV2 } from "./schema";
const dbName = "storage";
-const version = 1;
-const db = await openDB(dbName, version, {
+const version = 2;
+const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction, event) {
- // TODO: why is newVersion sometimes null?
- // @ts-ignore
- for (let i = oldVersion; i <= newVersion; i++) {
- if (MIGRATIONS[i]) {
- console.log(`Running database migration ${i}`);
- MIGRATIONS[i](db, transaction, event);
- }
+ if (oldVersion < 1) {
+ const v0 = db as unknown as IDBPDatabase;
+
+ const userMetadata = v0.createObjectStore("userMetadata", {
+ keyPath: "pubkey",
+ });
+ userMetadata.createIndex("created_at", "created_at");
+
+ const userRelays = v0.createObjectStore("userRelays", {
+ keyPath: "pubkey",
+ });
+ userRelays.createIndex("created_at", "created_at");
+
+ const contacts = v0.createObjectStore("userContacts", {
+ keyPath: "pubkey",
+ });
+ contacts.createIndex("created_at", "created_at");
+
+ const userFollows = v0.createObjectStore("userFollows", {
+ keyPath: "pubkey",
+ });
+ userFollows.createIndex("follows", "follows", { multiEntry: true, unique: false });
+
+ const dnsIdentifiers = v0.createObjectStore("dnsIdentifiers");
+ dnsIdentifiers.createIndex("pubkey", "pubkey", { unique: false });
+ dnsIdentifiers.createIndex("name", "name", { unique: false });
+ dnsIdentifiers.createIndex("domain", "domain", { unique: false });
+ dnsIdentifiers.createIndex("updated", "updated", { unique: false });
+
+ v0.createObjectStore("settings");
+ v0.createObjectStore("relayInfo");
+ v0.createObjectStore("relayScoreboardStats", { keyPath: "relay" });
+ v0.createObjectStore("accounts", { keyPath: "pubkey" });
+ }
+
+ if (oldVersion < 2) {
+ const v1 = db as unknown as IDBPDatabase;
+ const v2 = db as unknown as IDBPDatabase;
+
+ v1.deleteObjectStore("settings");
+ const settings = v2.createObjectStore("settings", {
+ keyPath: "pubkey",
+ });
+ settings.createIndex("created_at", "created_at");
}
},
});
diff --git a/src/services/db/schema.ts b/src/services/db/schema.ts
index 809880b45..53aad8ad3 100644
--- a/src/services/db/schema.ts
+++ b/src/services/db/schema.ts
@@ -3,7 +3,7 @@ import { NostrEvent } from "../../types/nostr-event";
import { Account } from "../account";
import { RelayInformationDocument } from "../relay-info";
-export interface CustomSchema extends DBSchema {
+export interface SchemaV1 extends DBSchema {
userMetadata: {
key: string;
value: NostrEvent;
@@ -49,3 +49,11 @@ export interface CustomSchema extends DBSchema {
value: Account;
};
}
+
+export interface SchemaV2 extends SchemaV1 {
+ settings: {
+ key: string;
+ value: NostrEvent;
+ indexes: { created_at: number };
+ };
+}
diff --git a/src/services/settings.ts b/src/services/settings.ts
deleted file mode 100644
index 4e6615cc6..000000000
--- a/src/services/settings.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { PersistentSubject } from "../classes/subject";
-import db from "./db";
-import { Account } from "./account";
-
-export enum LightningPayMode {
- Prompt = "prompt",
- Webln = "webln",
- External = "external",
-}
-
-const settings = {
- blurImages: new PersistentSubject(true),
- autoShowMedia: new PersistentSubject(true),
- proxyUserMedia: new PersistentSubject(false),
- showReactions: new PersistentSubject(true),
- showSignatureVerification: new PersistentSubject(false),
- accounts: new PersistentSubject([]),
- lightningPayMode: new PersistentSubject(LightningPayMode.Prompt),
- zapAmounts: new PersistentSubject([50, 200, 500, 1000]),
-};
-
-async function loadSettings() {
- let loading = true;
-
- // load
- for (const [key, subject] of Object.entries(settings)) {
- const value = await db.get("settings", key);
- // @ts-ignore
- if (value !== undefined) subject.next(value);
-
- // save
- subject.subscribe((newValue) => {
- if (loading) return;
- db.put("settings", newValue, key);
- });
- }
-
- loading = false;
-}
-await loadSettings();
-
-export default settings;
diff --git a/src/services/user-app-settings.ts b/src/services/user-app-settings.ts
new file mode 100644
index 000000000..65bf18b29
--- /dev/null
+++ b/src/services/user-app-settings.ts
@@ -0,0 +1,105 @@
+import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
+import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
+import { SuperMap } from "../classes/super-map";
+import { PersistentSubject } from "../classes/subject";
+import { safeJson } from "../helpers/parse";
+import moment from "moment";
+import { ColorMode } from "@chakra-ui/react";
+import db from "./db";
+
+const DTAG = "nostrudel-settings";
+
+export enum LightningPayMode {
+ Prompt = "prompt",
+ Webln = "webln",
+ External = "external",
+}
+
+export type AppSettings = {
+ colorMode: ColorMode;
+ blurImages: boolean;
+ autoShowMedia: boolean;
+ proxyUserMedia: boolean;
+ showReactions: boolean;
+ showSignatureVerification: boolean;
+ lightningPayMode: LightningPayMode;
+ zapAmounts: number[];
+};
+
+export const defaultSettings: AppSettings = {
+ colorMode: "light",
+ blurImages: true,
+ autoShowMedia: true,
+ proxyUserMedia: false,
+ showReactions: true,
+ showSignatureVerification: false,
+ lightningPayMode: LightningPayMode.Prompt,
+ zapAmounts: [50, 200, 500, 1000],
+};
+
+function parseAppSettings(event: NostrEvent): AppSettings {
+ const json = safeJson(event.content, {});
+ return {
+ ...defaultSettings,
+ ...json,
+ };
+}
+
+class UserAppSettings {
+ requester: CachedPubkeyEventRequester;
+ constructor() {
+ this.requester = new CachedPubkeyEventRequester(30078, "user-app-data", DTAG);
+ this.requester.readCache = this.readCache;
+ this.requester.writeCache = this.writeCache;
+ }
+
+ readCache(pubkey: string) {
+ return db.get("settings", pubkey);
+ }
+ writeCache(pubkey: string, event: NostrEvent) {
+ return db.put("settings", event);
+ }
+
+ private parsedSubjects = new SuperMap>(
+ () => new PersistentSubject(defaultSettings)
+ );
+ getSubject(pubkey: string) {
+ return this.parsedSubjects.get(pubkey);
+ }
+ requestAppSettings(pubkey: string, relays: string[], alwaysRequest = false) {
+ const sub = this.parsedSubjects.get(pubkey);
+ const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest);
+ sub.connectWithHandler(requestSub, (event, next) => next(parseAppSettings(event)));
+ return sub;
+ }
+
+ receiveEvent(event: NostrEvent) {
+ this.requester.handleEvent(event);
+ }
+
+ update() {
+ this.requester.update();
+ }
+
+ buildAppSettingsEvent(settings: AppSettings): DraftNostrEvent {
+ return {
+ kind: 30078,
+ tags: [["d", DTAG]],
+ content: JSON.stringify(settings),
+ created_at: moment().unix(),
+ };
+ }
+}
+
+const userAppSettings = new UserAppSettings();
+
+setInterval(() => {
+ userAppSettings.update();
+}, 1000 * 2);
+
+if (import.meta.env.DEV) {
+ // @ts-ignore
+ window.userAppSettings = userAppSettings;
+}
+
+export default userAppSettings;
diff --git a/src/types/nostr-event.ts b/src/types/nostr-event.ts
index 5558fb9b3..f75a67fb4 100644
--- a/src/types/nostr-event.ts
+++ b/src/types/nostr-event.ts
@@ -1,7 +1,8 @@
export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string];
export type PTag = ["p", string] | ["p", string, string];
export type RTag = ["r", string] | ["r", string, string];
-export type Tag = string[] | ETag | PTag | RTag;
+export type DTag = ["d"] | ["d", string];
+export type Tag = string[] | ETag | PTag | RTag | DTag;
export type NostrEvent = {
id: string;
@@ -30,3 +31,6 @@ export function isPTag(tag: Tag): tag is PTag {
export function isRTag(tag: Tag): tag is RTag {
return tag[0] === "r" && tag[1] !== undefined;
}
+export function isDTag(tag: Tag): tag is DTag {
+ return tag[0] === "d";
+}
diff --git a/src/types/nostr-query.ts b/src/types/nostr-query.ts
index 0e002b010..cdd3cc996 100644
--- a/src/types/nostr-query.ts
+++ b/src/types/nostr-query.ts
@@ -12,6 +12,7 @@ export type NostrQuery = {
kinds?: number[];
"#e"?: string[];
"#p"?: string[];
+ "#d"?: string[];
since?: number;
until?: number;
limit?: number;
diff --git a/src/views/settings/display-settings.tsx b/src/views/settings/display-settings.tsx
index affb94134..b47345ade 100644
--- a/src/views/settings/display-settings.tsx
+++ b/src/views/settings/display-settings.tsx
@@ -3,7 +3,6 @@ import {
FormControl,
FormLabel,
Switch,
- useColorMode,
AccordionItem,
AccordionPanel,
AccordionButton,
@@ -11,13 +10,11 @@ import {
AccordionIcon,
FormHelperText,
} from "@chakra-ui/react";
-import settings from "../../services/settings";
import useSubject from "../../hooks/use-subject";
+import appSettings, { updateSettings } from "../../services/app-settings";
export default function DisplaySettings() {
- const blurImages = useSubject(settings.blurImages);
-
- const { colorMode, setColorMode } = useColorMode();
+ const { blurImages, colorMode } = useSubject(appSettings);
return (
@@ -39,7 +36,7 @@ export default function DisplaySettings() {
setColorMode(v.target.checked ? "dark" : "light")}
+ onChange={(v) => updateSettings({ colorMode: v.target.checked ? "dark" : "light" })}
/>
@@ -54,7 +51,7 @@ export default function DisplaySettings() {
settings.blurImages.next(v.target.checked)}
+ onChange={(v) => updateSettings({ blurImages: v.target.checked })}
/>
diff --git a/src/views/settings/lightning-settings.tsx b/src/views/settings/lightning-settings.tsx
index 820333a09..f92477f13 100644
--- a/src/views/settings/lightning-settings.tsx
+++ b/src/views/settings/lightning-settings.tsx
@@ -11,16 +11,17 @@ import {
Input,
Select,
} from "@chakra-ui/react";
-import { useState } from "react";
-import settings, { LightningPayMode } from "../../services/settings";
+import { useEffect, useState } from "react";
+import appSettings, { updateSettings } from "../../services/app-settings";
import useSubject from "../../hooks/use-subject";
import { LightningIcon } from "../../components/icons";
+import { LightningPayMode } from "../../services/user-app-settings";
export default function LightningSettings() {
- const lightningPayMode = useSubject(settings.lightningPayMode);
- const zapAmounts = useSubject(settings.zapAmounts);
+ const { lightningPayMode, zapAmounts } = useSubject(appSettings);
const [zapInput, setZapInput] = useState(zapAmounts.join(","));
+ useEffect(() => setZapInput(zapAmounts.join(",")), [zapAmounts.join(",")]);
return (
@@ -41,7 +42,7 @@ export default function LightningSettings() {