diff --git a/packages/app/src/Pages/HashTagsPage.tsx b/packages/app/src/Pages/HashTagsPage.tsx
index a2e7a8c8..82ef1a88 100644
--- a/packages/app/src/Pages/HashTagsPage.tsx
+++ b/packages/app/src/Pages/HashTagsPage.tsx
@@ -3,7 +3,7 @@ import { useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import Timeline from "Element/Timeline";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import useLogin from "Hooks/useLogin";
import { setTags } from "Login";
import { System } from "index";
diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx
index cd613a69..a876f71d 100644
--- a/packages/app/src/Pages/Layout.tsx
+++ b/packages/app/src/Pages/Layout.tsx
@@ -1,6 +1,5 @@
import "./Layout.css";
import { useEffect, useMemo, useState } from "react";
-import { useDispatch, useSelector } from "react-redux";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
import { useUserProfile } from "@snort/system-react";
@@ -9,8 +8,6 @@ import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system";
import messages from "./messages";
import Icon from "Icons/Icon";
-import { RootState } from "State/Store";
-import { setShow, reset } from "State/NoteCreator";
import useLoginFeed from "Feed/LoginFeed";
import { NoteCreator } from "Element/NoteCreator";
import { mapPlanName } from "./subscribe";
@@ -23,34 +20,17 @@ import Spinner from "Icons/Spinner";
import { fetchNip05Pubkey } from "Nip05/Verifier";
import { useTheme } from "Hooks/useTheme";
import { useLoginRelays } from "Hooks/useLoginRelays";
+import { useNoteCreator } from "State/NoteCreator";
+import { LoginUnlock } from "Element/PinPrompt";
export default function Layout() {
const location = useLocation();
- const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
- const isNoteCreatorShowing = useSelector((s: RootState) => s.noteCreator.show);
- const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const { publicKey, subscriptions } = useLogin();
- const currentSubscription = getCurrentSubscription(subscriptions);
const [pageClass, setPageClass] = useState("page");
useLoginFeed();
useTheme();
useLoginRelays();
- const handleNoteCreatorButtonClick = () => {
- if (replyTo) {
- dispatch(reset());
- }
- dispatch(setShow(true));
- };
-
- const shouldHideNoteCreator = useMemo(() => {
- const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
- return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
- }, [location, isReplyNoteCreatorShowing]);
-
const shouldHideHeader = useMemo(() => {
const hideOn = ["/login", "/new"];
return hideOn.some(a => location.pathname.startsWith(a));
@@ -67,43 +47,51 @@ export default function Layout() {
}, [location]);
return (
-
- {!shouldHideHeader && (
-
+ )}
+
+
+
+
+
+ >
);
}
+const NoteCreatorButton = () => {
+ const location = useLocation();
+ const { show, replyTo, update } = useNoteCreator(v => ({ show: v.show, replyTo: v.replyTo, update: v.update }));
+
+ const shouldHideNoteCreator = useMemo(() => {
+ const isReplyNoteCreatorShowing = replyTo && show;
+ const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
+ return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
+ }, [location]);
+
+ if (shouldHideNoteCreator) return;
+ return (
+ <>
+
+
+ >
+ );
+};
+
const AccountHeader = () => {
const navigate = useNavigate();
const { formatMessage } = useIntl();
@@ -156,6 +144,13 @@ const AccountHeader = () => {
}
}
+ if (!publicKey) {
+ return (
+
+ );
+ }
return (
{!location.pathname.startsWith("/search") && (
@@ -199,3 +194,20 @@ const AccountHeader = () => {
);
};
+
+function LogoHeader() {
+ const { subscriptions } = useLogin();
+ const currentSubscription = getCurrentSubscription(subscriptions);
+
+ return (
+
+
Snort
+ {currentSubscription && (
+
+
+ {mapPlanName(currentSubscription.type)}
+
+ )}
+
+ );
+}
diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx
index 2ebbb1ed..d0f9dc3d 100644
--- a/packages/app/src/Pages/LoginPage.tsx
+++ b/packages/app/src/Pages/LoginPage.tsx
@@ -3,22 +3,22 @@ import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl";
-import { HexKey, Nip46Signer, PrivateKeySigner } from "@snort/system";
+import { HexKey, Nip46Signer, PinEncrypted, PrivateKeySigner } from "@snort/system";
import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils";
import ZapButton from "Element/ZapButton";
import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon";
-import useLogin from "Hooks/useLogin";
import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
import AsyncButton from "Element/AsyncButton";
-import useLoginHandler from "Hooks/useLoginHandler";
+import useLoginHandler, { PinRequiredError } from "Hooks/useLoginHandler";
import { secp256k1 } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/curves/abstract/utils";
import Modal from "Element/Modal";
import QrCode from "Element/QrCode";
import Copy from "Element/Copy";
import { delay } from "SnortUtils";
+import { PinPrompt } from "Element/PinPrompt";
declare global {
interface Window {
@@ -75,9 +75,10 @@ export async function getNip05PubKey(addr: string): Promise
{
export default function LoginPage() {
const navigate = useNavigate();
- const login = useLogin();
const [key, setKey] = useState("");
+ const [nip46Key, setNip46Key] = useState("");
const [error, setError] = useState("");
+ const [pin, setPin] = useState(false);
const [art, setArt] = useState();
const [isMasking, setMasking] = useState(true);
const { formatMessage } = useIntl();
@@ -87,22 +88,19 @@ export default function LoginPage() {
const hasSubtleCrypto = window.crypto.subtle !== undefined;
const [nostrConnect, setNostrConnect] = useState("");
- useEffect(() => {
- if (login.publicKey) {
- navigate("/");
- }
- }, [login, navigate]);
-
useEffect(() => {
const ret = unwrap(Artwork.at(Artwork.length * Math.random()));
const url = proxy(ret.link);
setArt({ ...ret, link: url });
}, []);
- async function doLogin() {
+ async function doLogin(pin?: string) {
try {
- await loginHandler.doLogin(key);
+ await loginHandler.doLogin(key, pin);
} catch (e) {
+ if (e instanceof PinRequiredError) {
+ setPin(true);
+ }
if (e instanceof Error) {
setError(e.message);
} else {
@@ -116,10 +114,16 @@ export default function LoginPage() {
}
}
- async function makeRandomKey() {
- await generateNewLogin();
- window.plausible?.("Generate Account");
- navigate("/new");
+ async function makeRandomKey(pin: string) {
+ try {
+ await generateNewLogin(pin);
+ window.plausible?.("Generate Account");
+ navigate("/new");
+ } catch (e) {
+ if (e instanceof Error) {
+ setError(e.message);
+ }
+ }
}
async function doNip07Login() {
@@ -127,9 +131,10 @@ export default function LoginPage() {
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;
const pubKey = await unwrap(window.nostr).getPublicKey();
LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7, relays);
+ navigate("/");
}
- async function startNip46() {
+ function generateNip46() {
const meta = {
name: "Snort",
url: window.location.href,
@@ -142,26 +147,51 @@ export default function LoginPage() {
`metadata=${encodeURIComponent(JSON.stringify(meta))}`,
].join("&")}`;
setNostrConnect(connectUrl);
+ setNip46Key(newKey);
+ }
- const signer = new Nip46Signer(connectUrl, new PrivateKeySigner(newKey));
+ async function startNip46(pin: string) {
+ if (!nostrConnect || !nip46Key) return;
+
+ const signer = new Nip46Signer(nostrConnect, new PrivateKeySigner(nip46Key));
await signer.init();
await delay(500);
await signer.describe();
+ LoginStore.loginWithPubkey(
+ await signer.getPubKey(),
+ LoginSessionType.Nip46,
+ undefined,
+ ["wss://relay.damus.io"],
+ await PinEncrypted.create(nip46Key, pin),
+ );
+ navigate("/");
}
function nip46Buttons() {
- return null;
return (
<>
-
-
+ {
+ generateNip46();
+ setPin(true);
+ }}>
+
- {nostrConnect && (
- setNostrConnect("")}>
-
-
-
-
+ {nostrConnect && !pin && (
+ setNostrConnect("")}>
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
)}
>
@@ -177,7 +207,7 @@ export default function LoginPage() {
<>
@@ -258,7 +288,7 @@ export default function LoginPage() {
-
+
setMasking(!isMasking)}
/>
@@ -283,12 +313,32 @@ export default function LoginPage() {
/>
-
+ doLogin()}>
- makeRandomKey()}>
+ setPin(true)}>
+ {pin && (
+
+
+
+ }
+ onResult={async pin => {
+ setPin(false);
+ if (key) {
+ await doLogin(pin);
+ } else if (nostrConnect) {
+ await startNip46(pin);
+ } else {
+ await makeRandomKey(pin);
+ }
+ }}
+ onCancel={() => setPin(false)}
+ />
+ )}
{altLogins()}
{installExtension()}
diff --git a/packages/app/src/Pages/MessagesPage.tsx b/packages/app/src/Pages/MessagesPage.tsx
index 5c5e0eaf..963fd030 100644
--- a/packages/app/src/Pages/MessagesPage.tsx
+++ b/packages/app/src/Pages/MessagesPage.tsx
@@ -210,7 +210,7 @@ function NewChatWindow() {
{show && (
-
setShow(false)} className="new-chat-modal">
+ setShow(false)} className="new-chat-modal">
diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx
index 3397a269..15a050e8 100644
--- a/packages/app/src/Pages/ProfilePage.tsx
+++ b/packages/app/src/Pages/ProfilePage.tsx
@@ -447,7 +447,7 @@ export default function ProfilePage() {
{showProfileQr && (
- setShowProfileQr(false)}>
+ setShowProfileQr(false)}>
@@ -473,7 +473,7 @@ export default function ProfilePage() {
navigate(
`/messages/${encodeTLVEntries("chat4" as NostrPrefix, {
type: TLVEntryType.Author,
- length: 64,
+ length: 32,
value: id,
})}`,
)
diff --git a/packages/app/src/Pages/new/ProfileSetup.tsx b/packages/app/src/Pages/new/ProfileSetup.tsx
index 3d23df0e..4724ea69 100644
--- a/packages/app/src/Pages/new/ProfileSetup.tsx
+++ b/packages/app/src/Pages/new/ProfileSetup.tsx
@@ -5,7 +5,7 @@ import { mapEventToProfile } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import Logo from "Element/Logo";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import useLogin from "Hooks/useLogin";
import { UserCache } from "Cache";
import AvatarEditor from "Element/AvatarEditor";
diff --git a/packages/app/src/Pages/settings/Accounts.tsx b/packages/app/src/Pages/settings/Accounts.tsx
index ce113010..1fb0a987 100644
--- a/packages/app/src/Pages/settings/Accounts.tsx
+++ b/packages/app/src/Pages/settings/Accounts.tsx
@@ -1,57 +1,32 @@
-import { useState } from "react";
-import { FormattedMessage, useIntl } from "react-intl";
+import { FormattedMessage } from "react-intl";
+import { Link } from "react-router-dom";
import ProfilePreview from "Element/ProfilePreview";
import { LoginStore } from "Login";
-import useLoginHandler from "Hooks/useLoginHandler";
-import AsyncButton from "Element/AsyncButton";
import { getActiveSubscriptions } from "Subscription";
export default function AccountsPage() {
- const { formatMessage } = useIntl();
- const [key, setKey] = useState("");
- const [error, setError] = useState("");
- const loginHandler = useLoginHandler();
const logins = LoginStore.getSessions();
const sub = getActiveSubscriptions(LoginStore.allSubscriptions());
- async function doLogin() {
- try {
- setError("");
- await loginHandler.doLogin(key);
- setKey("");
- } catch (e) {
- if (e instanceof Error) {
- setError(e.message);
- } else {
- setError(
- formatMessage({
- defaultMessage: "Unknown login error",
- }),
- );
- }
- console.error(e);
- }
- }
-
return (
- <>
+
{logins.map(a => (
-
+
-
@@ -61,27 +36,12 @@ export default function AccountsPage() {
))}
{sub && (
- <>
-
+
+
-
-
-
setKey(e.target.value)}
- />
-
doLogin()}>
-
-
-
- >
+
+
)}
- {error &&
{error}}
- >
+
);
}
diff --git a/packages/app/src/Pages/settings/Keys.tsx b/packages/app/src/Pages/settings/Keys.tsx
index a678ffec..f4f3f626 100644
--- a/packages/app/src/Pages/settings/Keys.tsx
+++ b/packages/app/src/Pages/settings/Keys.tsx
@@ -1,6 +1,6 @@
import "./Keys.css";
import { FormattedMessage } from "react-intl";
-import { encodeTLV, NostrPrefix } from "@snort/system";
+import { encodeTLV, NostrPrefix, PinEncrypted } from "@snort/system";
import Copy from "Element/Copy";
import useLogin from "Hooks/useLogin";
@@ -8,7 +8,7 @@ import { hexToMnemonic } from "nip6";
import { hexToBech32 } from "SnortUtils";
export default function ExportKeys() {
- const { publicKey, privateKey, generatedEntropy } = useLogin();
+ const { publicKey, privateKeyData, generatedEntropy } = useLogin();
return (
@@ -16,12 +16,12 @@ export default function ExportKeys() {
- {privateKey && (
+ {privateKeyData instanceof PinEncrypted && (
<>
-
+
>
)}
{generatedEntropy && (
diff --git a/packages/app/src/Pages/settings/Profile.tsx b/packages/app/src/Pages/settings/Profile.tsx
index 73865224..52ef353d 100644
--- a/packages/app/src/Pages/settings/Profile.tsx
+++ b/packages/app/src/Pages/settings/Profile.tsx
@@ -6,7 +6,7 @@ import { mapEventToProfile } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { System } from "index";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import { openFile } from "SnortUtils";
import useFileUpload from "Upload";
import AsyncButton from "Element/AsyncButton";
diff --git a/packages/app/src/Pages/settings/Relays.tsx b/packages/app/src/Pages/settings/Relays.tsx
index 2942d75b..c49491e6 100644
--- a/packages/app/src/Pages/settings/Relays.tsx
+++ b/packages/app/src/Pages/settings/Relays.tsx
@@ -4,7 +4,7 @@ import { unixNowMs } from "@snort/shared";
import { randomSample } from "SnortUtils";
import Relay from "Element/Relay";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import { System } from "index";
import useLogin from "Hooks/useLogin";
import { setRelays } from "Login";
diff --git a/packages/app/src/Pages/settings/Root.tsx b/packages/app/src/Pages/settings/Root.tsx
index 2ac92d6f..34cc52f2 100644
--- a/packages/app/src/Pages/settings/Root.tsx
+++ b/packages/app/src/Pages/settings/Root.tsx
@@ -5,7 +5,6 @@ import { Outlet, useLocation, useNavigate } from "react-router-dom";
import Icon from "Icons/Icon";
import { LoginStore, logout } from "Login";
import useLogin from "Hooks/useLogin";
-import { unwrap } from "SnortUtils";
import { getCurrentSubscription } from "Subscription";
import messages from "./messages";
@@ -19,7 +18,7 @@ const SettingsIndex = () => {
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
function handleLogout() {
- logout(unwrap(login.publicKey));
+ logout(login.id);
navigate("/");
}
diff --git a/packages/app/src/Pages/settings/handle/LNAddress.tsx b/packages/app/src/Pages/settings/handle/LNAddress.tsx
index 0cad952a..1f4d5fc0 100644
--- a/packages/app/src/Pages/settings/handle/LNAddress.tsx
+++ b/packages/app/src/Pages/settings/handle/LNAddress.tsx
@@ -4,7 +4,7 @@ import { LNURL } from "@snort/shared";
import { ApiHost } from "Const";
import AsyncButton from "Element/AsyncButton";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
diff --git a/packages/app/src/Pages/settings/handle/ListHandles.tsx b/packages/app/src/Pages/settings/handle/ListHandles.tsx
index b209710a..8bef9940 100644
--- a/packages/app/src/Pages/settings/handle/ListHandles.tsx
+++ b/packages/app/src/Pages/settings/handle/ListHandles.tsx
@@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
import { ApiHost } from "Const";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
export default function ListHandles() {
diff --git a/packages/app/src/Pages/settings/handle/TransferHandle.tsx b/packages/app/src/Pages/settings/handle/TransferHandle.tsx
index ba5b0907..d21249f2 100644
--- a/packages/app/src/Pages/settings/handle/TransferHandle.tsx
+++ b/packages/app/src/Pages/settings/handle/TransferHandle.tsx
@@ -1,6 +1,6 @@
import { ApiHost } from "Const";
import AsyncButton from "Element/AsyncButton";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import { ServiceError } from "Nip05/ServiceProvider";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
import { useState } from "react";
diff --git a/packages/app/src/Pages/subscribe/ManageSubscription.tsx b/packages/app/src/Pages/subscribe/ManageSubscription.tsx
index 07c90eab..ea616a3c 100644
--- a/packages/app/src/Pages/subscribe/ManageSubscription.tsx
+++ b/packages/app/src/Pages/subscribe/ManageSubscription.tsx
@@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
import PageSpinner from "Element/PageSpinner";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
import { mapSubscriptionErrorCode } from ".";
import SubscriptionCard from "./SubscriptionCard";
diff --git a/packages/app/src/Pages/subscribe/SubscriptionCard.tsx b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx
index b653972d..f0b752f6 100644
--- a/packages/app/src/Pages/subscribe/SubscriptionCard.tsx
+++ b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx
@@ -5,7 +5,7 @@ import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
import { mapPlanName, mapSubscriptionErrorCode } from ".";
import AsyncButton from "Element/AsyncButton";
import Icon from "Icons/Icon";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import SendSats from "Element/SendSats";
import Nip5Service from "Element/Nip5Service";
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
diff --git a/packages/app/src/Pages/subscribe/index.tsx b/packages/app/src/Pages/subscribe/index.tsx
index 48acf99a..58eb6020 100644
--- a/packages/app/src/Pages/subscribe/index.tsx
+++ b/packages/app/src/Pages/subscribe/index.tsx
@@ -8,7 +8,7 @@ import { formatShort } from "Number";
import { LockedFeatures, Plans, SubscriptionType } from "Subscription";
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
import AsyncButton from "Element/AsyncButton";
-import useEventPublisher from "Feed/EventPublisher";
+import useEventPublisher from "Hooks/useEventPublisher";
import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "SnortApi";
import SendSats from "Element/SendSats";
diff --git a/packages/app/src/State/NoteCreator.ts b/packages/app/src/State/NoteCreator.ts
deleted file mode 100644
index 60f9a955..00000000
--- a/packages/app/src/State/NoteCreator.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { NostrEvent, TaggedNostrEvent } from "@snort/system";
-import { ZapTarget } from "Zapper";
-
-interface NoteCreatorStore {
- show: boolean;
- note: string;
- error: string;
- active: boolean;
- preview?: NostrEvent;
- replyTo?: TaggedNostrEvent;
- showAdvanced: boolean;
- selectedCustomRelays: false | Array
;
- zapSplits?: Array;
- sensitive: string;
- pollOptions?: Array;
- otherEvents: Array;
-}
-
-const InitState: NoteCreatorStore = {
- show: false,
- note: "",
- error: "",
- active: false,
- showAdvanced: false,
- selectedCustomRelays: false,
- sensitive: "",
- otherEvents: [],
-};
-
-const NoteCreatorSlice = createSlice({
- name: "NoteCreator",
- initialState: InitState,
- reducers: {
- setShow: (state, action: PayloadAction) => {
- state.show = action.payload;
- },
- setNote: (state, action: PayloadAction) => {
- state.note = action.payload;
- },
- setError: (state, action: PayloadAction) => {
- state.error = action.payload;
- },
- setActive: (state, action: PayloadAction) => {
- state.active = action.payload;
- },
- setPreview: (state, action: PayloadAction) => {
- state.preview = action.payload;
- },
- setReplyTo: (state, action: PayloadAction) => {
- state.replyTo = action.payload;
- },
- setShowAdvanced: (state, action: PayloadAction) => {
- state.showAdvanced = action.payload;
- },
- setSelectedCustomRelays: (state, action: PayloadAction>) => {
- state.selectedCustomRelays = action.payload;
- },
- setSensitive: (state, action: PayloadAction) => {
- state.sensitive = action.payload;
- },
- setPollOptions: (state, action: PayloadAction | undefined>) => {
- state.pollOptions = action.payload;
- },
- setOtherEvents: (state, action: PayloadAction>) => {
- state.otherEvents = action.payload;
- },
- setZapSplits: (state, action: PayloadAction>) => {
- state.zapSplits = action.payload;
- },
- reset: () => InitState,
- },
-});
-
-export const {
- setShow,
- setNote,
- setError,
- setActive,
- setPreview,
- setReplyTo,
- setShowAdvanced,
- setSelectedCustomRelays,
- setZapSplits,
- setSensitive,
- setPollOptions,
- setOtherEvents,
- reset,
-} = NoteCreatorSlice.actions;
-
-export const reducer = NoteCreatorSlice.reducer;
diff --git a/packages/app/src/State/NoteCreator.tsx b/packages/app/src/State/NoteCreator.tsx
new file mode 100644
index 00000000..b2b84e6c
--- /dev/null
+++ b/packages/app/src/State/NoteCreator.tsx
@@ -0,0 +1,94 @@
+import { ExternalStore } from "@snort/shared";
+import { NostrEvent, TaggedNostrEvent } from "@snort/system";
+import { ZapTarget } from "Zapper";
+import { useSyncExternalStore } from "react";
+import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
+
+interface NoteCreatorDataSnapshot {
+ show: boolean;
+ note: string;
+ error: string;
+ active: boolean;
+ advanced: boolean;
+ preview?: NostrEvent;
+ replyTo?: TaggedNostrEvent;
+ selectedCustomRelays?: Array;
+ zapSplits?: Array;
+ sensitive?: string;
+ pollOptions?: Array;
+ otherEvents?: Array;
+ reset: () => void;
+ update: (fn: (v: NoteCreatorDataSnapshot) => void) => void;
+}
+
+class NoteCreatorStore extends ExternalStore {
+ #data: NoteCreatorDataSnapshot;
+
+ constructor() {
+ super();
+ this.#data = {
+ show: false,
+ note: "",
+ error: "",
+ active: false,
+ advanced: false,
+ reset: () => {
+ this.#reset(this.#data);
+ this.notifyChange(this.#data);
+ },
+ update: (fn: (v: NoteCreatorDataSnapshot) => void) => {
+ fn(this.#data);
+ this.notifyChange(this.#data);
+ },
+ };
+ }
+
+ #reset(d: NoteCreatorDataSnapshot) {
+ d.show = false;
+ d.note = "";
+ d.error = "";
+ d.active = false;
+ d.advanced = false;
+ d.preview = undefined;
+ d.replyTo = undefined;
+ d.selectedCustomRelays = undefined;
+ d.zapSplits = undefined;
+ d.sensitive = undefined;
+ d.pollOptions = undefined;
+ d.otherEvents = undefined;
+ }
+
+ takeSnapshot(): NoteCreatorDataSnapshot {
+ const sn = {
+ ...this.#data,
+ reset: () => {
+ this.#reset(this.#data);
+ },
+ update: (fn: (v: NoteCreatorDataSnapshot) => void) => {
+ fn(this.#data);
+ this.notifyChange(this.#data);
+ },
+ } as NoteCreatorDataSnapshot;
+ return sn;
+ }
+}
+
+const NoteCreatorState = new NoteCreatorStore();
+
+export function useNoteCreator(
+ selector?: (v: NoteCreatorDataSnapshot) => T,
+) {
+ if (selector) {
+ return useSyncExternalStoreWithSelector(
+ c => NoteCreatorState.hook(c),
+ () => NoteCreatorState.snapshot(),
+ undefined,
+ selector,
+ );
+ } else {
+ return useSyncExternalStore(
+ c => NoteCreatorState.hook(c),
+ () => NoteCreatorState.snapshot() as T,
+ );
+ }
+}
diff --git a/packages/app/src/State/ReBroadcast.ts b/packages/app/src/State/ReBroadcast.ts
deleted file mode 100644
index 23cc253b..00000000
--- a/packages/app/src/State/ReBroadcast.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { NostrEvent } from "@snort/system";
-
-interface ReBroadcastStore {
- show: boolean;
- selectedCustomRelays: false | Array;
- note?: NostrEvent;
-}
-
-const InitState: ReBroadcastStore = {
- show: false,
- selectedCustomRelays: false,
-};
-
-const ReBroadcastSlice = createSlice({
- name: "ReBroadcast",
- initialState: InitState,
- reducers: {
- setShow: (state, action: PayloadAction) => {
- state.show = action.payload;
- },
- setNote: (state, action: PayloadAction) => {
- state.note = action.payload;
- },
- setSelectedCustomRelays: (state, action: PayloadAction>) => {
- state.selectedCustomRelays = action.payload;
- },
- reset: () => InitState,
- },
-});
-
-export const { setShow, setNote, setSelectedCustomRelays, reset } = ReBroadcastSlice.actions;
-
-export const reducer = ReBroadcastSlice.reducer;
diff --git a/packages/app/src/State/Store.ts b/packages/app/src/State/Store.ts
deleted file mode 100644
index 57bec805..00000000
--- a/packages/app/src/State/Store.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { configureStore } from "@reduxjs/toolkit";
-import { reducer as NoteCreatorReducer } from "State/NoteCreator";
-import { reducer as ReBroadcastReducer } from "State/ReBroadcast";
-
-const store = configureStore({
- reducer: {
- noteCreator: NoteCreatorReducer,
- reBroadcast: ReBroadcastReducer,
- },
-});
-
-export type RootState = ReturnType;
-export type AppDispatch = typeof store.dispatch;
-
-export default store;
diff --git a/packages/app/src/chat/nip4.ts b/packages/app/src/chat/nip4.ts
index f011d575..f9a3949b 100644
--- a/packages/app/src/chat/nip4.ts
+++ b/packages/app/src/chat/nip4.ts
@@ -8,6 +8,7 @@ import {
TLVEntryType,
encodeTLVEntries,
TaggedNostrEvent,
+ decodeTLV,
} from "@snort/system";
import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "chat";
import { debug } from "debug";
@@ -59,26 +60,32 @@ export class Nip4ChatSystem extends ExternalStore> implements ChatSy
{} as Record>,
);
- return [...Object.entries(chats)].map(([k, v]) => Nip4ChatSystem.createChatObj(k, v));
+ return [...Object.entries(chats)].map(([k, v]) =>
+ Nip4ChatSystem.createChatObj(
+ encodeTLVEntries("chat4" as NostrPrefix, {
+ type: TLVEntryType.Author,
+ value: k,
+ length: 32,
+ }),
+ v,
+ ),
+ );
}
static createChatObj(id: string, messages: Array) {
const last = lastReadInChat(id);
+ const participants = decodeTLV(id)
+ .filter(v => v.type === TLVEntryType.Author)
+ .map(v => ({
+ type: "pubkey",
+ id: v.value as string,
+ }));
return {
type: ChatType.DirectMessage,
- id: encodeTLVEntries("chat4" as NostrPrefix, {
- type: TLVEntryType.Author,
- value: id,
- length: 0,
- }),
+ id,
unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0),
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
- participants: [
- {
- type: "pubkey",
- id: id,
- },
- ],
+ participants,
messages: messages.map(m => ({
id: m.id,
created_at: m.created_at,
@@ -91,7 +98,7 @@ export class Nip4ChatSystem extends ExternalStore> implements ChatSy
},
})),
createMessage: async (msg, pub) => {
- return [await pub.sendDm(msg, id)];
+ return await Promise.all(participants.map(v => pub.sendDm(msg, v.id)));
},
sendMessage: (ev, system: SystemInterface) => {
ev.forEach(a => system.BroadcastEvent(a));
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
index 4ac8111d..c131f265 100644
--- a/packages/app/src/index.css
+++ b/packages/app/src/index.css
@@ -46,6 +46,7 @@
--header-padding-tb: 10px;
--btn-color: #fff;
--primary-gradient: linear-gradient(90deg, rgba(239, 150, 68, 1) 0%, rgba(123, 65, 246, 1) 100%);
+ --cashu-gradient: linear-gradient(90deg, #40b039, #adff2a);
}
::-webkit-scrollbar {
@@ -132,10 +133,26 @@ code {
}
}
+.bg-primary {
+ background: var(--primary-gradient);
+}
+
+.br {
+ border-radius: 16px;
+}
+
.p {
padding: 12px 16px;
}
+.p24 {
+ padding: 24px;
+}
+
+.uppercase {
+ text-transform: uppercase;
+}
+
.card {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
@@ -522,7 +539,7 @@ div.form-col {
height: 100vh;
}
-small.xs {
+.xs {
font-size: small;
}
diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx
index 720982a5..bb5c6c5a 100644
--- a/packages/app/src/index.tsx
+++ b/packages/app/src/index.tsx
@@ -7,24 +7,13 @@ import WasmPath from "@snort/system-query/pkg/system_query_bg.wasm";
import { StrictMode } from "react";
import * as ReactDOM from "react-dom/client";
-import { Provider } from "react-redux";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
-import {
- EventPublisher,
- NostrSystem,
- ProfileLoaderService,
- Nip7Signer,
- PowWorker,
- QueryOptimizer,
- FlatReqFilter,
- ReqFilter,
-} from "@snort/system";
+import { NostrSystem, ProfileLoaderService, PowWorker, QueryOptimizer, FlatReqFilter, ReqFilter } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import * as serviceWorkerRegistration from "serviceWorkerRegistration";
import { IntlProvider } from "IntlProvider";
import { unwrap } from "SnortUtils";
-import Store from "State/Store";
import Layout from "Pages/Layout";
import LoginPage from "Pages/LoginPage";
import ProfilePage from "Pages/ProfilePage";
@@ -73,13 +62,9 @@ export const System = new NostrSystem({
relayMetrics: RelayMetrics,
queryOptimizer: WasmQueryOptimizer,
authHandler: async (c, r) => {
- const { publicKey, privateKey } = LoginStore.snapshot();
- if (privateKey) {
- const pub = EventPublisher.privateKey(privateKey);
- return await pub.nip42Auth(c, r);
- }
- if (publicKey) {
- const pub = new EventPublisher(new Nip7Signer(), publicKey);
+ const { id } = LoginStore.snapshot();
+ const pub = LoginStore.getPublisher(id);
+ if (pub) {
return await pub.nip42Auth(c, r);
}
},
@@ -218,12 +203,10 @@ export const router = createBrowserRouter([
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
root.render(
-
-
-
-
-
-
-
+
+
+
+
+
,
);
diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json
index 805232ab..e0721c8b 100644
--- a/packages/app/src/lang.json
+++ b/packages/app/src/lang.json
@@ -142,6 +142,9 @@
"3yk8fB": {
"defaultMessage": "Wallet"
},
+ "40VR6s": {
+ "defaultMessage": "Nostr Connect"
+ },
"450Fty": {
"defaultMessage": "None"
},
@@ -190,6 +193,12 @@
"5ykRmX": {
"defaultMessage": "Send zap"
},
+ "6/SF6e": {
+ "defaultMessage": "{n}
Cashu sats"
+ },
+ "6/hB3S": {
+ "defaultMessage": "Watch Replay"
+ },
"65BmHb": {
"defaultMessage": "Failed to proxy image from {host}, click here to load directly"
},
@@ -223,6 +232,9 @@
"89q5wc": {
"defaultMessage": "Confirm Reposts"
},
+ "8Kboo2": {
+ "defaultMessage": "Scan this QR code with your signer app to get started"
+ },
"8QDesP": {
"defaultMessage": "Zap {n} sats"
},
@@ -238,6 +250,10 @@
"8v1NN+": {
"defaultMessage": "Pairing phrase"
},
+ "8xNnhi": {
+ "defaultMessage": "Nostr Extension",
+ "description": "Login button for NIP7 key manager extension"
+ },
"9+Ddtu": {
"defaultMessage": "Next"
},
@@ -352,6 +368,9 @@
"Dh3hbq": {
"defaultMessage": "Auto Zap"
},
+ "Dn82AL": {
+ "defaultMessage": "Live"
+ },
"DtYelJ": {
"defaultMessage": "Transfer"
},
@@ -424,6 +443,9 @@
"GL8aXW": {
"defaultMessage": "Bookmarks ({n})"
},
+ "GQPtfk": {
+ "defaultMessage": "Join Stream"
+ },
"GSye7T": {
"defaultMessage": "Lightning Address"
},
@@ -449,9 +471,6 @@
"HAlOn1": {
"defaultMessage": "Name"
},
- "HF4YnO": {
- "defaultMessage": "Watch Live!"
- },
"HFls6j": {
"defaultMessage": "name will be available later"
},
@@ -539,6 +558,9 @@
"KoFlZg": {
"defaultMessage": "Enter mint URL"
},
+ "KtsyO0": {
+ "defaultMessage": "Enter Pin"
+ },
"LF5kYT": {
"defaultMessage": "Other Connections"
},
@@ -557,6 +579,10 @@
"LwYmVi": {
"defaultMessage": "Zaps on this note will be split to the following users."
},
+ "M10zFV": {
+ "defaultMessage": "Nostr Connect",
+ "description": "Login button for NIP-46 signer app"
+ },
"M3Oirc": {
"defaultMessage": "Debug Menus"
},
@@ -708,10 +734,6 @@
"SP0+yi": {
"defaultMessage": "Buy Subscription"
},
- "SX58hM": {
- "defaultMessage": "Copy",
- "description": "Button: Copy Cashu token"
- },
"SYQtZ7": {
"defaultMessage": "LN Address Proxy"
},
@@ -730,8 +752,8 @@
"TDR5ge": {
"defaultMessage": "Media in notes will automatically be shown for selected people, otherwise only the link will show"
},
- "TMfYfY": {
- "defaultMessage": "Cashu token"
+ "TP/cMX": {
+ "defaultMessage": "Ended"
},
"TpgeGw": {
"defaultMessage": "Hex Salt..",
@@ -743,9 +765,6 @@
"UDYlxu": {
"defaultMessage": "Pending Subscriptions"
},
- "ULotH9": {
- "defaultMessage": "Amount: {amount} sats"
- },
"UT7Nkj": {
"defaultMessage": "New Chat"
},
@@ -773,10 +792,6 @@
"VnXp8Z": {
"defaultMessage": "Avatar"
},
- "VtPV/B": {
- "defaultMessage": "Login with Extension (NIP-07)",
- "description": "Login button for NIP7 key manager extension"
- },
"VvaJst": {
"defaultMessage": "View Wallets"
},
@@ -887,6 +902,9 @@
"defaultMessage": "Install Extension",
"description": "Heading for install key manager extension"
},
+ "c2DTVd": {
+ "defaultMessage": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort."
+ },
"c35bj2": {
"defaultMessage": "If you have an enquiry about your NIP-05 order please DM {link}"
},
@@ -931,6 +949,9 @@
"e61Jf3": {
"defaultMessage": "Coming soon"
},
+ "e7VmYP": {
+ "defaultMessage": "Enter pin to unlock your private key"
+ },
"e7qqly": {
"defaultMessage": "Mark All Read"
},
@@ -1000,10 +1021,6 @@
"hMzcSq": {
"defaultMessage": "Messages"
},
- "hWSp+B": {
- "defaultMessage": "Nostr Connect (NIP-46)",
- "description": "Login button for NIP-46 signer app"
- },
"hY4lzx": {
"defaultMessage": "Supports"
},
@@ -1034,9 +1051,6 @@
"iNWbVV": {
"defaultMessage": "Handle"
},
- "iUsU2x": {
- "defaultMessage": "Mint: {url}"
- },
"iXPL0Z": {
"defaultMessage": "Can't login with private key on an insecure connection, please use a Nostr key manager extension instead"
},
@@ -1230,6 +1244,9 @@
"qtWLmt": {
"defaultMessage": "Like"
},
+ "qz9fty": {
+ "defaultMessage": "Incorrect pin"
+ },
"r3C4x/": {
"defaultMessage": "Software"
},
@@ -1410,5 +1427,8 @@
},
"zvCDao": {
"defaultMessage": "Automatically show latest notes"
+ },
+ "zwb6LR": {
+ "defaultMessage": "Mint: {url}"
}
}
diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json
index 216b763e..90891315 100644
--- a/packages/app/src/translations/en.json
+++ b/packages/app/src/translations/en.json
@@ -46,6 +46,7 @@
"3tVy+Z": "{n} Followers",
"3xCwbZ": "OR",
"3yk8fB": "Wallet",
+ "40VR6s": "Nostr Connect",
"450Fty": "None",
"47FYwb": "Cancel",
"4IPzdn": "Primary Developers",
@@ -62,6 +63,8 @@
"5u6iEc": "Transfer to Pubkey",
"5vMmmR": "Usernames are not unique on Nostr. The nostr address is your unique human-readable address that is unique to you upon registration.",
"5ykRmX": "Send zap",
+ "6/SF6e": "{n}
Cashu sats",
+ "6/hB3S": "Watch Replay",
"65BmHb": "Failed to proxy image from {host}, click here to load directly",
"6OSOXl": "Reason: {reason}",
"6Yfvvp": "Get an identifier",
@@ -73,11 +76,13 @@
"7hp70g": "NIP-05",
"8/vBbP": "Reposts ({n})",
"89q5wc": "Confirm Reposts",
+ "8Kboo2": "Scan this QR code with your signer app to get started",
"8QDesP": "Zap {n} sats",
"8Rkoyb": "Recipient",
"8Y6bZQ": "Invalid zap split: {input}",
"8g2vyB": "name too long",
"8v1NN+": "Pairing phrase",
+ "8xNnhi": "Nostr Extension",
"9+Ddtu": "Next",
"9HU8vw": "Reply",
"9SvQep": "Follows {n}",
@@ -115,6 +120,7 @@
"DZzCem": "Show latest {n} notes",
"DcL8P+": "Supporter",
"Dh3hbq": "Auto Zap",
+ "Dn82AL": "Live",
"DtYelJ": "Transfer",
"E8a4yq": "Follow some popular accounts",
"ELbg9p": "Data Providers",
@@ -139,6 +145,7 @@
"G1BGCg": "Select Wallet",
"GFOoEE": "Salt",
"GL8aXW": "Bookmarks ({n})",
+ "GQPtfk": "Join Stream",
"GSye7T": "Lightning Address",
"GUlSVG": "Claim your included Snort nostr address",
"Gcn9NQ": "Magnet Link",
@@ -147,7 +154,6 @@
"H0JBH6": "Log Out",
"H6/kLh": "Order Paid!",
"HAlOn1": "Name",
- "HF4YnO": "Watch Live!",
"HFls6j": "name will be available later",
"HOzFdo": "Muted",
"HWbkEK": "Clear cache and reload",
@@ -177,12 +183,14 @@
"KWuDfz": "I have saved my keys, continue",
"KahimY": "Unknown event kind: {kind}",
"KoFlZg": "Enter mint URL",
+ "KtsyO0": "Enter Pin",
"LF5kYT": "Other Connections",
"LXxsbk": "Anonymous",
"LgbKvU": "Comment",
"Lu5/Bj": "Open on Zapstr",
"Lw+I+J": "{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}",
"LwYmVi": "Zaps on this note will be split to the following users.",
+ "M10zFV": "Nostr Connect",
"M3Oirc": "Debug Menus",
"MBAYRO": "Shows \"Copy ID\" and \"Copy Event JSON\" in the context menu on each message",
"MI2jkA": "Not available:",
@@ -232,18 +240,16 @@
"SMO+on": "Send zap to {name}",
"SOqbe9": "Update Lightning Address",
"SP0+yi": "Buy Subscription",
- "SX58hM": "Copy",
"SYQtZ7": "LN Address Proxy",
"ShdEie": "Mark all read",
"Sjo1P4": "Custom",
"Ss0sWu": "Pay Now",
"StKzTE": "The author has marked this note as a sensitive topic",
"TDR5ge": "Media in notes will automatically be shown for selected people, otherwise only the link will show",
- "TMfYfY": "Cashu token",
+ "TP/cMX": "Ended",
"TpgeGw": "Hex Salt..",
"Tpy00S": "People",
"UDYlxu": "Pending Subscriptions",
- "ULotH9": "Amount: {amount} sats",
"UT7Nkj": "New Chat",
"UUPFlt": "Users must accept the content warning to show the content of your note.",
"Up5U7K": "Block",
@@ -253,7 +259,6 @@
"VR5eHw": "Public key (npub/nprofile)",
"VlJkSk": "{n} muted",
"VnXp8Z": "Avatar",
- "VtPV/B": "Login with Extension (NIP-07)",
"VvaJst": "View Wallets",
"Vx7Zm2": "How do keys work?",
"W1yoZY": "It looks like you dont have any subscriptions, you can get one {link}",
@@ -290,6 +295,7 @@
"brAXSu": "Pick a username",
"bxv59V": "Just now",
"c+oiJe": "Install Extension",
+ "c2DTVd": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort.",
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
"c3g2hL": "Broadcast Again",
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
@@ -304,6 +310,7 @@
"d7d0/x": "LN Address",
"dOQCL8": "Display name",
"e61Jf3": "Coming soon",
+ "e7VmYP": "Enter pin to unlock your private key",
"e7qqly": "Mark All Read",
"eHAneD": "Reaction emoji",
"eJj8HD": "Get Verified",
@@ -327,7 +334,6 @@
"h8XMJL": "Badges",
"hK5ZDk": "the world",
"hMzcSq": "Messages",
- "hWSp+B": "Nostr Connect (NIP-46)",
"hY4lzx": "Supports",
"hicxcO": "Show replies",
"hmZ3Bz": "Media",
@@ -338,7 +344,6 @@
"iEoXYx": "DeepL translations",
"iGT1eE": "Prevent fake accounts from imitating you",
"iNWbVV": "Handle",
- "iUsU2x": "Mint: {url}",
"iXPL0Z": "Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
"ieGrWo": "Follow",
"itPgxd": "Profile",
@@ -402,6 +407,7 @@
"qkvYUb": "Add to Profile",
"qmJ8kD": "Translation failed",
"qtWLmt": "Like",
+ "qz9fty": "Incorrect pin",
"r3C4x/": "Software",
"r5srDR": "Enter wallet password",
"rT14Ow": "Add Relays",
@@ -460,5 +466,6 @@
"zcaOTs": "Zap amount in sats",
"zjJZBd": "You're ready!",
"zonsdq": "Failed to load LNURL service",
- "zvCDao": "Automatically show latest notes"
+ "zvCDao": "Automatically show latest notes",
+ "zwb6LR": "Mint: {url}"
}
diff --git a/packages/shared/src/external-store.ts b/packages/shared/src/external-store.ts
index 08f9b4fd..21d3f035 100644
--- a/packages/shared/src/external-store.ts
+++ b/packages/shared/src/external-store.ts
@@ -9,7 +9,7 @@ export interface HookFilter {
*/
export abstract class ExternalStore {
#hooks: Array> = [];
- #snapshot: Readonly = {} as Readonly;
+ #snapshot: TSnapshot = {} as TSnapshot;
#changed = true;
hook(fn: HookFn) {
diff --git a/packages/system-query/benches/basic.rs b/packages/system-query/benches/basic.rs
index 1e48e6ab..5bdef41a 100644
--- a/packages/system-query/benches/basic.rs
+++ b/packages/system-query/benches/basic.rs
@@ -24,6 +24,8 @@ fn criterion_benchmark(c: &mut Criterion) {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -39,6 +41,8 @@ fn criterion_benchmark(c: &mut Criterion) {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
diff --git a/packages/system-query/pkg/system_query_bg.wasm b/packages/system-query/pkg/system_query_bg.wasm
index 9ab1834b..15d2cdbf 100644
Binary files a/packages/system-query/pkg/system_query_bg.wasm and b/packages/system-query/pkg/system_query_bg.wasm differ
diff --git a/packages/system-query/src/diff.rs b/packages/system-query/src/diff.rs
index 0e1a35d5..58b2af90 100644
--- a/packages/system-query/src/diff.rs
+++ b/packages/system-query/src/diff.rs
@@ -28,6 +28,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -42,6 +44,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -63,6 +67,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -78,6 +84,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -92,6 +100,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -111,6 +121,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -130,6 +142,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -144,6 +158,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -162,6 +178,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
diff --git a/packages/system-query/src/filter.rs b/packages/system-query/src/filter.rs
index 333b2ee8..93f7b075 100644
--- a/packages/system-query/src/filter.rs
+++ b/packages/system-query/src/filter.rs
@@ -29,6 +29,10 @@ pub struct ReqFilter {
pub d_tag: Option>,
#[serde(rename = "#r", skip_serializing_if = "Option::is_none")]
pub r_tag: Option>,
+ #[serde(rename = "#a", skip_serializing_if = "Option::is_none")]
+ pub a_tag: Option>,
+ #[serde(rename = "#g", skip_serializing_if = "Option::is_none")]
+ pub g_tag: Option>,
#[serde(rename = "search", skip_serializing_if = "Option::is_none")]
pub search: Option>,
#[serde(rename = "since", skip_serializing_if = "Option::is_none")]
@@ -64,6 +68,10 @@ pub struct FlatReqFilter {
pub d_tag: Option,
#[serde(rename = "#r", skip_serializing_if = "Option::is_none")]
pub r_tag: Option,
+ #[serde(rename = "#a", skip_serializing_if = "Option::is_none")]
+ pub a_tag: Option,
+ #[serde(rename = "#g", skip_serializing_if = "Option::is_none")]
+ pub g_tag: Option,
#[serde(rename = "search", skip_serializing_if = "Option::is_none")]
pub search: Option,
#[serde(rename = "since", skip_serializing_if = "Option::is_none")]
@@ -145,6 +153,8 @@ impl From> for ReqFilter {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -159,6 +169,8 @@ impl From> for ReqFilter {
array_prop_append(&x.t_tag, &mut acc.t_tag);
array_prop_append(&x.d_tag, &mut acc.d_tag);
array_prop_append(&x.r_tag, &mut acc.r_tag);
+ array_prop_append(&x.a_tag, &mut acc.a_tag);
+ array_prop_append(&x.g_tag, &mut acc.g_tag);
array_prop_append(&x.search, &mut acc.search);
acc.since = x.since;
acc.until = x.until;
@@ -180,6 +192,8 @@ impl From> for ReqFilter {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -194,6 +208,8 @@ impl From> for ReqFilter {
array_prop_append_vec(&x.t_tag, &mut acc.t_tag);
array_prop_append_vec(&x.d_tag, &mut acc.d_tag);
array_prop_append_vec(&x.r_tag, &mut acc.r_tag);
+ array_prop_append_vec(&x.a_tag, &mut acc.a_tag);
+ array_prop_append_vec(&x.g_tag, &mut acc.g_tag);
array_prop_append_vec(&x.search, &mut acc.search);
acc.since = x.since;
acc.until = x.until;
@@ -265,6 +281,20 @@ impl Into> for &ReqFilter {
.collect();
inputs.push(t_ids);
}
+ if let Some(a_tags) = &self.a_tag {
+ let t_ids = a_tags
+ .iter()
+ .map(|z| StringOrNumberEntry::String(("a_tag", z)))
+ .collect();
+ inputs.push(t_ids);
+ }
+ if let Some(g_tags) = &self.g_tag {
+ let t_ids = g_tags
+ .iter()
+ .map(|z| StringOrNumberEntry::String(("g_tag", z)))
+ .collect();
+ inputs.push(t_ids);
+ }
if let Some(search) = &self.search {
let t_ids = search
.iter()
@@ -339,6 +369,22 @@ impl Into> for &ReqFilter {
}
None
}),
+ a_tag: p.iter().find_map(|q| {
+ if let StringOrNumberEntry::String((k, v)) = q {
+ if (*k).eq("a_tag") {
+ return Some((*v).to_string());
+ }
+ }
+ None
+ }),
+ g_tag: p.iter().find_map(|q| {
+ if let StringOrNumberEntry::String((k, v)) = q {
+ if (*k).eq("g_tag") {
+ return Some((*v).to_string());
+ }
+ }
+ None
+ }),
search: p.iter().find_map(|q| {
if let StringOrNumberEntry::String((k, v)) = q {
if (*k).eq("search") {
@@ -355,6 +401,7 @@ impl Into> for &ReqFilter {
ret
}
}
+
impl Distance for ReqFilter {
fn distance(&self, b: &Self) -> u32 {
let mut ret = 0u32;
@@ -367,6 +414,7 @@ impl Distance for ReqFilter {
ret += prop_dist_vec(&self.d_tag, &b.d_tag);
ret += prop_dist_vec(&self.r_tag, &b.r_tag);
ret += prop_dist_vec(&self.t_tag, &b.t_tag);
+ ret += prop_dist_vec(&self.a_tag, &b.a_tag);
ret += prop_dist_vec(&self.search, &b.search);
ret
@@ -464,6 +512,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -471,7 +521,7 @@ mod tests {
e_tag: None,
};
- let output : Vec = (&input).into();
+ let output: Vec = (&input).into();
let expected = vec![
FlatReqFilter {
author: Some("a".to_owned()),
@@ -481,6 +531,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -495,6 +547,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -509,6 +563,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -523,6 +579,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -537,6 +595,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -551,6 +611,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -565,6 +627,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -579,6 +643,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -593,6 +659,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -607,6 +675,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -621,6 +691,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -635,6 +707,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -649,6 +723,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -663,6 +739,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -677,6 +755,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -691,6 +771,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -705,6 +787,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
@@ -719,6 +803,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: Some(99),
until: None,
diff --git a/packages/system-query/src/lib.rs b/packages/system-query/src/lib.rs
index 001f2ecc..ad46a46a 100644
--- a/packages/system-query/src/lib.rs
+++ b/packages/system-query/src/lib.rs
@@ -74,6 +74,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
authors: Some(HashSet::from([
"kieran".to_string(),
"snort".to_string(),
@@ -94,6 +96,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -109,6 +113,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -122,6 +128,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
diff --git a/packages/system-query/src/merge.rs b/packages/system-query/src/merge.rs
index b8e797be..56349ef1 100644
--- a/packages/system-query/src/merge.rs
+++ b/packages/system-query/src/merge.rs
@@ -1,9 +1,9 @@
use crate::filter::CanMerge;
pub fn merge<'a, T, Z>(all: Vec<&'a T>) -> Vec
-where
- T: CanMerge,
- for<'b> Z: CanMerge + From> + From>,
+ where
+ T: CanMerge,
+ for<'b> Z: CanMerge + From> + From>,
{
let mut ret: Vec = merge_once(all);
loop {
@@ -17,9 +17,9 @@ where
}
fn merge_once<'a, T, Z>(all: Vec<&'a T>) -> Vec
-where
- T: CanMerge,
- Z: From>,
+ where
+ T: CanMerge,
+ Z: From>,
{
let mut ret: Vec = vec![];
if all.is_empty() {
@@ -66,6 +66,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -80,6 +82,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -94,6 +98,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -108,6 +114,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -122,6 +130,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -144,6 +154,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -158,6 +170,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -173,6 +187,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -192,6 +208,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -206,6 +224,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -220,6 +240,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -241,6 +263,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -255,6 +279,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -269,6 +295,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -283,6 +311,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -297,6 +327,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -311,6 +343,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -325,6 +359,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -339,6 +375,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -353,6 +391,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -373,6 +413,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -387,6 +429,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -401,6 +445,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -415,6 +461,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
@@ -429,6 +477,8 @@ mod tests {
t_tag: None,
d_tag: None,
r_tag: None,
+ a_tag: None,
+ g_tag: None,
search: None,
since: None,
until: None,
diff --git a/packages/system/src/encrypted.ts b/packages/system/src/encrypted.ts
new file mode 100644
index 00000000..707bb1b3
--- /dev/null
+++ b/packages/system/src/encrypted.ts
@@ -0,0 +1,69 @@
+import { scryptAsync } from "@noble/hashes/scrypt";
+import { sha256 } from "@noble/hashes/sha256";
+import { hmac } from "@noble/hashes/hmac";
+import { bytesToHex, hexToBytes, randomBytes } from "@noble/hashes/utils";
+import { base64 } from "@scure/base";
+import { streamXOR as xchacha20 } from "@stablelib/xchacha20";
+
+export class InvalidPinError extends Error {
+ constructor() {
+ super();
+ }
+}
+
+/**
+ * Pin protected data
+ */
+export class PinEncrypted {
+ static readonly #opts = { N: 2 ** 20, r: 8, p: 1, dkLen: 32 };
+ #decrypted?: Uint8Array;
+ #encrypted: PinEncryptedPayload;
+
+ constructor(enc: PinEncryptedPayload) {
+ this.#encrypted = enc;
+ }
+
+ get value() {
+ if (!this.#decrypted) throw new Error("Content has not been decrypted yet");
+ return bytesToHex(this.#decrypted);
+ }
+
+ async decrypt(pin: string) {
+ const key = await scryptAsync(pin, base64.decode(this.#encrypted.salt), PinEncrypted.#opts);
+ const ciphertext = base64.decode(this.#encrypted.ciphertext);
+ const nonce = base64.decode(this.#encrypted.iv);
+ const plaintext = xchacha20(key, nonce, ciphertext, new Uint8Array(32));
+ if (plaintext.length !== 32) throw new InvalidPinError();
+ const mac = base64.encode(hmac(sha256, key, plaintext));
+ if (mac !== this.#encrypted.mac) throw new InvalidPinError();
+ this.#decrypted = plaintext;
+ }
+
+ toPayload() {
+ return this.#encrypted;
+ }
+
+ static async create(content: string, pin: string) {
+ const salt = randomBytes(24);
+ const nonce = randomBytes(24);
+ const plaintext = hexToBytes(content);
+ const key = await scryptAsync(pin, salt, PinEncrypted.#opts);
+ const mac = base64.encode(hmac(sha256, key, plaintext));
+ const ciphertext = xchacha20(key, nonce, plaintext, new Uint8Array(32));
+ const ret = new PinEncrypted({
+ salt: base64.encode(salt),
+ ciphertext: base64.encode(ciphertext),
+ iv: base64.encode(nonce),
+ mac,
+ });
+ ret.#decrypted = plaintext;
+ return ret;
+ }
+}
+
+export interface PinEncryptedPayload {
+ salt: string; // for KDF
+ ciphertext: string;
+ iv: string;
+ mac: string;
+}
diff --git a/packages/system/src/event-publisher.ts b/packages/system/src/event-publisher.ts
index ab62e21a..87201330 100644
--- a/packages/system/src/event-publisher.ts
+++ b/packages/system/src/event-publisher.ts
@@ -9,6 +9,7 @@ import {
HexKey,
Lists,
NostrEvent,
+ NostrLink,
NotSignedNostrEvent,
PowMiner,
PrivateKeySigner,
@@ -185,10 +186,11 @@ export class EventPublisher {
const thread = EventExt.extractThread(replyTo);
if (thread) {
- if (thread.root || thread.replyTo) {
- eb.tag(["e", thread.root?.value ?? thread.replyTo?.value ?? "", "", "root"]);
+ const rootOrReplyAsRoot = thread.root || thread.replyTo;
+ if (rootOrReplyAsRoot) {
+ eb.tag([rootOrReplyAsRoot.key, rootOrReplyAsRoot.value ?? "", rootOrReplyAsRoot.relay ?? "", "root"]);
}
- eb.tag(["e", replyTo.id, replyTo.relays?.[0] ?? "", "reply"]);
+ eb.tag([...(NostrLink.fromEvent(replyTo).toEventTag() ?? []), "reply"]);
eb.tag(["p", replyTo.pubkey]);
for (const pk of thread.pubKeys) {
@@ -198,7 +200,7 @@ export class EventPublisher {
eb.tag(["p", pk]);
}
} else {
- eb.tag(["e", replyTo.id, "", "reply"]);
+ eb.tag([...(NostrLink.fromEvent(replyTo).toEventTag() ?? []), "reply"]);
// dont tag self in replies
if (replyTo.pubkey !== this.#pubKey) {
eb.tag(["p", replyTo.pubkey]);
diff --git a/packages/system/src/gossip-model.ts b/packages/system/src/gossip-model.ts
index 2f2d9be8..caa05792 100644
--- a/packages/system/src/gossip-model.ts
+++ b/packages/system/src/gossip-model.ts
@@ -84,7 +84,7 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array<
},
});
}
- debug("GOSSIP")("Picked %o", picked);
+ debug("GOSSIP")("Picked %O => %O", filter, picked);
return picked;
}
@@ -119,7 +119,7 @@ export function splitFlatByWriteRelays(cache: RelayCache, input: Array implements System
{
filters: [{ ...f, ids: [...resultIds] }],
strategy: RequestStrategy.ExplicitRelays,
- relay: "",
+ relay: qSend.relay,
},
cacheResults as Array,
);
diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts
index a9f20b6f..46a860e6 100644
--- a/packages/system/src/query.ts
+++ b/packages/system/src/query.ts
@@ -202,7 +202,7 @@ export class Query implements QueryBase {
*/
insertCompletedTrace(subq: BuiltRawReqFilter, data: Readonly>) {
const qt = new QueryTrace(
- "",
+ subq.relay,
subq.filters,
"",
() => {
diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts
index 75feeb01..e8ae703f 100644
--- a/packages/system/src/request-builder.ts
+++ b/packages/system/src/request-builder.ts
@@ -1,6 +1,6 @@
import debug from "debug";
import { v4 as uuid } from "uuid";
-import { appendDedupe, sanitizeRelayUrl, unixNowMs, unwrap } from "@snort/shared";
+import { appendDedupe, dedupe, sanitizeRelayUrl, unixNowMs, unwrap } from "@snort/shared";
import EventKind from "./event-kind";
import { NostrLink, NostrPrefix, SystemInterface } from "index";
@@ -113,7 +113,7 @@ export class RequestBuilder {
const diff = system.QueryOptimizer.getDiff(prev, this.buildRaw());
const ts = unixNowMs() - start;
- this.#log("buildDiff %s %d ms +%d %O=>%O", this.id, ts, diff.length, prev, this.buildRaw());
+ this.#log("buildDiff %s %d ms +%d", this.id, ts, diff.length);
if (diff.length > 0) {
return splitFlatByWriteRelays(system.RelayCache, diff).map(a => {
return {
@@ -219,9 +219,9 @@ export class RequestFilterBuilder {
return this;
}
- tag(key: "e" | "p" | "d" | "t" | "r" | "a" | "g", value?: Array) {
+ tag(key: "e" | "p" | "d" | "t" | "r" | "a" | "g" | string, value?: Array) {
if (!value) return this;
- this.#filter[`#${key}`] = appendDedupe(this.#filter[`#${key}`], value);
+ this.#filter[`#${key}`] = appendDedupe(this.#filter[`#${key}`] as Array, value);
return this;
}
@@ -239,28 +239,25 @@ export class RequestFilterBuilder {
this.tag("d", [link.id])
.kinds([unwrap(link.kind)])
.authors([unwrap(link.author)]);
- link.relays?.forEach(v => this.relay(v));
} else {
this.ids([link.id]);
- link.relays?.forEach(v => this.relay(v));
}
+ link.relays?.forEach(v => this.relay(v));
return this;
}
/**
* Get replies to link with e/a tags
*/
- replyToLink(link: NostrLink) {
- if (link.type === NostrPrefix.Address) {
- this.tag("a", [`${link.kind}:${link.author}:${link.id}`]);
- link.relays?.forEach(v => this.relay(v));
- } else if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) {
- this.tag("p", [link.id]);
- link.relays?.forEach(v => this.relay(v));
- } else {
- this.tag("e", [link.id]);
- link.relays?.forEach(v => this.relay(v));
- }
+ replyToLink(links: Array) {
+ const types = dedupe(links.map(a => a.type));
+ if (types.length > 1) throw new Error("Cannot add multiple links of different kinds");
+
+ const tags = links.map(a => unwrap(a.toEventTag()));
+ this.tag(
+ tags[0][0],
+ tags.map(v => v[1]),
+ );
return this;
}
@@ -293,7 +290,7 @@ export class RequestFilterBuilder {
return [
{
- filters: [this.filter],
+ filters: [this.#filter],
relay: "",
strategy: RequestStrategy.DefaultRelays,
},
diff --git a/packages/system/src/request-trim.ts b/packages/system/src/request-trim.ts
index b9a6a92d..a1de5f64 100644
--- a/packages/system/src/request-trim.ts
+++ b/packages/system/src/request-trim.ts
@@ -6,19 +6,8 @@ import { ReqFilter } from "nostr";
export function trimFilters(filters: Array) {
const fNew = [];
for (const f of filters) {
- let arrays = 0;
- for (const [k, v] of Object.entries(f)) {
- if (Array.isArray(v)) {
- arrays++;
- if (v.length === 0) {
- delete f[k];
- }
- }
- }
-
- if (arrays > 0 && Object.entries(f).some(v => Array.isArray(v))) {
- fNew.push(f);
- } else if (arrays === 0) {
+ const ent = Object.entries(f).filter(([, v]) => Array.isArray(v));
+ if (ent.every(([, v]) => (v as Array).length > 0)) {
fNew.push(f);
}
}
diff --git a/yarn.lock b/yarn.lock
index c3c267e4..8ba3aaa3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1376,7 +1376,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
+"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.8.4":
version: 7.22.11
resolution: "@babel/runtime@npm:7.22.11"
dependencies:
@@ -2507,26 +2507,6 @@ __metadata:
languageName: node
linkType: hard
-"@reduxjs/toolkit@npm:^1.9.1":
- version: 1.9.5
- resolution: "@reduxjs/toolkit@npm:1.9.5"
- dependencies:
- immer: ^9.0.21
- redux: ^4.2.1
- redux-thunk: ^2.4.2
- reselect: ^4.1.8
- peerDependencies:
- react: ^16.9.0 || ^17.0.0 || ^18
- react-redux: ^7.2.1 || ^8.0.2
- peerDependenciesMeta:
- react:
- optional: true
- react-redux:
- optional: true
- checksum: 54672c5593d05208af577e948a338f23128d3aa01ef056ab0d40bcfa14400cf6566be99e11715388f12c1d7655cdf7c5c6b63cb92eb0fecf996c454a46a3914c
- languageName: node
- linkType: hard
-
"@remix-run/router@npm:1.8.0":
version: 1.8.0
resolution: "@remix-run/router@npm:1.8.0"
@@ -2705,7 +2685,6 @@ __metadata:
"@lightninglabs/lnc-web": ^0.2.3-alpha
"@noble/curves": ^1.0.0
"@noble/hashes": ^1.2.0
- "@reduxjs/toolkit": ^1.9.1
"@scure/base": ^1.1.1
"@scure/bip32": ^1.3.0
"@scure/bip39": ^1.1.1
@@ -2719,6 +2698,7 @@ __metadata:
"@types/node": ^20.4.1
"@types/react": ^18.0.26
"@types/react-dom": ^18.0.10
+ "@types/use-sync-external-store": ^0.0.4
"@types/uuid": ^9.0.2
"@types/webscopeio__react-textarea-autocomplete": ^4.7.2
"@types/webtorrent": ^0.109.3
@@ -2751,7 +2731,6 @@ __metadata:
react-dom: ^18.2.0
react-intersection-observer: ^9.4.1
react-intl: ^6.4.4
- react-redux: ^8.0.5
react-router-dom: ^6.5.0
react-textarea-autosize: ^8.4.0
react-twitter-embed: ^4.0.4
@@ -2762,6 +2741,7 @@ __metadata:
ts-loader: ^9.4.4
typescript: ^5.2.2
use-long-press: ^2.0.3
+ use-sync-external-store: ^1.2.0
uuid: ^9.0.0
webpack: ^5.88.2
webpack-bundle-analyzer: ^4.8.0
@@ -3590,10 +3570,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/use-sync-external-store@npm:^0.0.3":
- version: 0.0.3
- resolution: "@types/use-sync-external-store@npm:0.0.3"
- checksum: 161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e
+"@types/use-sync-external-store@npm:^0.0.4":
+ version: 0.0.4
+ resolution: "@types/use-sync-external-store@npm:0.0.4"
+ checksum: f8bf56b14f28fda6d0215281d50623d5affd17c549ba46bcfcfbb97c6301a583066c0477856260ae6feadcaa714c46cd45678e76b74da0f6f8b364aec07bd854
languageName: node
linkType: hard
@@ -7614,13 +7594,6 @@ __metadata:
languageName: node
linkType: hard
-"immer@npm:^9.0.21":
- version: 9.0.21
- resolution: "immer@npm:9.0.21"
- checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432
- languageName: node
- linkType: hard
-
"import-fresh@npm:^3.2.1":
version: 3.3.0
resolution: "import-fresh@npm:3.3.0"
@@ -11273,38 +11246,6 @@ __metadata:
languageName: node
linkType: hard
-"react-redux@npm:^8.0.5":
- version: 8.1.2
- resolution: "react-redux@npm:8.1.2"
- dependencies:
- "@babel/runtime": ^7.12.1
- "@types/hoist-non-react-statics": ^3.3.1
- "@types/use-sync-external-store": ^0.0.3
- hoist-non-react-statics: ^3.3.2
- react-is: ^18.0.0
- use-sync-external-store: ^1.0.0
- peerDependencies:
- "@types/react": ^16.8 || ^17.0 || ^18.0
- "@types/react-dom": ^16.8 || ^17.0 || ^18.0
- react: ^16.8 || ^17.0 || ^18.0
- react-dom: ^16.8 || ^17.0 || ^18.0
- react-native: ">=0.59"
- redux: ^4 || ^5.0.0-beta.0
- peerDependenciesMeta:
- "@types/react":
- optional: true
- "@types/react-dom":
- optional: true
- react-dom:
- optional: true
- react-native:
- optional: true
- redux:
- optional: true
- checksum: 4d5976b0f721e4148475871fcabce2fee875cc7f70f9a292f3370d63b38aa1dd474eb303c073c5555f3e69fc732f3bac05303def60304775deb28361e3f4b7cc
- languageName: node
- linkType: hard
-
"react-router-dom@npm:^6.5.0":
version: 6.15.0
resolution: "react-router-dom@npm:6.15.0"
@@ -11522,24 +11463,6 @@ __metadata:
languageName: node
linkType: hard
-"redux-thunk@npm:^2.4.2":
- version: 2.4.2
- resolution: "redux-thunk@npm:2.4.2"
- peerDependencies:
- redux: ^4
- checksum: c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c
- languageName: node
- linkType: hard
-
-"redux@npm:^4.2.1":
- version: 4.2.1
- resolution: "redux@npm:4.2.1"
- dependencies:
- "@babel/runtime": ^7.9.2
- checksum: f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd
- languageName: node
- linkType: hard
-
"regenerate-unicode-properties@npm:^10.1.0":
version: 10.1.0
resolution: "regenerate-unicode-properties@npm:10.1.0"
@@ -11670,13 +11593,6 @@ __metadata:
languageName: node
linkType: hard
-"reselect@npm:^4.1.8":
- version: 4.1.8
- resolution: "reselect@npm:4.1.8"
- checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e
- languageName: node
- linkType: hard
-
"resolve-cwd@npm:^3.0.0":
version: 3.0.0
resolution: "resolve-cwd@npm:3.0.0"
@@ -13424,7 +13340,7 @@ __metadata:
languageName: node
linkType: hard
-"use-sync-external-store@npm:^1.0.0":
+"use-sync-external-store@npm:^1.2.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
peerDependencies: