mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-28 13:20:16 +02: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",
|
"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",
|
||||||
|
@ -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;
|
||||||
|
@ -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>>();
|
||||||
|
@ -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);
|
||||||
|
@ -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";
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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";
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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";
|
||||||
|
@ -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
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 { 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);
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
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">
|
||||||
|
@ -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"
|
||||||
|
@ -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>();
|
||||||
|
@ -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>();
|
||||||
|
@ -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;
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user