mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
small setting fixes
This commit is contained in:
parent
1f04766d0e
commit
85dd32ae29
5
.changeset/brown-bulldogs-play.md
Normal file
5
.changeset/brown-bulldogs-play.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Add logging to app setting services
|
5
.changeset/soft-mails-fetch.md
Normal file
5
.changeset/soft-mails-fetch.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix color theme picker
|
@ -18,6 +18,7 @@
|
||||
"bech32": "^2.0.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"dayjs": "^1.11.9",
|
||||
"debug": "^4.3.4",
|
||||
"framer-motion": "^7.10.3",
|
||||
"hls.js": "^1.4.7",
|
||||
"idb": "^7.1.1",
|
||||
@ -40,6 +41,7 @@
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"@testing-library/cypress": "^9.0.0",
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/identicon.js": "^2.3.1",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
|
@ -1,9 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import debug, { Debugger } from "debug";
|
||||
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";
|
||||
import { nameOrPubkey } from "../helpers/debug";
|
||||
|
||||
type pubkey = string;
|
||||
type relay = string;
|
||||
@ -19,13 +21,17 @@ class PubkeyEventRequestSubscription {
|
||||
|
||||
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.dTag = dTag;
|
||||
this.subscription = new NostrSubscription(relay, undefined, name);
|
||||
|
||||
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
|
||||
this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
|
||||
|
||||
this.log = log || debug("misc");
|
||||
}
|
||||
|
||||
private handleEvent(event: NostrEvent) {
|
||||
@ -41,6 +47,7 @@ class PubkeyEventRequestSubscription {
|
||||
|
||||
const current = sub.value;
|
||||
if (!current || event.created_at > current.created_at) {
|
||||
this.log(`Found newer event for ${nameOrPubkey(event.pubkey)}`);
|
||||
sub.next(event);
|
||||
}
|
||||
}
|
||||
@ -57,6 +64,7 @@ class PubkeyEventRequestSubscription {
|
||||
const sub = this.subjects.get(pubkey);
|
||||
|
||||
if (!sub.value) {
|
||||
this.log(`Adding ${nameOrPubkey(pubkey)} to queue`);
|
||||
this.requestNext.add(pubkey);
|
||||
}
|
||||
|
||||
@ -79,6 +87,7 @@ class PubkeyEventRequestSubscription {
|
||||
if (dayjs(date).isBefore(timeout)) {
|
||||
this.requestedPubkeys.delete(pubkey);
|
||||
needsUpdate = true;
|
||||
this.log(`Request for ${nameOrPubkey(pubkey)} expired`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +96,10 @@ class PubkeyEventRequestSubscription {
|
||||
if (this.requestedPubkeys.size > 0) {
|
||||
const query: NostrQuery = { authors: Array.from(this.requestedPubkeys.keys()), kinds: [this.kind] };
|
||||
if (this.dTag) query["#d"] = [this.dTag];
|
||||
|
||||
this.log(`Updating query with ${query.authors?.length} pubkeys`);
|
||||
this.subscription.setQuery(query);
|
||||
|
||||
if (this.subscription.state !== NostrSubscription.OPEN) {
|
||||
this.subscription.open();
|
||||
}
|
||||
@ -105,13 +117,17 @@ export class PubkeyEventRequester {
|
||||
private subjects = new SuperMap<pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||
|
||||
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.name = name;
|
||||
this.dTag = dTag;
|
||||
|
||||
this.log = log || debug("misc");
|
||||
}
|
||||
|
||||
getSubject(pubkey: string) {
|
||||
@ -120,25 +136,29 @@ export class PubkeyEventRequester {
|
||||
|
||||
handleEvent(event: NostrEvent) {
|
||||
if (event.kind !== this.kind) return;
|
||||
const sub = this.subjects.get(event.pubkey);
|
||||
|
||||
const sub = this.subjects.get(event.pubkey);
|
||||
const current = sub.value;
|
||||
if (!current || event.created_at > current.created_at) {
|
||||
this.log(`New event for ${nameOrPubkey(event.pubkey)}`);
|
||||
sub.next(event);
|
||||
}
|
||||
}
|
||||
|
||||
private connected = new WeakSet<any>();
|
||||
requestEvent(pubkey: string, relays: string[]) {
|
||||
this.log(`Requesting event for ${nameOrPubkey(pubkey)}`);
|
||||
const sub = this.subjects.get(pubkey);
|
||||
|
||||
for (const relay of relays) {
|
||||
const relaySub = this.subscriptions.get(relay).requestEvent(pubkey);
|
||||
|
||||
if (!this.connected.has(relaySub)) {
|
||||
relaySub.subscribe((event) => event && this.handleEvent(event));
|
||||
this.connected.add(relaySub);
|
||||
}
|
||||
sub.connectWithHandler(relaySub, (event, next, current) => {
|
||||
if (event.kind !== this.kind) return;
|
||||
if (!current || event.created_at > current.created_at) {
|
||||
this.log(`Event for ${nameOrPubkey(event.pubkey)} from connection`);
|
||||
next(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sub;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Subject from "./subject";
|
||||
|
||||
/** @deprecated */
|
||||
export class PubkeySubjectCache<T> {
|
||||
subjects = new Map<string, Subject<T | null>>();
|
||||
relays = new Map<string, Set<string>>();
|
||||
|
@ -19,6 +19,8 @@ export class Subject<Value> implements Connectable<Value> {
|
||||
}
|
||||
|
||||
next(value: Value) {
|
||||
if (this.value === value) return;
|
||||
|
||||
this.value = value;
|
||||
for (const [listener, ctx] of this.listeners) {
|
||||
if (ctx) listener.call(ctx, value);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import { useTrusted } from "../../providers/trust";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { replaceDomain } from "../../helpers/url";
|
||||
import appSettings from "../../services/app-settings";
|
||||
import appSettings from "../../services/settings/app-settings";
|
||||
import { renderGenericUrl } from "./common";
|
||||
|
||||
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/reddit.js
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { replaceDomain } from "../../helpers/url";
|
||||
import appSettings from "../../services/app-settings";
|
||||
import appSettings from "../../services/settings/app-settings";
|
||||
import { renderOpenGraphUrl } from "./common";
|
||||
|
||||
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { replaceDomain } from "../../helpers/url";
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { UserAvatarLink } from "../user-avatar-link";
|
||||
import { UserLink } from "../user-link";
|
||||
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||
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 { TrustProvider } from "../../providers/trust";
|
||||
import { NoteLink } from "../note-link";
|
||||
|
@ -25,7 +25,7 @@ import ReactionButton from "./buttons/reaction-button";
|
||||
import NoteZapButton from "./note-zap-button";
|
||||
import { ExpandProvider } from "./expanded";
|
||||
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 { ReplyButton } from "./buttons/reply-button";
|
||||
import { RepostButton } from "./buttons/repost-button";
|
||||
|
@ -4,7 +4,7 @@ import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import { useAsync } from "react-use";
|
||||
import { getIdenticon } from "../helpers/identicon";
|
||||
import { safeUrl } from "../helpers/parse";
|
||||
import appSettings from "../services/app-settings";
|
||||
import appSettings from "../services/settings/app-settings";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
|
||||
export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => {
|
||||
|
@ -24,7 +24,7 @@ import { Kind } from "nostr-tools";
|
||||
import clientRelaysService from "../services/client-relays";
|
||||
import { getEventRelays } from "../services/event-relays";
|
||||
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 useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata";
|
||||
import { requestZapInvoice } from "../helpers/zaps";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import appSettings from "../services/app-settings";
|
||||
import appSettings from "../services/settings/app-settings";
|
||||
import { convertToUrl } from "./url";
|
||||
|
||||
const corsFailedHosts = new Set();
|
||||
|
11
src/helpers/debug.ts
Normal file
11
src/helpers/debug.ts
Normal 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;
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
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 { AppSettings } from "../services/user-app-settings";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
import { AppSettings } from "../services/settings/migrations";
|
||||
|
||||
export default function useAppSettings() {
|
||||
const settings = useSubject(appSettings);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useColorMode } from "@chakra-ui/react";
|
||||
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 { useEffect } from "react";
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
import InvoiceModal from "../components/invoice-modal";
|
||||
import createDefer, { Deferred } from "../classes/deferred";
|
||||
import appSettings from "../services/app-settings";
|
||||
import appSettings from "../services/settings/app-settings";
|
||||
|
||||
export type InvoiceModalContext = {
|
||||
requestPay: (invoice: string) => Promise<void>;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import db from "./db";
|
||||
import { AppSettings } from "./user-app-settings";
|
||||
import { AppSettings } from "./settings/migrations";
|
||||
|
||||
export type Account = {
|
||||
pubkey: string;
|
||||
|
@ -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;
|
61
src/services/settings/app-settings.ts
Normal file
61
src/services/settings/app-settings.ts
Normal 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;
|
67
src/services/settings/migrations.ts
Normal file
67
src/services/settings/migrations.ts
Normal 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;
|
||||
}
|
66
src/services/settings/user-app-settings.ts
Normal file
66
src/services/settings/user-app-settings.ts
Normal 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;
|
@ -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;
|
@ -11,8 +11,10 @@ import {
|
||||
AccordionIcon,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Stack,
|
||||
Select,
|
||||
} from "@chakra-ui/react";
|
||||
import { AppSettings } from "../../services/user-app-settings";
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
|
||||
export default function DisplaySettings() {
|
||||
const { register } = useFormContext<AppSettings>();
|
||||
@ -30,15 +32,14 @@ export default function DisplaySettings() {
|
||||
<AccordionPanel>
|
||||
<Flex direction="column" gap="4">
|
||||
<FormControl>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="colorMode" mb="0">
|
||||
Use dark theme
|
||||
</FormLabel>
|
||||
<Switch id="colorMode" {...register("colorMode")} />
|
||||
</Flex>
|
||||
<FormHelperText>
|
||||
<span>Enables hacker mode</span>
|
||||
</FormHelperText>
|
||||
<FormLabel htmlFor="colorMode" mb="0">
|
||||
Use dark theme
|
||||
</FormLabel>
|
||||
<Select id="colorMode" {...register("colorMode")}>
|
||||
<option value="system">System Default</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Flex alignItems="center">
|
||||
|
@ -16,6 +16,9 @@ export default function SettingsView() {
|
||||
const form = useForm({
|
||||
mode: "all",
|
||||
values: settings,
|
||||
resetOptions: {
|
||||
keepDirty: true,
|
||||
},
|
||||
});
|
||||
|
||||
const saveSettings = form.handleSubmit(async (values) => {
|
||||
@ -48,7 +51,7 @@ export default function SettingsView() {
|
||||
</Link>
|
||||
<Button
|
||||
ml="auto"
|
||||
isLoading={form.formState.isLoading || form.formState.isValidating}
|
||||
isLoading={form.formState.isLoading || form.formState.isValidating || form.formState.isSubmitting}
|
||||
isDisabled={!form.formState.isDirty}
|
||||
colorScheme="brand"
|
||||
type="submit"
|
||||
|
@ -14,8 +14,8 @@ import {
|
||||
FormErrorMessage,
|
||||
} from "@chakra-ui/react";
|
||||
import { LightningIcon } from "../../components/icons";
|
||||
import { AppSettings } from "../../services/user-app-settings";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
|
||||
export default function LightningSettings() {
|
||||
const { register, formState } = useFormContext<AppSettings>();
|
||||
|
@ -14,8 +14,8 @@ import {
|
||||
Link,
|
||||
FormErrorMessage,
|
||||
} from "@chakra-ui/react";
|
||||
import { AppSettings } from "../../services/user-app-settings";
|
||||
import { safeUrl } from "../../helpers/parse";
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
|
||||
export default function PerformanceSettings() {
|
||||
const { register, formState } = useFormContext<AppSettings>();
|
||||
|
@ -13,8 +13,8 @@ import {
|
||||
FormErrorMessage,
|
||||
} from "@chakra-ui/react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { AppSettings } from "../../services/user-app-settings";
|
||||
import { safeUrl } from "../../helpers/parse";
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
|
||||
async function validateInvidiousUrl(url?: string) {
|
||||
if (!url) return true;
|
||||
|
12
yarn.lock
12
yarn.lock
@ -2521,6 +2521,13 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "0.0.39"
|
||||
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"
|
||||
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@*":
|
||||
version "20.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9"
|
||||
|
Loading…
x
Reference in New Issue
Block a user