small setting fixes

This commit is contained in:
hzrd149 2023-07-24 08:18:10 -05:00
parent 1f04766d0e
commit 85dd32ae29
31 changed files with 293 additions and 192 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Add logging to app setting services

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix color theme picker

View File

@ -18,6 +18,7 @@
"bech32": "^2.0.0", "bech32": "^2.0.0",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"debug": "^4.3.4",
"framer-motion": "^7.10.3", "framer-motion": "^7.10.3",
"hls.js": "^1.4.7", "hls.js": "^1.4.7",
"idb": "^7.1.1", "idb": "^7.1.1",
@ -40,6 +41,7 @@
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.26.2", "@changesets/cli": "^2.26.2",
"@testing-library/cypress": "^9.0.0", "@testing-library/cypress": "^9.0.0",
"@types/debug": "^4.1.8",
"@types/identicon.js": "^2.3.1", "@types/identicon.js": "^2.3.1",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6", "@types/react-dom": "^18.2.6",

View File

@ -1,9 +1,11 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import debug, { Debugger } from "debug";
import { NostrSubscription } from "./nostr-subscription"; import { NostrSubscription } from "./nostr-subscription";
import { SuperMap } from "./super-map"; import { SuperMap } from "./super-map";
import { NostrEvent } from "../types/nostr-event"; import { NostrEvent } from "../types/nostr-event";
import Subject from "./subject"; import Subject from "./subject";
import { NostrQuery } from "../types/nostr-query"; import { NostrQuery } from "../types/nostr-query";
import { nameOrPubkey } from "../helpers/debug";
type pubkey = string; type pubkey = string;
type relay = string; type relay = string;
@ -19,13 +21,17 @@ class PubkeyEventRequestSubscription {
private requestedPubkeys = new Map<pubkey, Date>(); private requestedPubkeys = new Map<pubkey, Date>();
constructor(relay: string, kind: number, name?: string, dTag?: string) { log: Debugger;
constructor(relay: string, kind: number, name?: string, dTag?: string, log?: Debugger) {
this.kind = kind; this.kind = kind;
this.dTag = dTag; this.dTag = dTag;
this.subscription = new NostrSubscription(relay, undefined, name); this.subscription = new NostrSubscription(relay, undefined, name);
this.subscription.onEvent.subscribe(this.handleEvent.bind(this)); this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this)); this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
this.log = log || debug("misc");
} }
private handleEvent(event: NostrEvent) { private handleEvent(event: NostrEvent) {
@ -41,6 +47,7 @@ class PubkeyEventRequestSubscription {
const current = sub.value; const current = sub.value;
if (!current || event.created_at > current.created_at) { if (!current || event.created_at > current.created_at) {
this.log(`Found newer event for ${nameOrPubkey(event.pubkey)}`);
sub.next(event); sub.next(event);
} }
} }
@ -57,6 +64,7 @@ class PubkeyEventRequestSubscription {
const sub = this.subjects.get(pubkey); const sub = this.subjects.get(pubkey);
if (!sub.value) { if (!sub.value) {
this.log(`Adding ${nameOrPubkey(pubkey)} to queue`);
this.requestNext.add(pubkey); this.requestNext.add(pubkey);
} }
@ -79,6 +87,7 @@ class PubkeyEventRequestSubscription {
if (dayjs(date).isBefore(timeout)) { if (dayjs(date).isBefore(timeout)) {
this.requestedPubkeys.delete(pubkey); this.requestedPubkeys.delete(pubkey);
needsUpdate = true; needsUpdate = true;
this.log(`Request for ${nameOrPubkey(pubkey)} expired`);
} }
} }
@ -87,7 +96,10 @@ class PubkeyEventRequestSubscription {
if (this.requestedPubkeys.size > 0) { if (this.requestedPubkeys.size > 0) {
const query: NostrQuery = { 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]; if (this.dTag) query["#d"] = [this.dTag];
this.log(`Updating query with ${query.authors?.length} pubkeys`);
this.subscription.setQuery(query); this.subscription.setQuery(query);
if (this.subscription.state !== NostrSubscription.OPEN) { if (this.subscription.state !== NostrSubscription.OPEN) {
this.subscription.open(); this.subscription.open();
} }
@ -105,13 +117,17 @@ export class PubkeyEventRequester {
private subjects = new SuperMap<pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>()); private subjects = new SuperMap<pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
private subscriptions = new SuperMap<relay, PubkeyEventRequestSubscription>( private subscriptions = new SuperMap<relay, PubkeyEventRequestSubscription>(
(relay) => new PubkeyEventRequestSubscription(relay, this.kind, this.name, this.dTag) (relay) => new PubkeyEventRequestSubscription(relay, this.kind, this.name, this.dTag, this.log.extend(relay))
); );
constructor(kind: number, name?: string, dTag?: string) { log: Debugger;
constructor(kind: number, name?: string, dTag?: string, log?: Debugger) {
this.kind = kind; this.kind = kind;
this.name = name; this.name = name;
this.dTag = dTag; this.dTag = dTag;
this.log = log || debug("misc");
} }
getSubject(pubkey: string) { getSubject(pubkey: string) {
@ -120,25 +136,29 @@ export class PubkeyEventRequester {
handleEvent(event: NostrEvent) { handleEvent(event: NostrEvent) {
if (event.kind !== this.kind) return; if (event.kind !== this.kind) return;
const sub = this.subjects.get(event.pubkey);
const sub = this.subjects.get(event.pubkey);
const current = sub.value; const current = sub.value;
if (!current || event.created_at > current.created_at) { if (!current || event.created_at > current.created_at) {
this.log(`New event for ${nameOrPubkey(event.pubkey)}`);
sub.next(event); sub.next(event);
} }
} }
private connected = new WeakSet<any>();
requestEvent(pubkey: string, relays: string[]) { requestEvent(pubkey: string, relays: string[]) {
this.log(`Requesting event for ${nameOrPubkey(pubkey)}`);
const sub = this.subjects.get(pubkey); const sub = this.subjects.get(pubkey);
for (const relay of relays) { for (const relay of relays) {
const relaySub = this.subscriptions.get(relay).requestEvent(pubkey); const relaySub = this.subscriptions.get(relay).requestEvent(pubkey);
if (!this.connected.has(relaySub)) { sub.connectWithHandler(relaySub, (event, next, current) => {
relaySub.subscribe((event) => event && this.handleEvent(event)); if (event.kind !== this.kind) return;
this.connected.add(relaySub); if (!current || event.created_at > current.created_at) {
} this.log(`Event for ${nameOrPubkey(event.pubkey)} from connection`);
next(event);
}
});
} }
return sub; return sub;

View File

@ -1,5 +1,6 @@
import Subject from "./subject"; import Subject from "./subject";
/** @deprecated */
export class PubkeySubjectCache<T> { export class PubkeySubjectCache<T> {
subjects = new Map<string, Subject<T | null>>(); subjects = new Map<string, Subject<T | null>>();
relays = new Map<string, Set<string>>(); relays = new Map<string, Set<string>>();

View File

@ -19,6 +19,8 @@ export class Subject<Value> implements Connectable<Value> {
} }
next(value: Value) { next(value: Value) {
if (this.value === value) return;
this.value = value; this.value = value;
for (const [listener, ctx] of this.listeners) { for (const [listener, ctx] of this.listeners) {
if (ctx) listener.call(ctx, value); if (ctx) listener.call(ctx, value);

View File

@ -1,5 +1,5 @@
import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react"; import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
import appSettings from "../../services/app-settings"; import appSettings from "../../services/settings/app-settings";
import { ImageGalleryLink } from "../image-gallery"; import { ImageGalleryLink } from "../image-gallery";
import { useIsMobile } from "../../hooks/use-is-mobile"; import { useIsMobile } from "../../hooks/use-is-mobile";
import { useTrusted } from "../../providers/trust"; import { useTrusted } from "../../providers/trust";

View File

@ -1,5 +1,5 @@
import { replaceDomain } from "../../helpers/url"; import { replaceDomain } from "../../helpers/url";
import appSettings from "../../services/app-settings"; import appSettings from "../../services/settings/app-settings";
import { renderGenericUrl } from "./common"; import { renderGenericUrl } from "./common";
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/reddit.js // copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/reddit.js

View File

@ -1,5 +1,5 @@
import { replaceDomain } from "../../helpers/url"; import { replaceDomain } from "../../helpers/url";
import appSettings from "../../services/app-settings"; import appSettings from "../../services/settings/app-settings";
import { renderOpenGraphUrl } from "./common"; import { renderOpenGraphUrl } from "./common";
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js // copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js

View File

@ -1,5 +1,5 @@
import { AspectRatio, list } from "@chakra-ui/react"; import { AspectRatio, list } from "@chakra-ui/react";
import appSettings from "../../services/app-settings"; import appSettings from "../../services/settings/app-settings";
import { renderOpenGraphUrl } from "./common"; import { renderOpenGraphUrl } from "./common";
import { replaceDomain } from "../../helpers/url"; import { replaceDomain } from "../../helpers/url";

View File

@ -7,7 +7,7 @@ import { UserAvatarLink } from "../user-avatar-link";
import { UserLink } from "../user-link"; import { UserLink } from "../user-link";
import { UserDnsIdentityIcon } from "../user-dns-identity-icon"; import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
import useSubject from "../../hooks/use-subject"; import useSubject from "../../hooks/use-subject";
import appSettings from "../../services/app-settings"; import appSettings from "../../services/settings/app-settings";
import EventVerificationIcon from "../event-verification-icon"; import EventVerificationIcon from "../event-verification-icon";
import { TrustProvider } from "../../providers/trust"; import { TrustProvider } from "../../providers/trust";
import { NoteLink } from "../note-link"; import { NoteLink } from "../note-link";

View File

@ -25,7 +25,7 @@ import ReactionButton from "./buttons/reaction-button";
import NoteZapButton from "./note-zap-button"; import NoteZapButton from "./note-zap-button";
import { ExpandProvider } from "./expanded"; import { ExpandProvider } from "./expanded";
import useSubject from "../../hooks/use-subject"; import useSubject from "../../hooks/use-subject";
import appSettings from "../../services/app-settings"; import appSettings from "../../services/settings/app-settings";
import EventVerificationIcon from "../event-verification-icon"; import EventVerificationIcon from "../event-verification-icon";
import { ReplyButton } from "./buttons/reply-button"; import { ReplyButton } from "./buttons/reply-button";
import { RepostButton } from "./buttons/repost-button"; import { RepostButton } from "./buttons/repost-button";

View File

@ -4,7 +4,7 @@ import { useUserMetadata } from "../hooks/use-user-metadata";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import { getIdenticon } from "../helpers/identicon"; import { getIdenticon } from "../helpers/identicon";
import { safeUrl } from "../helpers/parse"; import { safeUrl } from "../helpers/parse";
import appSettings from "../services/app-settings"; import appSettings from "../services/settings/app-settings";
import useSubject from "../hooks/use-subject"; import useSubject from "../hooks/use-subject";
export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => { export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => {

View File

@ -24,7 +24,7 @@ import { Kind } from "nostr-tools";
import clientRelaysService from "../services/client-relays"; import clientRelaysService from "../services/client-relays";
import { getEventRelays } from "../services/event-relays"; import { getEventRelays } from "../services/event-relays";
import { useSigningContext } from "../providers/signing-provider"; import { useSigningContext } from "../providers/signing-provider";
import appSettings from "../services/app-settings"; import appSettings from "../services/settings/app-settings";
import useSubject from "../hooks/use-subject"; import useSubject from "../hooks/use-subject";
import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata"; import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata";
import { requestZapInvoice } from "../helpers/zaps"; import { requestZapInvoice } from "../helpers/zaps";

View File

@ -1,4 +1,4 @@
import appSettings from "../services/app-settings"; import appSettings from "../services/settings/app-settings";
import { convertToUrl } from "./url"; import { convertToUrl } from "./url";
const corsFailedHosts = new Set(); const corsFailedHosts = new Set();

11
src/helpers/debug.ts Normal file
View File

@ -0,0 +1,11 @@
import debug from "debug";
import userMetadataService from "../services/user-metadata";
export const logger = debug("noStrudel");
debug.enable("noStrudel:*");
export function nameOrPubkey(pubkey: string) {
const parsed = userMetadataService.getSubject(pubkey).value;
return parsed?.name || parsed?.display_name || pubkey;
}

View File

@ -1,8 +1,8 @@
import { useCallback } from "react"; import { useCallback } from "react";
import appSettings, { replaceSettings } from "../services/app-settings"; import appSettings, { replaceSettings } from "../services/settings/app-settings";
import useSubject from "./use-subject"; import useSubject from "./use-subject";
import { AppSettings } from "../services/user-app-settings";
import { useToast } from "@chakra-ui/react"; import { useToast } from "@chakra-ui/react";
import { AppSettings } from "../services/settings/migrations";
export default function useAppSettings() { export default function useAppSettings() {
const settings = useSubject(appSettings); const settings = useSubject(appSettings);

View File

@ -1,6 +1,6 @@
import { useColorMode } from "@chakra-ui/react"; import { useColorMode } from "@chakra-ui/react";
import useSubject from "./use-subject"; import useSubject from "./use-subject";
import appSettings from "../services/app-settings"; import appSettings from "../services/settings/app-settings";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { useEffect } from "react"; import { useEffect } from "react";

View File

@ -1,7 +1,7 @@
import React, { useCallback, useContext, useState } from "react"; import React, { useCallback, useContext, useState } from "react";
import InvoiceModal from "../components/invoice-modal"; import InvoiceModal from "../components/invoice-modal";
import createDefer, { Deferred } from "../classes/deferred"; import createDefer, { Deferred } from "../classes/deferred";
import appSettings from "../services/app-settings"; import appSettings from "../services/settings/app-settings";
export type InvoiceModalContext = { export type InvoiceModalContext = {
requestPay: (invoice: string) => Promise<void>; requestPay: (invoice: string) => Promise<void>;

View File

@ -1,6 +1,6 @@
import { PersistentSubject } from "../classes/subject"; import { PersistentSubject } from "../classes/subject";
import db from "./db"; import db from "./db";
import { AppSettings } from "./user-app-settings"; import { AppSettings } from "./settings/migrations";
export type Account = { export type Account = {
pubkey: string; pubkey: string;

View File

@ -1,45 +0,0 @@
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 replaceSettings(newSettings: AppSettings) {
const account = accountService.current.value;
if (!account) return;
if (account.readonly) {
accountService.updateAccountLocalSettings(account.pubkey, newSettings);
appSettings.next(newSettings);
} else {
const draft = userAppSettings.buildAppSettingsEvent(newSettings);
const event = await signingService.requestSignature(draft, account);
userAppSettings.receiveEvent(event);
await nostrPostAction(clientRelaysService.getWriteUrls(), event).onComplete;
}
}
export async function loadSettings() {
const account = accountService.current.value;
if (!account) {
appSettings.next(defaultSettings);
return;
}
appSettings.disconnectAll();
if (account.readonly) {
if (account.localSettings) {
appSettings.next(account.localSettings);
}
} else {
const subject = userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getReadUrls(), true);
appSettings.connect(subject);
}
}
accountService.current.subscribe(loadSettings);
clientRelaysService.relays.subscribe(loadSettings);
export default appSettings;

View File

@ -0,0 +1,61 @@
import { PersistentSubject } from "../../classes/subject";
import accountService from "../account";
import userAppSettings from "./user-app-settings";
import clientRelaysService from "../client-relays";
import signingService from "../signing";
import { nostrPostAction } from "../../classes/nostr-post-action";
import { AppSettings, defaultSettings } from "./migrations";
import { logger } from "../../helpers/debug";
const log = logger.extend("AppSettings");
export let appSettings = new PersistentSubject(defaultSettings);
appSettings.subscribe((event) => {
log(`Changed`, event);
});
export async function replaceSettings(newSettings: AppSettings) {
const account = accountService.current.value;
if (!account) return;
if (account.readonly) {
accountService.updateAccountLocalSettings(account.pubkey, newSettings);
appSettings.next(newSettings);
} else {
const draft = userAppSettings.buildAppSettingsEvent(newSettings);
const event = await signingService.requestSignature(draft, account);
userAppSettings.receiveEvent(event);
await nostrPostAction(clientRelaysService.getWriteUrls(), event).onComplete;
}
}
accountService.current.subscribe(() => {
const account = accountService.current.value;
if (!account) {
appSettings.next(defaultSettings);
return;
}
appSettings.disconnectAll();
if (account.localSettings) {
appSettings.next(account.localSettings);
log("Loaded user settings from local storage");
}
const subject = userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getReadUrls(), true);
appSettings.next(defaultSettings);
appSettings.connect(subject);
});
clientRelaysService.relays.subscribe(() => {
// relays changed, look for settings again
const account = accountService.current.value;
if (account) {
userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getReadUrls(), true);
}
});
export default appSettings;

View File

@ -0,0 +1,67 @@
import { ColorModeWithSystem } from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import { safeJson } from "../../helpers/parse";
export type AppSettingsV0 = {
version: 0;
colorMode: ColorModeWithSystem;
blurImages: boolean;
autoShowMedia: boolean;
proxyUserMedia: boolean;
showReactions: boolean;
showSignatureVerification: boolean;
autoPayWithWebLN: boolean;
customZapAmounts: string;
primaryColor: string;
imageProxy: string;
corsProxy: string;
showContentWarning: boolean;
twitterRedirect?: string;
redditRedirect?: string;
youtubeRedirect?: string;
};
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
return settings.version === undefined || settings.version === 0;
}
export type AppSettings = AppSettingsV0;
export const defaultSettings: AppSettings = {
version: 0,
colorMode: "system",
blurImages: true,
autoShowMedia: true,
proxyUserMedia: false,
showReactions: true,
showSignatureVerification: false,
autoPayWithWebLN: true,
customZapAmounts: "50,200,500,1000,2000,5000",
primaryColor: "#8DB600",
imageProxy: "",
corsProxy: "",
showContentWarning: true,
twitterRedirect: undefined,
redditRedirect: undefined,
youtubeRedirect: undefined,
};
export function upgradeSettings(settings: { version: number }) {
if (isV0(settings)) return settings;
return null;
}
export function parseAppSettings(event: NostrEvent): AppSettings {
const json = safeJson(event.content, {});
const upgraded = upgradeSettings(json);
return upgraded
? {
...defaultSettings,
...upgraded,
}
: defaultSettings;
}

View File

@ -0,0 +1,66 @@
import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import db from "../db";
import { logger } from "../../helpers/debug";
import { SuperMap } from "../../classes/super-map";
import { PersistentSubject } from "../../classes/subject";
import { CachedPubkeyEventRequester } from "../../classes/cached-pubkey-event-requester";
import { AppSettings, defaultSettings, parseAppSettings } from "./migrations";
const DTAG = "nostrudel-settings";
class UserAppSettings {
requester: CachedPubkeyEventRequester;
log = logger.extend("UserAppSettings");
constructor() {
this.requester = new CachedPubkeyEventRequester(30078, "user-app-data", DTAG, this.log.extend("requester"));
this.requester.readCache = (pubkey) => db.get("settings", pubkey);
this.requester.writeCache = (pubkey, event) => db.put("settings", event);
}
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
(pubkey) => new PersistentSubject<AppSettings>(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: dayjs().unix(),
};
}
}
const userAppSettings = new UserAppSettings();
setInterval(() => {
userAppSettings.update();
}, 1000 * 2);
if (import.meta.env.DEV) {
// @ts-ignore
window.userAppSettings = userAppSettings;
}
export default userAppSettings;

View File

@ -1,110 +0,0 @@
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 dayjs from "dayjs";
import { ColorMode } from "@chakra-ui/react";
import db from "./db";
const DTAG = "nostrudel-settings";
export type AppSettings = {
colorMode: ColorMode;
blurImages: boolean;
autoShowMedia: boolean;
proxyUserMedia: boolean;
showReactions: boolean;
showSignatureVerification: boolean;
autoPayWithWebLN: boolean;
customZapAmounts: string;
primaryColor: string;
imageProxy: string;
corsProxy: string;
showContentWarning: boolean;
twitterRedirect?: string;
redditRedirect?: string;
youtubeRedirect?: string;
};
export const defaultSettings: AppSettings = {
colorMode: "light",
blurImages: true,
autoShowMedia: true,
proxyUserMedia: false,
showReactions: true,
showSignatureVerification: false,
autoPayWithWebLN: true,
customZapAmounts: "50,200,500,1000,2000,5000",
primaryColor: "#8DB600",
imageProxy: "",
corsProxy: "",
showContentWarning: true,
twitterRedirect: undefined,
redditRedirect: undefined,
youtubeRedirect: undefined,
};
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 = (pubkey) => db.get("settings", pubkey);
this.requester.writeCache = (pubkey, event) => db.put("settings", event);
}
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
() => new PersistentSubject<AppSettings>(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: dayjs().unix(),
};
}
}
const userAppSettings = new UserAppSettings();
setInterval(() => {
userAppSettings.update();
}, 1000 * 2);
if (import.meta.env.DEV) {
// @ts-ignore
window.userAppSettings = userAppSettings;
}
export default userAppSettings;

View File

@ -11,8 +11,10 @@ import {
AccordionIcon, AccordionIcon,
FormHelperText, FormHelperText,
Input, Input,
Stack,
Select,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { AppSettings } from "../../services/user-app-settings"; import { AppSettings } from "../../services/settings/migrations";
export default function DisplaySettings() { export default function DisplaySettings() {
const { register } = useFormContext<AppSettings>(); const { register } = useFormContext<AppSettings>();
@ -30,15 +32,14 @@ export default function DisplaySettings() {
<AccordionPanel> <AccordionPanel>
<Flex direction="column" gap="4"> <Flex direction="column" gap="4">
<FormControl> <FormControl>
<Flex alignItems="center"> <FormLabel htmlFor="colorMode" mb="0">
<FormLabel htmlFor="colorMode" mb="0"> Use dark theme
Use dark theme </FormLabel>
</FormLabel> <Select id="colorMode" {...register("colorMode")}>
<Switch id="colorMode" {...register("colorMode")} /> <option value="system">System Default</option>
</Flex> <option value="light">Light</option>
<FormHelperText> <option value="dark">Dark</option>
<span>Enables hacker mode</span> </Select>
</FormHelperText>
</FormControl> </FormControl>
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">

View File

@ -16,6 +16,9 @@ export default function SettingsView() {
const form = useForm({ const form = useForm({
mode: "all", mode: "all",
values: settings, values: settings,
resetOptions: {
keepDirty: true,
},
}); });
const saveSettings = form.handleSubmit(async (values) => { const saveSettings = form.handleSubmit(async (values) => {
@ -48,7 +51,7 @@ export default function SettingsView() {
</Link> </Link>
<Button <Button
ml="auto" ml="auto"
isLoading={form.formState.isLoading || form.formState.isValidating} isLoading={form.formState.isLoading || form.formState.isValidating || form.formState.isSubmitting}
isDisabled={!form.formState.isDirty} isDisabled={!form.formState.isDirty}
colorScheme="brand" colorScheme="brand"
type="submit" type="submit"

View File

@ -14,8 +14,8 @@ import {
FormErrorMessage, FormErrorMessage,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { LightningIcon } from "../../components/icons"; import { LightningIcon } from "../../components/icons";
import { AppSettings } from "../../services/user-app-settings";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { AppSettings } from "../../services/settings/migrations";
export default function LightningSettings() { export default function LightningSettings() {
const { register, formState } = useFormContext<AppSettings>(); const { register, formState } = useFormContext<AppSettings>();

View File

@ -14,8 +14,8 @@ import {
Link, Link,
FormErrorMessage, FormErrorMessage,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { AppSettings } from "../../services/user-app-settings";
import { safeUrl } from "../../helpers/parse"; import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
export default function PerformanceSettings() { export default function PerformanceSettings() {
const { register, formState } = useFormContext<AppSettings>(); const { register, formState } = useFormContext<AppSettings>();

View File

@ -13,8 +13,8 @@ import {
FormErrorMessage, FormErrorMessage,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { AppSettings } from "../../services/user-app-settings";
import { safeUrl } from "../../helpers/parse"; import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
async function validateInvidiousUrl(url?: string) { async function validateInvidiousUrl(url?: string) {
if (!url) return true; if (!url) return true;

View File

@ -2521,6 +2521,13 @@
dependencies: dependencies:
"@types/filesystem" "*" "@types/filesystem" "*"
"@types/debug@^4.1.8":
version "4.1.8"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317"
integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==
dependencies:
"@types/ms" "*"
"@types/estree@0.0.39": "@types/estree@0.0.39":
version "0.0.39" version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
@ -2572,6 +2579,11 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/ms@*":
version "0.7.31"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*": "@types/node@*":
version "20.4.2" version "20.4.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9"