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() {