mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
make app settings use query store
This commit is contained in:
parent
60b61e96b8
commit
bb855a2f8f
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@ -94,13 +94,13 @@ importers:
|
||||
version: 0.6.0(typescript@5.6.2)
|
||||
applesauce-core:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0(typescript@5.6.2)
|
||||
version: link:../applesauce/packages/core
|
||||
applesauce-react:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0(typescript@5.6.2)
|
||||
version: link:../applesauce/packages/react
|
||||
applesauce-signer:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0(typescript@5.6.2)
|
||||
version: link:../applesauce/packages/signer
|
||||
bech32:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@ -2703,14 +2703,6 @@ packages:
|
||||
resolution:
|
||||
{ integrity: sha512-23W/7P0hzjVGIp51Yp4ppJgbDFNtrJrF3HO3/M36/z+Msdb3HKiSXmt3bMxEVMY6nJTT+zWneq7mAd7VvwhWaA== }
|
||||
|
||||
applesauce-react@0.6.0:
|
||||
resolution:
|
||||
{ integrity: sha512-T8fd7ImkrSe0o+FjC5AoLsYeqlLWq5bE5evwavR2+NTLC9CW185qKPPIzbTDrakq4hPADV8by3AOCmD+MY1Riw== }
|
||||
|
||||
applesauce-signer@0.6.0:
|
||||
resolution:
|
||||
{ integrity: sha512-BunnObvSqIBJ04MMQnpXIflfYEAwqWROCJqQrFZXHy+yXmZjHzPVMkXQLcJ7oXoNVc4CaxgJwp7w8eSmohJKFg== }
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution:
|
||||
{ integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== }
|
||||
@ -8301,29 +8293,6 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-react@0.6.0(typescript@5.6.2):
|
||||
dependencies:
|
||||
applesauce-core: 0.6.0(typescript@5.6.2)
|
||||
nostr-tools: 2.7.2(typescript@5.6.2)
|
||||
react: 18.3.1
|
||||
zen-observable: 0.10.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-signer@0.6.0(typescript@5.6.2):
|
||||
dependencies:
|
||||
"@noble/hashes": 1.5.0
|
||||
"@noble/secp256k1": 1.7.1
|
||||
"@scure/base": 1.1.9
|
||||
"@types/dom-serial": 1.0.6
|
||||
applesauce-core: 0.6.0(typescript@5.6.2)
|
||||
debug: 4.3.7
|
||||
nostr-tools: 2.7.2(typescript@5.6.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
import { AppSettings } from "../../helpers/app-settings";
|
||||
import { Nip07Interface } from "applesauce-signer";
|
||||
|
||||
export class Account {
|
||||
readonly type: string = "unknown";
|
||||
pubkey: string;
|
||||
localSettings?: AppSettings;
|
||||
localSettings?: Partial<AppSettings>;
|
||||
|
||||
protected _signer?: Nip07Interface | undefined;
|
||||
public get signer(): Nip07Interface | undefined {
|
||||
|
@ -6,7 +6,6 @@ import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import { NoteLink } from "../../note/note-link";
|
||||
@ -17,9 +16,10 @@ import HoverLinkOverlay from "../../hover-link-overlay";
|
||||
import singleEventService from "../../../services/single-event";
|
||||
import relayHintService from "../../../services/event-relay-hint";
|
||||
import localSettings from "../../../services/local-settings";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
|
||||
export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
const { showSignatureVerification } = useSubject(appSettings);
|
||||
const { showSignatureVerification } = useAppSettings();
|
||||
const enableDrawer = useSubject(localSettings.enableNoteThreadDrawer);
|
||||
const navigate = enableDrawer ? useNavigateInDrawer() : useNavigate();
|
||||
const to = `/n/${relayHintService.getSharableEventAddress(event)}`;
|
||||
|
@ -4,8 +4,6 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import Timestamp from "../../timestamp";
|
||||
@ -17,13 +15,14 @@ import { getTorrentTitle } from "../../../helpers/nostr/torrents";
|
||||
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
|
||||
export default function EmbeddedTorrentComment({
|
||||
comment,
|
||||
...props
|
||||
}: Omit<CardProps, "children"> & { comment: NostrEvent }) {
|
||||
const navigate = useNavigateInDrawer();
|
||||
const { showSignatureVerification } = useSubject(appSettings);
|
||||
const { showSignatureVerification } = useAppSettings();
|
||||
const refs = getThreadReferences(comment);
|
||||
const torrent = useSingleEvent(refs.root?.e?.id, refs.root?.e?.relays);
|
||||
const linkToTorrent = refs.root?.e && `/torrents/${nip19.neventEncode(refs.root.e)}`;
|
||||
|
@ -21,13 +21,14 @@ import { EmbedProps } from "../embed-event";
|
||||
import userMailboxesService from "../../services/user-mailboxes";
|
||||
import InputStep from "./input-step";
|
||||
import lnurlMetadataService from "../../services/lnurl-metadata";
|
||||
import userMetadataService from "../../services/user-metadata";
|
||||
import signingService from "../../services/signing";
|
||||
import accountService from "../../services/account";
|
||||
import PayStep from "./pay-step";
|
||||
import { getInvoiceFromCallbackUrl } from "../../helpers/lnurl";
|
||||
import UserLink from "../user/user-link";
|
||||
import relayHintService from "../../services/event-relay-hint";
|
||||
import { queryStore } from "../../services/event-store";
|
||||
import { getValue } from "applesauce-core/observable";
|
||||
|
||||
export type PayRequest = { invoice?: string; pubkey: string; error?: any };
|
||||
|
||||
@ -38,7 +39,8 @@ async function getPayRequestForPubkey(
|
||||
comment?: string,
|
||||
additionalRelays?: Iterable<string>,
|
||||
): Promise<PayRequest> {
|
||||
const metadata = userMetadataService.getSubject(pubkey).value;
|
||||
const metadata = await getValue(queryStore.profile(pubkey));
|
||||
if (!metadata) throw new Error("Cant find user metadata");
|
||||
const address = metadata?.lud16 || metadata?.lud06;
|
||||
if (!address) throw new Error("User missing lightning address");
|
||||
const lnurlMetadata = await lnurlMetadataService.requestMetadata(address);
|
||||
|
@ -7,7 +7,7 @@ import UserLink from "../user/user-link";
|
||||
import { ChevronDownIcon, ChevronUpIcon, CheckIcon, ErrorIcon, LightningIcon } from "../icons";
|
||||
import { InvoiceModalContent } from "../invoice-modal";
|
||||
import { PropsWithChildren, useEffect, useState } from "react";
|
||||
import appSettings from "../../services/settings/app-settings";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
|
||||
function UserCard({ children, pubkey }: PropsWithChildren & { pubkey: string }) {
|
||||
return (
|
||||
@ -79,6 +79,7 @@ function ErrorCard({ pubkey, error }: { pubkey: string; error: any }) {
|
||||
|
||||
export default function PayStep({ callbacks, onComplete }: { callbacks: PayRequest[]; onComplete: () => void }) {
|
||||
const [paid, setPaid] = useState<string[]>([]);
|
||||
const { autoPayWithWebLN } = useAppSettings();
|
||||
|
||||
const [payingAll, setPayingAll] = useState(false);
|
||||
const payAllWithWebLN = async () => {
|
||||
@ -99,14 +100,16 @@ export default function PayStep({ callbacks, onComplete }: { callbacks: PayReque
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!callbacks.filter((p) => !!p.invoice).some(({ pubkey }) => !paid.includes(pubkey))) {
|
||||
const withInvoice = callbacks.filter((p) => !!p.invoice);
|
||||
const hasUnpaid = withInvoice.some(({ pubkey }) => !paid.includes(pubkey));
|
||||
if (withInvoice.length > 0 && !hasUnpaid) {
|
||||
onComplete();
|
||||
}
|
||||
}, [paid]);
|
||||
|
||||
// if autoPayWithWebLN is enabled, try to pay all immediately
|
||||
useMount(() => {
|
||||
if (appSettings.value.autoPayWithWebLN) {
|
||||
if (autoPayWithWebLN) {
|
||||
payAllWithWebLN();
|
||||
}
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { Box, useColorMode } from "@chakra-ui/react";
|
||||
import { EmbedEventPointer } from "../../embed-event";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import { STEMSTR_RELAY } from "../../../helpers/nostr/stemstr";
|
||||
import ExpandableEmbed from "../expandable-embed";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
|
||||
const setZIndex: CSSProperties = { zIndex: 1, position: "relative" };
|
||||
|
||||
@ -137,8 +137,8 @@ export function renderStemstrUrl(match: URL) {
|
||||
return <EmbedEventPointer pointer={{ type: "nevent", data: { id, relays: [STEMSTR_RELAY] } }} />;
|
||||
}
|
||||
|
||||
export function renderSoundCloudUrl(match: URL) {
|
||||
if (match.hostname !== "soundcloud.com" || match.pathname.split("/").length !== 3) return null;
|
||||
function SoundCloudEmbed({ match }: { match: URL }) {
|
||||
const { primaryColor } = useAppSettings();
|
||||
|
||||
return (
|
||||
<iframe
|
||||
@ -150,9 +150,14 @@ export function renderSoundCloudUrl(match: URL) {
|
||||
src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(
|
||||
match.protocol + match.host + match.pathname,
|
||||
)}&color=${encodeURIComponent(
|
||||
"#" + appSettings.value.primaryColor || "ff5500",
|
||||
"#" + primaryColor || "ff5500",
|
||||
)}&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true`}
|
||||
style={setZIndex}
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
export function renderSoundCloudUrl(match: URL) {
|
||||
if (match.hostname !== "soundcloud.com" || match.pathname.split("/").length !== 3) return null;
|
||||
|
||||
return <SoundCloudEmbed match={match} />;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { replaceDomain } from "../../../helpers/url";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
import { renderGenericUrl } from "./common";
|
||||
|
||||
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/reddit.js
|
||||
@ -13,13 +13,17 @@ const REDDIT_DOMAINS = [
|
||||
"old.reddit.com",
|
||||
];
|
||||
|
||||
function RedditLink({ url }: { url: URL }) {
|
||||
const { redditRedirect } = useAppSettings();
|
||||
const fixed = redditRedirect ? replaceDomain(url, redditRedirect) : url;
|
||||
|
||||
return renderGenericUrl(fixed);
|
||||
}
|
||||
|
||||
const bypassPaths = /\/(gallery\/poll\/rpan\/settings\/topics)/;
|
||||
export function renderRedditUrl(match: URL) {
|
||||
if (!REDDIT_DOMAINS.includes(match.hostname)) return null;
|
||||
if (match.pathname.match(bypassPaths)) return null;
|
||||
|
||||
const { redditRedirect } = appSettings.value;
|
||||
const fixed = redditRedirect ? replaceDomain(match, redditRedirect) : match;
|
||||
|
||||
return renderGenericUrl(fixed);
|
||||
return <RedditLink url={match} />;
|
||||
}
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { replaceDomain } from "../../../helpers/url";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
import { renderOpenGraphUrl } from "./common";
|
||||
|
||||
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js
|
||||
export const TWITTER_DOMAINS = ["x.com", "twitter.com", "www.twitter.com", "mobile.twitter.com", "pbs.twimg.com"];
|
||||
|
||||
function TwitterLink({ url, isLineEnd }: { url: URL; isLineEnd?: boolean }) {
|
||||
const { twitterRedirect } = useAppSettings();
|
||||
if (twitterRedirect) return renderOpenGraphUrl(replaceDomain(url, twitterRedirect), !!isLineEnd);
|
||||
else return renderOpenGraphUrl(url, !!isLineEnd);
|
||||
}
|
||||
|
||||
export function renderTwitterUrl(match: URL, isLineEnd: boolean) {
|
||||
if (!TWITTER_DOMAINS.includes(match.hostname)) return null;
|
||||
|
||||
const { twitterRedirect } = appSettings.value;
|
||||
if (twitterRedirect) return renderOpenGraphUrl(replaceDomain(match, twitterRedirect), isLineEnd);
|
||||
else return renderOpenGraphUrl(match, isLineEnd);
|
||||
return <TwitterLink url={match} isLineEnd={isLineEnd} />;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AspectRatio } from "@chakra-ui/react";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import ExpandableEmbed from "../expandable-embed";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
|
||||
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/youtube.js
|
||||
export const YOUTUBE_DOMAINS = [
|
||||
@ -15,20 +15,15 @@ export const YOUTUBE_DOMAINS = [
|
||||
"music.youtube.com",
|
||||
];
|
||||
|
||||
export function renderYoutubePlaylistURL(match: URL) {
|
||||
if (!YOUTUBE_DOMAINS.includes(match.hostname)) return null;
|
||||
if (!match.pathname.startsWith("/playlist")) return null;
|
||||
|
||||
const { youtubeRedirect } = appSettings.value;
|
||||
|
||||
const listId = match.searchParams.get("list");
|
||||
if (!listId) return null;
|
||||
function YoutubePlaylistEmbed({ url }: { url: URL }) {
|
||||
const { youtubeRedirect } = useAppSettings();
|
||||
const listId = url.searchParams.get("list")!;
|
||||
|
||||
const embedUrl = new URL(`embed/videoseries`, youtubeRedirect || "https://www.youtube-nocookie.com");
|
||||
embedUrl.searchParams.set("list", listId);
|
||||
|
||||
return (
|
||||
<ExpandableEmbed label="Youtube Playlist" url={match}>
|
||||
<ExpandableEmbed label="Youtube Playlist" url={url}>
|
||||
<AspectRatio ratio={560 / 315} maxWidth="40rem" zIndex={1} position="relative">
|
||||
<iframe
|
||||
src={embedUrl.toString()}
|
||||
@ -42,20 +37,26 @@ export function renderYoutubePlaylistURL(match: URL) {
|
||||
);
|
||||
}
|
||||
|
||||
export function renderYoutubeVideoURL(match: URL) {
|
||||
export function renderYoutubePlaylistURL(match: URL) {
|
||||
if (!YOUTUBE_DOMAINS.includes(match.hostname)) return null;
|
||||
if (match.pathname.startsWith("/live")) return null;
|
||||
if (!match.pathname.startsWith("/playlist")) return null;
|
||||
|
||||
const { youtubeRedirect } = appSettings.value;
|
||||
const listId = match.searchParams.get("list");
|
||||
if (!listId) return null;
|
||||
|
||||
var videoId = match.searchParams.get("v");
|
||||
if (match.hostname === "youtu.be") videoId = match.pathname.split("/")[1];
|
||||
return <YoutubePlaylistEmbed url={match} />;
|
||||
}
|
||||
|
||||
function YoutubeVideoEmbed({ url }: { url: URL }) {
|
||||
const { youtubeRedirect } = useAppSettings();
|
||||
var videoId = url.searchParams.get("v");
|
||||
if (url.hostname === "youtu.be") videoId = url.pathname.split("/")[1];
|
||||
if (!videoId) return null;
|
||||
|
||||
const embedUrl = new URL(`/embed/${videoId}`, youtubeRedirect || "https://www.youtube-nocookie.com");
|
||||
|
||||
return (
|
||||
<ExpandableEmbed label="Youtube" url={match}>
|
||||
<ExpandableEmbed label="Youtube" url={url}>
|
||||
<AspectRatio ratio={16 / 10} maxWidth="40rem" zIndex={1} position="relative">
|
||||
<iframe
|
||||
src={embedUrl.toString()}
|
||||
@ -69,12 +70,21 @@ export function renderYoutubeVideoURL(match: URL) {
|
||||
);
|
||||
}
|
||||
|
||||
export function renderYoutubeVideoURL(match: URL) {
|
||||
if (!YOUTUBE_DOMAINS.includes(match.hostname)) return null;
|
||||
if (match.pathname.startsWith("/live")) return null;
|
||||
|
||||
var videoId = match.searchParams.get("v");
|
||||
if (match.hostname === "youtu.be") videoId = match.pathname.split("/")[1];
|
||||
if (!videoId) return null;
|
||||
|
||||
return <YoutubeVideoEmbed url={match} />;
|
||||
}
|
||||
|
||||
// nostr:nevent1qqszwj6mk665ga4r25w5vzxmy9rsvqj42kk4gnkq2t2utljr6as948qpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk245xvyn
|
||||
export function renderYoutubeURL(match: URL) {
|
||||
if (!YOUTUBE_DOMAINS.includes(match.hostname)) return null;
|
||||
if (match.pathname.startsWith("/live")) return null;
|
||||
|
||||
const { youtubeRedirect } = appSettings.value;
|
||||
|
||||
return renderYoutubePlaylistURL(match) || renderYoutubeVideoURL(match);
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ import UserLink from "../../user/user-link";
|
||||
import NoteZapButton from "../note-zap-button";
|
||||
import { ExpandProvider } from "../../../providers/local/expanded";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import RepostButton from "./components/repost-button";
|
||||
import QuoteEventButton from "../quote-event-button";
|
||||
@ -48,6 +47,7 @@ import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||
import relayHintService from "../../../services/event-relay-hint";
|
||||
import localSettings from "../../../services/local-settings";
|
||||
import NotePublishedUsing from "../note-published-using";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
|
||||
export type TimelineNoteProps = Omit<CardProps, "children"> & {
|
||||
event: NostrEvent;
|
||||
@ -69,7 +69,7 @@ export function TimelineNote({
|
||||
...props
|
||||
}: TimelineNoteProps) {
|
||||
const account = useCurrentAccount();
|
||||
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
||||
const { showReactions, showSignatureVerification } = useAppSettings();
|
||||
const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
|
||||
const replyForm = useDisclosure();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ColorModeWithSystem } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { safeJson } from "../../helpers/parse";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { safeJson } from "./parse";
|
||||
|
||||
export type AppSettingsV0 = {
|
||||
version: 0;
|
@ -1,5 +1,8 @@
|
||||
import { fixOrientationAndStripMetadata } from "../lib/fix-image-orientation";
|
||||
import appSettings from "../services/settings/app-settings";
|
||||
import AppSettingsQuery from "../queries/app-settings";
|
||||
import accountService from "../services/account";
|
||||
import { queryStore } from "../services/event-store";
|
||||
import { AppSettings } from "./app-settings";
|
||||
|
||||
export type ImageSize = { width: number; height: number };
|
||||
const imageSizeCache = new Map<string, ImageSize>();
|
||||
@ -21,12 +24,23 @@ export function getImageSize(src: string): Promise<{ width: number; height: numb
|
||||
});
|
||||
}
|
||||
|
||||
// hack to get app settings
|
||||
let settings: AppSettings | undefined;
|
||||
let sub: ZenObservable.Subscription;
|
||||
accountService.current.subscribe((account) => {
|
||||
if (sub) sub.unsubscribe();
|
||||
if (!account) return;
|
||||
sub = queryStore
|
||||
.runQuery(AppSettingsQuery)(account.pubkey)
|
||||
.subscribe((v) => (settings = v));
|
||||
});
|
||||
|
||||
export function buildImageProxyURL(src: string, size: string | number) {
|
||||
let url: URL | null = null;
|
||||
if (window.IMAGE_PROXY_PATH) {
|
||||
url = new URL(location.origin);
|
||||
url.pathname = window.IMAGE_PROXY_PATH;
|
||||
} else if (appSettings.value.imageProxy) url = new URL(appSettings.value.imageProxy);
|
||||
} else if (settings?.imageProxy) url = new URL(settings.imageProxy);
|
||||
if (url === null) return;
|
||||
|
||||
url.pathname = url.pathname.replace(/\/$/, "") + "/" + size + "/" + src;
|
||||
|
@ -1,12 +1,26 @@
|
||||
import appSettings from "../services/settings/app-settings";
|
||||
import AppSettingsQuery from "../queries/app-settings";
|
||||
import accountService from "../services/account";
|
||||
import { queryStore } from "../services/event-store";
|
||||
import { AppSettings } from "./app-settings";
|
||||
import { convertToUrl } from "./url";
|
||||
|
||||
// hack to get app settings
|
||||
let settings: AppSettings | undefined;
|
||||
let sub: ZenObservable.Subscription;
|
||||
accountService.current.subscribe((account) => {
|
||||
if (sub) sub.unsubscribe();
|
||||
if (!account) return;
|
||||
sub = queryStore
|
||||
.runQuery(AppSettingsQuery)(account.pubkey)
|
||||
.subscribe((v) => (settings = v));
|
||||
});
|
||||
|
||||
const clearNetFailedHosts = new Set();
|
||||
const proxyFailedHosts = new Set();
|
||||
|
||||
export function createRequestProxyUrl(url: URL | string, corsProxy?: string) {
|
||||
if (!corsProxy && window.REQUEST_PROXY) corsProxy = new URL(window.REQUEST_PROXY, location.origin).toString();
|
||||
if (!corsProxy && appSettings.value.corsProxy) corsProxy = appSettings.value.corsProxy;
|
||||
if (!corsProxy && settings?.corsProxy) corsProxy = settings.corsProxy;
|
||||
if (!corsProxy) return url;
|
||||
|
||||
if (corsProxy.includes("<url>")) {
|
||||
@ -19,7 +33,7 @@ export function createRequestProxyUrl(url: URL | string, corsProxy?: string) {
|
||||
}
|
||||
|
||||
export function fetchWithProxy(url: URL | string, opts?: RequestInit) {
|
||||
if (!appSettings.value.corsProxy && !window.REQUEST_PROXY) return fetch(url, opts);
|
||||
if (!settings?.corsProxy && !window.REQUEST_PROXY) return fetch(url, opts);
|
||||
|
||||
const u = typeof url === "string" ? convertToUrl(url) : url;
|
||||
|
||||
|
@ -1,35 +1,42 @@
|
||||
import { useCallback } from "react";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import appSettings from "../services/settings/app-settings";
|
||||
import useSubject from "./use-subject";
|
||||
import { AppSettings } from "../services/settings/migrations";
|
||||
import { AppSettings, defaultSettings } from "../helpers/app-settings";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import accountService from "../services/account";
|
||||
import userAppSettings from "../services/settings/user-app-settings";
|
||||
import userAppSettings from "../services/user-app-settings";
|
||||
import { usePublishEvent } from "../providers/global/publish-provider";
|
||||
import { useStoreQuery } from "applesauce-react";
|
||||
import AppSettingsQuery from "../queries/app-settings";
|
||||
import { useReadRelays } from "./use-client-relays";
|
||||
|
||||
export default function useAppSettings() {
|
||||
const account = useCurrentAccount();
|
||||
const settings = useSubject(appSettings);
|
||||
const publish = usePublishEvent();
|
||||
|
||||
const localSettings = account?.localSettings;
|
||||
const syncedSettings = useStoreQuery(AppSettingsQuery, account && [account.pubkey]);
|
||||
|
||||
const readRelays = useReadRelays();
|
||||
useEffect(() => {
|
||||
if (account?.pubkey) userAppSettings.requestAppSettings(account.pubkey, readRelays);
|
||||
}, [account?.pubkey, readRelays]);
|
||||
|
||||
const updateSettings = useCallback(
|
||||
async (newSettings: Partial<AppSettings>) => {
|
||||
if (!account) return;
|
||||
const full: AppSettings = { ...settings, ...newSettings };
|
||||
const updated: Partial<AppSettings> = { ...syncedSettings, ...newSettings };
|
||||
|
||||
if (account.readonly) {
|
||||
accountService.updateAccountLocalSettings(account.pubkey, full);
|
||||
appSettings.next(full);
|
||||
} else {
|
||||
const draft = userAppSettings.buildAppSettingsEvent(full);
|
||||
accountService.updateAccountLocalSettings(account.pubkey, updated);
|
||||
if (!account.readonly) {
|
||||
const draft = userAppSettings.buildAppSettingsEvent(updated);
|
||||
await publish("Update Settings", draft);
|
||||
}
|
||||
},
|
||||
[settings, account, publish],
|
||||
[syncedSettings, account, publish],
|
||||
);
|
||||
|
||||
const settings: AppSettings = { ...defaultSettings, ...localSettings, ...syncedSettings };
|
||||
|
||||
return {
|
||||
...settings,
|
||||
updateSettings,
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { useColorMode } from "@chakra-ui/react";
|
||||
import useSubject from "./use-subject";
|
||||
import appSettings from "../services/settings/app-settings";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import useAppSettings from "./use-app-settings";
|
||||
|
||||
export default function useSetColorMode() {
|
||||
const { setColorMode } = useColorMode();
|
||||
const { colorMode } = useSubject(appSettings);
|
||||
const { colorMode } = useAppSettings();
|
||||
const [params] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -14,8 +14,7 @@ import PublishProvider from "./publish-provider";
|
||||
import WebOfTrustProvider from "./web-of-trust-provider";
|
||||
import { queryStore } from "../../services/event-store";
|
||||
|
||||
// Top level providers, should be render as close to the root as possible
|
||||
export const GlobalProviders = ({ children }: { children: React.ReactNode }) => {
|
||||
function ThemeProviders({ children }: { children: React.ReactNode }) {
|
||||
const { theme: themeName, primaryColor, maxPageWidth } = useAppSettings();
|
||||
const theme = useMemo(
|
||||
() => buildTheme(themeName, primaryColor, maxPageWidth !== "none" ? maxPageWidth : undefined),
|
||||
@ -24,25 +23,32 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) =>
|
||||
|
||||
return (
|
||||
<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
|
||||
<BreakpointProvider>
|
||||
<QueryStoreProvider store={queryStore}>
|
||||
<SigningProvider>
|
||||
<PublishProvider>
|
||||
<NotificationsProvider>
|
||||
<DMTimelineProvider>
|
||||
<DefaultEmojiProvider>
|
||||
<UserEmojiProvider>
|
||||
<AllUserSearchDirectoryProvider>
|
||||
<WebOfTrustProvider>{children}</WebOfTrustProvider>
|
||||
</AllUserSearchDirectoryProvider>
|
||||
</UserEmojiProvider>
|
||||
</DefaultEmojiProvider>
|
||||
</DMTimelineProvider>
|
||||
</NotificationsProvider>
|
||||
</PublishProvider>
|
||||
</SigningProvider>
|
||||
</QueryStoreProvider>
|
||||
</BreakpointProvider>
|
||||
<BreakpointProvider>{children}</BreakpointProvider>
|
||||
</ChakraProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Top level providers, should be render as close to the root as possible
|
||||
export const GlobalProviders = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<QueryStoreProvider store={queryStore}>
|
||||
<ThemeProviders>
|
||||
<SigningProvider>
|
||||
<PublishProvider>
|
||||
<NotificationsProvider>
|
||||
<DMTimelineProvider>
|
||||
<DefaultEmojiProvider>
|
||||
<UserEmojiProvider>
|
||||
<AllUserSearchDirectoryProvider>
|
||||
<WebOfTrustProvider>{children}</WebOfTrustProvider>
|
||||
</AllUserSearchDirectoryProvider>
|
||||
</UserEmojiProvider>
|
||||
</DefaultEmojiProvider>
|
||||
</DMTimelineProvider>
|
||||
</NotificationsProvider>
|
||||
</PublishProvider>
|
||||
</SigningProvider>
|
||||
</ThemeProviders>
|
||||
</QueryStoreProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import AppHandlerProvider from "./app-handler-provider";
|
||||
import DebugModalProvider from "./debug-modal-provider";
|
||||
import DeleteEventProvider from "./delete-event-provider";
|
||||
import InvoiceModalProvider from "./invoice-modal";
|
||||
import InvoiceModalProvider from "./invoice-modal-provider";
|
||||
import MuteModalProvider from "./mute-modal-provider";
|
||||
import PostModalProvider from "./post-modal-provider";
|
||||
import RequireReadRelays from "./require-read-relays";
|
||||
|
@ -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/settings/app-settings";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
|
||||
export type InvoiceModalContext = {
|
||||
requestPay: (invoice: string) => Promise<void>;
|
||||
@ -20,9 +20,10 @@ export function useInvoiceModalContext() {
|
||||
export default function InvoiceModalProvider({ children }: { children: React.ReactNode }) {
|
||||
const [invoice, setInvoice] = useState<string>();
|
||||
const [defer, setDefer] = useState<Deferred<void>>();
|
||||
const { autoPayWithWebLN } = useAppSettings();
|
||||
|
||||
const requestPay = useCallback(async (invoice: string) => {
|
||||
if (window.webln && appSettings.value.autoPayWithWebLN) {
|
||||
if (window.webln && autoPayWithWebLN) {
|
||||
try {
|
||||
if (!window.webln.enabled) await window.webln.enable();
|
||||
await window.webln.sendPayment(invoice);
|
17
src/queries/app-settings.ts
Normal file
17
src/queries/app-settings.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Query } from "applesauce-core";
|
||||
|
||||
import { AppSettings, defaultSettings } from "../helpers/app-settings";
|
||||
import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "../services/user-app-settings";
|
||||
import { safeJson } from "../helpers/parse";
|
||||
|
||||
export default function AppSettingsQuery(pubkey: string): Query<AppSettings> {
|
||||
return {
|
||||
key: pubkey,
|
||||
run: (events) =>
|
||||
events.replaceable(APP_SETTINGS_KIND, pubkey, APP_SETTING_IDENTIFIER).map((event) => {
|
||||
if (!event) return defaultSettings;
|
||||
const parsed = safeJson(event.content, defaultSettings) as Partial<AppSettings>;
|
||||
return { ...defaultSettings, ...parsed };
|
||||
}),
|
||||
};
|
||||
}
|
@ -9,7 +9,7 @@ import SerialPortAccount from "../classes/accounts/serial-port-account";
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import { logger } from "../helpers/debug";
|
||||
import db from "./db";
|
||||
import { AppSettings } from "./settings/migrations";
|
||||
import { AppSettings } from "../helpers/app-settings";
|
||||
|
||||
type CommonAccount = {
|
||||
pubkey: string;
|
||||
@ -119,7 +119,7 @@ class AccountService {
|
||||
return db.put("accounts", account.toJSON());
|
||||
}
|
||||
|
||||
updateAccountLocalSettings(pubkey: string, settings: AppSettings) {
|
||||
updateAccountLocalSettings(pubkey: string, settings: Partial<AppSettings>) {
|
||||
const account = this.accounts.value.find((acc) => acc.pubkey === pubkey);
|
||||
if (account) account.localSettings = settings;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { DBSchema } from "idb";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { RelayInformationDocument } from "../relay-info";
|
||||
import { AppSettings } from "../settings/migrations";
|
||||
import { AppSettings } from "../../helpers/app-settings";
|
||||
|
||||
export interface SchemaV1 {
|
||||
userMetadata: {
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { PersistentSubject } from "../../classes/subject";
|
||||
import accountService from "../account";
|
||||
import userAppSettings from "./user-app-settings";
|
||||
import clientRelaysService from "../client-relays";
|
||||
import { 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);
|
||||
});
|
||||
|
||||
let accountSub: ZenObservable.Subscription;
|
||||
accountService.current.subscribe(() => {
|
||||
const account = accountService.current.value;
|
||||
|
||||
if (!account) {
|
||||
appSettings.next(defaultSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountSub) accountSub.unsubscribe();
|
||||
|
||||
if (account.localSettings) {
|
||||
appSettings.next(account.localSettings);
|
||||
log("Loaded user settings from local storage");
|
||||
}
|
||||
|
||||
const subject = userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.readRelays.value, {
|
||||
alwaysRequest: true,
|
||||
});
|
||||
appSettings.next(subject.value || defaultSettings);
|
||||
accountSub = subject.subscribe((s) => appSettings.next(s));
|
||||
});
|
||||
|
||||
// clientRelaysService.relays.subscribe(() => {
|
||||
// // relays changed, look for settings again
|
||||
// const account = accountService.current.value;
|
||||
|
||||
// if (account) {
|
||||
// userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.getInboxURLs(), { alwaysRequest: true });
|
||||
// }
|
||||
// });
|
||||
|
||||
export default appSettings;
|
@ -1,50 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
|
||||
import SuperMap from "../../classes/super-map";
|
||||
import { PersistentSubject } from "../../classes/subject";
|
||||
import { AppSettings, defaultSettings, parseAppSettings } from "./migrations";
|
||||
import replaceableEventsService, { RequestOptions } from "../replaceable-events";
|
||||
|
||||
export const APP_SETTINGS_KIND = 30078;
|
||||
export const SETTING_EVENT_IDENTIFIER = "nostrudel-settings";
|
||||
|
||||
class UserAppSettings {
|
||||
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
|
||||
() => new PersistentSubject<AppSettings>(defaultSettings),
|
||||
);
|
||||
getSubject(pubkey: string) {
|
||||
return this.parsedSubjects.get(pubkey);
|
||||
}
|
||||
requestAppSettings(pubkey: string, relays: Iterable<string>, opts?: RequestOptions) {
|
||||
const sub = this.parsedSubjects.get(pubkey);
|
||||
const requestSub = replaceableEventsService.requestEvent(
|
||||
relays,
|
||||
APP_SETTINGS_KIND,
|
||||
pubkey,
|
||||
SETTING_EVENT_IDENTIFIER,
|
||||
opts,
|
||||
);
|
||||
sub.connectWithMapper(requestSub, (event, next) => next(parseAppSettings(event)));
|
||||
return sub;
|
||||
}
|
||||
|
||||
buildAppSettingsEvent(settings: AppSettings): DraftNostrEvent {
|
||||
return {
|
||||
kind: APP_SETTINGS_KIND,
|
||||
tags: [["d", SETTING_EVENT_IDENTIFIER]],
|
||||
content: JSON.stringify(settings),
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const userAppSettings = new UserAppSettings();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.userAppSettings = userAppSettings;
|
||||
}
|
||||
|
||||
export default userAppSettings;
|
44
src/services/user-app-settings.ts
Normal file
44
src/services/user-app-settings.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { DraftNostrEvent } from "../types/nostr-event";
|
||||
|
||||
import SuperMap from "../classes/super-map";
|
||||
import { AppSettings } from "../helpers/app-settings";
|
||||
import replaceableEventsService, { RequestOptions } from "./replaceable-events";
|
||||
import { queryStore } from "./event-store";
|
||||
import Observable from "zen-observable";
|
||||
import AppSettingsQuery from "../queries/app-settings";
|
||||
|
||||
export const APP_SETTINGS_KIND = 30078;
|
||||
export const APP_SETTING_IDENTIFIER = "nostrudel-settings";
|
||||
|
||||
class UserAppSettings {
|
||||
private parsed = new SuperMap<string, Observable<AppSettings>>((pubkey) =>
|
||||
queryStore.runQuery(AppSettingsQuery)(pubkey),
|
||||
);
|
||||
getSubject(pubkey: string) {
|
||||
return this.parsed.get(pubkey);
|
||||
}
|
||||
requestAppSettings(pubkey: string, relays: Iterable<string>, opts?: RequestOptions) {
|
||||
replaceableEventsService.requestEvent(relays, APP_SETTINGS_KIND, pubkey, APP_SETTING_IDENTIFIER, opts);
|
||||
return this.parsed.get(pubkey);
|
||||
}
|
||||
|
||||
buildAppSettingsEvent(settings: Partial<AppSettings>): DraftNostrEvent {
|
||||
return {
|
||||
kind: APP_SETTINGS_KIND,
|
||||
tags: [["d", APP_SETTING_IDENTIFIER]],
|
||||
content: JSON.stringify(settings),
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const userAppSettings = new UserAppSettings();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.userAppSettings = userAppSettings;
|
||||
}
|
||||
|
||||
export default userAppSettings;
|
@ -7,10 +7,9 @@ import accountService from "./account";
|
||||
import clientRelaysService from "./client-relays";
|
||||
import { offlineMode } from "./offline-mode";
|
||||
import replaceableEventsService from "./replaceable-events";
|
||||
import userAppSettings from "./settings/user-app-settings";
|
||||
import userMailboxesService from "./user-mailboxes";
|
||||
import userMetadataService from "./user-metadata";
|
||||
import userAppSettings, { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "./user-app-settings";
|
||||
import { USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
|
||||
import { queryStore } from "./event-store";
|
||||
|
||||
const log = logger.extend("user-event-sync");
|
||||
|
||||
@ -18,31 +17,52 @@ function downloadEvents() {
|
||||
const account = accountService.current.value!;
|
||||
const relays = clientRelaysService.readRelays.value;
|
||||
|
||||
log("Loading user information");
|
||||
userMetadataService.requestMetadata(account.pubkey, [...relays, COMMON_CONTACT_RELAY], { alwaysRequest: true });
|
||||
userMailboxesService.requestMailboxes(account.pubkey, [...relays, COMMON_CONTACT_RELAY], { alwaysRequest: true });
|
||||
userAppSettings.requestAppSettings(account.pubkey, relays, { alwaysRequest: true });
|
||||
replaceableEventsService.requestEvent(relays, USER_BLOSSOM_SERVER_LIST_KIND, account.pubkey, undefined, {
|
||||
alwaysRequest: true,
|
||||
const requestReplaceable = (relays: Iterable<string>, kind: number, d?: string) => {
|
||||
replaceableEventsService.requestEvent(relays, kind, account.pubkey, d, {
|
||||
alwaysRequest: true,
|
||||
});
|
||||
};
|
||||
|
||||
log("Loading outboxes");
|
||||
requestReplaceable([...relays, COMMON_CONTACT_RELAY], kinds.RelayList);
|
||||
|
||||
const mailboxesSub = queryStore.mailboxes(account.pubkey).subscribe((mailboxes) => {
|
||||
log("Loading user information");
|
||||
requestReplaceable(mailboxes?.outboxes || relays, kinds.Metadata);
|
||||
requestReplaceable(mailboxes?.outboxes || relays, USER_BLOSSOM_SERVER_LIST_KIND);
|
||||
requestReplaceable(mailboxes?.outboxes || relays, kinds.SearchRelaysList);
|
||||
requestReplaceable(mailboxes?.outboxes || relays, APP_SETTINGS_KIND, APP_SETTING_IDENTIFIER);
|
||||
userAppSettings.requestAppSettings(account.pubkey, relays, { alwaysRequest: true });
|
||||
|
||||
log("Loading contacts list");
|
||||
replaceableEventsService.requestEvent(
|
||||
[...clientRelaysService.readRelays.value, COMMON_CONTACT_RELAY],
|
||||
kinds.Contacts,
|
||||
account.pubkey,
|
||||
undefined,
|
||||
{
|
||||
alwaysRequest: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
log("Loading contacts list");
|
||||
replaceableEventsService.requestEvent(
|
||||
[...clientRelaysService.readRelays.value, COMMON_CONTACT_RELAY],
|
||||
kinds.Contacts,
|
||||
account.pubkey,
|
||||
undefined,
|
||||
{
|
||||
alwaysRequest: true,
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
mailboxesSub.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
accountService.current.subscribe((account) => {
|
||||
if (!account) return;
|
||||
downloadEvents();
|
||||
});
|
||||
let unsubscribe: Function | undefined;
|
||||
function update() {
|
||||
const account = accountService.current.value;
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = undefined;
|
||||
}
|
||||
|
||||
offlineMode.subscribe((offline) => {
|
||||
if (!offline && accountService.current.value) downloadEvents();
|
||||
});
|
||||
if (offlineMode.value) return;
|
||||
if (!account) return;
|
||||
unsubscribe = downloadEvents();
|
||||
}
|
||||
|
||||
accountService.current.subscribe(update);
|
||||
offlineMode.subscribe(update);
|
||||
|
@ -2,7 +2,7 @@ import { IconButton, IconButtonProps, useDisclosure } from "@chakra-ui/react";
|
||||
import useUserProfile from "../../../hooks/use-user-profile";
|
||||
import { LightningIcon } from "../../../components/icons";
|
||||
import ZapModal from "../../../components/event-zap-modal";
|
||||
import { useInvoiceModalContext } from "../../../providers/route/invoice-modal";
|
||||
import { useInvoiceModalContext } from "../../../providers/route/invoice-modal-provider";
|
||||
|
||||
export default function UserZapButton({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "aria-label">) {
|
||||
const metadata = useUserProfile(pubkey);
|
||||
|
Loading…
x
Reference in New Issue
Block a user