Various
This commit is contained in:
parent
4b57d57f94
commit
71f7f728fd
@ -6,7 +6,6 @@
|
||||
"@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",
|
||||
@ -27,7 +26,6 @@
|
||||
"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",
|
||||
|
@ -2,7 +2,7 @@ import { EventKind, EventPublisher, RequestBuilder, TaggedNostrEvent } from "@sn
|
||||
import { UnwrappedGift, db } from "Db";
|
||||
import { findTag, unwrap } from "SnortUtils";
|
||||
import { RefreshFeedCache } from "./RefreshFeedCache";
|
||||
import { LoginSession } from "Login";
|
||||
import { LoginSession, LoginSessionType } from "Login";
|
||||
|
||||
export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
||||
constructor() {
|
||||
@ -15,7 +15,7 @@ export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
||||
|
||||
buildSub(session: LoginSession, rb: RequestBuilder): void {
|
||||
const pubkey = session.publicKey;
|
||||
if (pubkey) {
|
||||
if (pubkey && session.type === LoginSessionType.PrivateKey) {
|
||||
rb.withFilter().kinds([EventKind.GiftWrap]).tag("p", [pubkey]).since(this.newest());
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ export default function BadgeList({ badges }: { badges: TaggedNostrEvent[] }) {
|
||||
))}
|
||||
</div>
|
||||
{showModal && (
|
||||
<Modal className="reactions-modal" onClose={() => setShowModal(false)}>
|
||||
<Modal id="badges" className="reactions-modal" onClose={() => setShowModal(false)}>
|
||||
<div className="reactions-view">
|
||||
<div className="close" onClick={() => setShowModal(false)}>
|
||||
<Icon name="close" />
|
||||
|
@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import Text from "Element/Text";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
@ -2,7 +2,7 @@ import { NostrLink } from "@snort/system";
|
||||
import { useArticles } from "Feed/ArticlesFeed";
|
||||
import { orderDescending } from "SnortUtils";
|
||||
import Note from "../Note";
|
||||
import { useReactions } from "Feed/FeedReactions";
|
||||
import { useReactions } from "Feed/Reactions";
|
||||
|
||||
export default function Articles() {
|
||||
const data = useArticles();
|
||||
|
@ -2,7 +2,7 @@ import "./FollowButton.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "@snort/system";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { parseId } from "SnortUtils";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
|
@ -2,13 +2,16 @@ import { ReactNode } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "@snort/system";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { System } from "index";
|
||||
|
||||
import messages from "./messages";
|
||||
import { FollowsFeed } from "Cache";
|
||||
import AsyncButton from "./AsyncButton";
|
||||
import { setFollows } from "Login";
|
||||
import { dedupe } from "@snort/shared";
|
||||
|
||||
export interface FollowListBaseProps {
|
||||
pubkeys: HexKey[];
|
||||
@ -30,13 +33,15 @@ export default function FollowListBase({
|
||||
profileActions,
|
||||
}: FollowListBaseProps) {
|
||||
const publisher = useEventPublisher();
|
||||
const { follows, relays } = useLogin();
|
||||
const login = useLogin();
|
||||
|
||||
async function followAll() {
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList([...pubkeys, ...follows.item], relays.item);
|
||||
await FollowsFeed.backFill(System, pubkeys);
|
||||
const newFollows = dedupe([...pubkeys, ...login.follows.item]);
|
||||
const ev = await publisher.contactList(newFollows, login.relays.item);
|
||||
System.BroadcastEvent(ev);
|
||||
await FollowsFeed.backFill(System, pubkeys);
|
||||
setFollows(login, newFollows, ev.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,9 +51,9 @@ export default function FollowListBase({
|
||||
<div className="flex mt10 mb10">
|
||||
<div className="f-grow bold">{title}</div>
|
||||
{actions}
|
||||
<button className="transparent" type="button" onClick={() => followAll()}>
|
||||
<AsyncButton className="transparent" type="button" onClick={() => followAll()}>
|
||||
<FormattedMessage {...messages.FollowAll} />
|
||||
</button>
|
||||
</AsyncButton>
|
||||
</div>
|
||||
)}
|
||||
{pubkeys?.map(a => (
|
||||
|
@ -7,15 +7,15 @@ import messages from "./messages";
|
||||
|
||||
export default function LogoutButton() {
|
||||
const navigate = useNavigate();
|
||||
const publicKey = useLogin().publicKey;
|
||||
const login = useLogin();
|
||||
|
||||
if (!publicKey) return;
|
||||
if (!login.publicKey) return;
|
||||
return (
|
||||
<button
|
||||
className="secondary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
logout(publicKey);
|
||||
logout(login.id);
|
||||
navigate("/");
|
||||
}}>
|
||||
<FormattedMessage {...messages.Logout} />
|
||||
|
@ -20,6 +20,7 @@
|
||||
width: 500px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
--border-color: var(--gray);
|
||||
}
|
||||
|
||||
.modal-body button.secondary:hover {
|
||||
|
@ -1,26 +1,17 @@
|
||||
import "./Modal.css";
|
||||
import { useEffect, MouseEventHandler, ReactNode } from "react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface ModalProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
onClose?: MouseEventHandler;
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Modal(props: ModalProps) {
|
||||
const onClose = props.onClose || (() => undefined);
|
||||
const className = props.className || "";
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add("scroll-lock");
|
||||
return () => document.body.classList.remove("scroll-lock");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`modal ${className}`} onClick={onClose}>
|
||||
return <div className={`modal${props.className ? ` ${props.className}` : ""}`} onClick={props.onClose}>
|
||||
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import AsyncButton from "Element/AsyncButton";
|
||||
import SendSats from "Element/SendSats";
|
||||
import Copy from "Element/Copy";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { debounce } from "SnortUtils";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import SnortServiceProvider from "Nip05/SnortServiceProvider";
|
||||
|
@ -6,7 +6,7 @@ import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { TaggedNostrEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt, parseZap, NostrLink } from "@snort/system";
|
||||
|
||||
import { System } from "index";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import Icon from "Icons/Icon";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import Text from "Element/Text";
|
||||
|
@ -1,23 +1,17 @@
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { TranslateHost } from "Const";
|
||||
import { System } from "index";
|
||||
import Icon from "Icons/Icon";
|
||||
import { setPinned, setBookmarked } from "Login";
|
||||
import {
|
||||
setNote as setReBroadcastNote,
|
||||
setShow as setReBroadcastShow,
|
||||
reset as resetReBroadcast,
|
||||
} from "State/ReBroadcast";
|
||||
import messages from "Element/messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { RootState } from "State/Store";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { ReBroadcaster } from "./ReBroadcaster";
|
||||
import { useState } from "react";
|
||||
|
||||
export interface NoteTranslation {
|
||||
text: string;
|
||||
@ -33,15 +27,12 @@ interface NosteContextMenuProps {
|
||||
}
|
||||
|
||||
export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useLogin();
|
||||
const { pinned, bookmarked, publicKey, preferences: prefs } = login;
|
||||
const { mute, block } = useModeration();
|
||||
const publisher = useEventPublisher();
|
||||
const showReBroadcastModal = useSelector((s: RootState) => s.reBroadcast.show);
|
||||
const reBroadcastNote = useSelector((s: RootState) => s.reBroadcast.note);
|
||||
const willRenderReBroadcast = showReBroadcastModal && reBroadcastNote && reBroadcastNote?.id === ev.id;
|
||||
const [showBroadcast, setShowBroadcast] = useState(false);
|
||||
const lang = window.navigator.language;
|
||||
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
||||
type: "language",
|
||||
@ -119,12 +110,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
}
|
||||
|
||||
const handleReBroadcastButtonClick = () => {
|
||||
if (reBroadcastNote?.id !== ev.id) {
|
||||
dispatch(resetReBroadcast());
|
||||
}
|
||||
|
||||
dispatch(setReBroadcastNote(ev));
|
||||
dispatch(setReBroadcastShow(!showReBroadcastModal));
|
||||
setShowBroadcast(true);
|
||||
};
|
||||
|
||||
function menuItems() {
|
||||
@ -214,7 +200,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||
menuClassName="ctx-menu">
|
||||
{menuItems()}
|
||||
</Menu>
|
||||
{willRenderReBroadcast && <ReBroadcaster />}
|
||||
{showBroadcast && <ReBroadcaster ev={ev} onClose={() => setShowBroadcast(false)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,31 +1,15 @@
|
||||
import "./NoteCreator.css";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
|
||||
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink, NostrEvent } from "@snort/system";
|
||||
|
||||
import Icon from "Icons/Icon";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { openFile } from "SnortUtils";
|
||||
import Textarea from "Element/Textarea";
|
||||
import Modal from "Element/Modal";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import useFileUpload from "Upload";
|
||||
import Note from "Element/Note";
|
||||
import {
|
||||
setShow,
|
||||
setNote,
|
||||
setError,
|
||||
setActive,
|
||||
setPreview,
|
||||
setShowAdvanced,
|
||||
setSelectedCustomRelays,
|
||||
setZapSplits,
|
||||
setSensitive,
|
||||
reset,
|
||||
setPollOptions,
|
||||
setOtherEvents,
|
||||
} from "State/NoteCreator";
|
||||
import type { RootState } from "State/Store";
|
||||
|
||||
import { ClipboardEventHandler } from "react";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
@ -34,37 +18,24 @@ import AsyncButton from "Element/AsyncButton";
|
||||
import { AsyncIcon } from "Element/AsyncIcon";
|
||||
import { fetchNip05Pubkey } from "@snort/shared";
|
||||
import { ZapTarget } from "Zapper";
|
||||
import { useNoteCreator } from "State/NoteCreator";
|
||||
|
||||
export function NoteCreator() {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
const {
|
||||
note,
|
||||
zapSplits,
|
||||
sensitive,
|
||||
pollOptions,
|
||||
replyTo,
|
||||
otherEvents,
|
||||
preview,
|
||||
active,
|
||||
show,
|
||||
showAdvanced,
|
||||
selectedCustomRelays,
|
||||
error,
|
||||
} = useSelector((s: RootState) => s.noteCreator);
|
||||
const dispatch = useDispatch();
|
||||
const login = useLogin();
|
||||
const note = useNoteCreator();
|
||||
const relays = login.relays;
|
||||
|
||||
async function buildNote() {
|
||||
try {
|
||||
dispatch(setError(""));
|
||||
note.update(v => v.error = "");
|
||||
if (note && publisher) {
|
||||
let extraTags: Array<Array<string>> | undefined;
|
||||
if (zapSplits) {
|
||||
if (note.zapSplits) {
|
||||
const parsedSplits = [] as Array<ZapTarget>;
|
||||
for (const s of zapSplits) {
|
||||
for (const s of note.zapSplits) {
|
||||
if (s.value.startsWith(NostrPrefix.PublicKey) || s.value.startsWith(NostrPrefix.Profile)) {
|
||||
const link = tryParseNostrLink(s.value);
|
||||
if (link) {
|
||||
@ -114,43 +85,53 @@ export function NoteCreator() {
|
||||
extraTags = parsedSplits.map(v => ["zap", v.value, "", String(v.weight)]);
|
||||
}
|
||||
|
||||
if (sensitive) {
|
||||
if (note.sensitive) {
|
||||
extraTags ??= [];
|
||||
extraTags.push(["content-warning", sensitive]);
|
||||
extraTags.push(["content-warning", note.sensitive]);
|
||||
}
|
||||
const kind = pollOptions ? EventKind.Polls : EventKind.TextNote;
|
||||
if (pollOptions) {
|
||||
const kind = note.pollOptions ? EventKind.Polls : EventKind.TextNote;
|
||||
if (note.pollOptions) {
|
||||
extraTags ??= [];
|
||||
extraTags.push(...pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
|
||||
extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
|
||||
}
|
||||
const hk = (eb: EventBuilder) => {
|
||||
extraTags?.forEach(t => eb.tag(t));
|
||||
eb.kind(kind);
|
||||
return eb;
|
||||
};
|
||||
const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk);
|
||||
const ev = note.replyTo ? await publisher.reply(note.replyTo, note.note, hk) : await publisher.note(note.note, hk);
|
||||
return ev;
|
||||
}
|
||||
} catch (e) {
|
||||
note.update(v => {
|
||||
if (e instanceof Error) {
|
||||
dispatch(setError(e.message));
|
||||
v.error = e.message;
|
||||
} else {
|
||||
dispatch(setError(e as string));
|
||||
v.error = e as string;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEventToRelays(ev: NostrEvent) {
|
||||
if (note.selectedCustomRelays) {
|
||||
await Promise.all(note.selectedCustomRelays.map(r => System.WriteOnceToRelay(r, ev)));
|
||||
} else {
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendNote() {
|
||||
const ev = await buildNote();
|
||||
if (ev) {
|
||||
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, ev));
|
||||
else System.BroadcastEvent(ev);
|
||||
dispatch(reset());
|
||||
for (const oe of otherEvents) {
|
||||
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, oe));
|
||||
else System.BroadcastEvent(oe);
|
||||
await sendEventToRelays(ev);
|
||||
for (const oe of note.otherEvents ?? []) {
|
||||
await sendEventToRelays(oe);
|
||||
}
|
||||
dispatch(reset());
|
||||
note.update(v => {
|
||||
v.reset();
|
||||
v.show = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,10 +141,14 @@ export function NoteCreator() {
|
||||
if (file) {
|
||||
uploadFile(file);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
dispatch(setError(error?.message));
|
||||
} catch (e) {
|
||||
note.update(v => {
|
||||
if (e instanceof Error) {
|
||||
v.error = e.message;
|
||||
} else {
|
||||
v.error = e as string;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,35 +156,39 @@ export function NoteCreator() {
|
||||
try {
|
||||
if (file) {
|
||||
const rx = await uploader.upload(file, file.name);
|
||||
note.update(v => {
|
||||
if (rx.header) {
|
||||
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`;
|
||||
dispatch(setNote(`${note ? `${note}\n` : ""}${link}`));
|
||||
dispatch(setOtherEvents([...otherEvents, rx.header]));
|
||||
v.note = `${v.note ? `${v.note}\n` : ""}${link}`;
|
||||
v.otherEvents = [...(v.otherEvents ?? []), rx.header];
|
||||
} else if (rx.url) {
|
||||
dispatch(setNote(`${note ? `${note}\n` : ""}${rx.url}`));
|
||||
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
|
||||
} else if (rx?.error) {
|
||||
dispatch(setError(rx.error));
|
||||
v.error = rx.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
dispatch(setError(error?.message));
|
||||
} catch (e) {
|
||||
note.update(v => {
|
||||
if (e instanceof Error) {
|
||||
v.error = e.message;
|
||||
} else {
|
||||
v.error = e as string;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
const { value } = ev.target;
|
||||
dispatch(setNote(value));
|
||||
if (value) {
|
||||
dispatch(setActive(true));
|
||||
} else {
|
||||
dispatch(setActive(false));
|
||||
}
|
||||
note.update(n => n.note = value);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
dispatch(reset());
|
||||
note.update(v => {
|
||||
v.show = false;
|
||||
v.reset();
|
||||
});
|
||||
}
|
||||
|
||||
async function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||
@ -208,21 +197,19 @@ export function NoteCreator() {
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
if (preview) {
|
||||
dispatch(setPreview(undefined));
|
||||
if (note.preview) {
|
||||
note.update(v => v.preview = undefined);
|
||||
} else if (publisher) {
|
||||
const tmpNote = await buildNote();
|
||||
if (tmpNote) {
|
||||
dispatch(setPreview(tmpNote));
|
||||
}
|
||||
note.update(v => v.preview = tmpNote);
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviewNote() {
|
||||
if (preview) {
|
||||
if (note.preview) {
|
||||
return (
|
||||
<Note
|
||||
data={preview as TaggedNostrEvent}
|
||||
data={note.preview as TaggedNostrEvent}
|
||||
related={[]}
|
||||
options={{
|
||||
showContextMenu: false,
|
||||
@ -236,13 +223,13 @@ export function NoteCreator() {
|
||||
}
|
||||
|
||||
function renderPollOptions() {
|
||||
if (pollOptions) {
|
||||
if (note.pollOptions) {
|
||||
return (
|
||||
<>
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Poll Options" />
|
||||
</h4>
|
||||
{pollOptions?.map((a, i) => (
|
||||
{note.pollOptions?.map((a, i) => (
|
||||
<div className="form-group w-max" key={`po-${i}`}>
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Option: {n}" values={{ n: i + 1 }} />
|
||||
@ -257,7 +244,7 @@ export function NoteCreator() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => dispatch(setPollOptions([...pollOptions, ""]))}>
|
||||
<button onClick={() => note.update(v => v.pollOptions = [...(note.pollOptions ?? []), ""])}>
|
||||
<Icon name="plus" size={14} />
|
||||
</button>
|
||||
</>
|
||||
@ -266,18 +253,18 @@ export function NoteCreator() {
|
||||
}
|
||||
|
||||
function changePollOption(i: number, v: string) {
|
||||
if (pollOptions) {
|
||||
const copy = [...pollOptions];
|
||||
if (note.pollOptions) {
|
||||
const copy = [...note.pollOptions];
|
||||
copy[i] = v;
|
||||
dispatch(setPollOptions(copy));
|
||||
note.update(v => v.pollOptions = copy);
|
||||
}
|
||||
}
|
||||
|
||||
function removePollOption(i: number) {
|
||||
if (pollOptions) {
|
||||
const copy = [...pollOptions];
|
||||
if (note.pollOptions) {
|
||||
const copy = [...note.pollOptions];
|
||||
copy.splice(i, 1);
|
||||
dispatch(setPollOptions(copy));
|
||||
note.update(v => v.pollOptions = copy);
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,19 +279,16 @@ export function NoteCreator() {
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setSelectedCustomRelays(
|
||||
checked={!note.selectedCustomRelays || note.selectedCustomRelays.includes(r)}
|
||||
onChange={e => {
|
||||
note.update(v => v.selectedCustomRelays = (
|
||||
// set false if all relays selected
|
||||
e.target.checked && selectedCustomRelays && selectedCustomRelays.length == a.length - 1
|
||||
? false
|
||||
e.target.checked && note.selectedCustomRelays && note.selectedCustomRelays.length == a.length - 1
|
||||
? undefined
|
||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||
a.filter(el =>
|
||||
el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el),
|
||||
),
|
||||
),
|
||||
)
|
||||
a.filter(el => el === r ? e.target.checked : !note.selectedCustomRelays || note.selectedCustomRelays.includes(el))
|
||||
));
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -345,13 +329,11 @@ export function NoteCreator() {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{show && (
|
||||
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
||||
{replyTo && (
|
||||
if (!note.show) return null;
|
||||
return (<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => v.show = false)}>
|
||||
{note.replyTo && (
|
||||
<Note
|
||||
data={replyTo}
|
||||
data={note.replyTo}
|
||||
related={[]}
|
||||
options={{
|
||||
showFooter: false,
|
||||
@ -362,15 +344,15 @@ export function NoteCreator() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{preview && getPreviewNote()}
|
||||
{!preview && (
|
||||
<div onPaste={handlePaste} className={`note-creator${pollOptions ? " poll" : ""}`}>
|
||||
{note.preview && getPreviewNote()}
|
||||
{!note.preview && (
|
||||
<div onPaste={handlePaste} className={`note-creator${note.pollOptions ? " poll" : ""}`}>
|
||||
<Textarea
|
||||
autoFocus
|
||||
className={`textarea ${active ? "textarea--focused" : ""}`}
|
||||
onChange={onChange}
|
||||
value={note}
|
||||
onFocus={() => dispatch(setActive(true))}
|
||||
className={`textarea ${note.active ? "textarea--focused" : ""}`}
|
||||
onChange={c => onChange(c)}
|
||||
value={note.note}
|
||||
onFocus={() => note.update(v => v.active = true)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" && e.metaKey) {
|
||||
sendNote().catch(console.warn);
|
||||
@ -382,14 +364,14 @@ export function NoteCreator() {
|
||||
)}
|
||||
<div className="flex f-space">
|
||||
<div className="flex g8">
|
||||
<ProfileImage pubkey={login.publicKey ?? ""} className="note-creator-icon" link="" showUsername={false} />
|
||||
{pollOptions === undefined && !replyTo && (
|
||||
<ProfileImage pubkey={login.publicKey ?? ""} className="note-creator-icon" link="" showUsername={false} showFollowingMark={false} />
|
||||
{note.pollOptions === undefined && !note.replyTo && (
|
||||
<div className="note-creator-icon">
|
||||
<Icon name="pie-chart" onClick={() => dispatch(setPollOptions(["A", "B"]))} size={24} />
|
||||
<Icon name="pie-chart" onClick={() => note.update(v => v.pollOptions = ["A", "B"])} size={24} />
|
||||
</div>
|
||||
)}
|
||||
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
|
||||
<button className="secondary" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
||||
<button className="secondary" onClick={() => note.update(v => v.advanced = !v.advanced)}>
|
||||
<FormattedMessage defaultMessage="Advanced" />
|
||||
</button>
|
||||
</div>
|
||||
@ -397,13 +379,13 @@ export function NoteCreator() {
|
||||
<button className="secondary" onClick={cancel}>
|
||||
<FormattedMessage defaultMessage="Cancel" />
|
||||
</button>
|
||||
<AsyncButton className="primary" onClick={onSubmit}>
|
||||
{replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
|
||||
<AsyncButton onClick={onSubmit}>
|
||||
{note.replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
{error && <span className="error">{error}</span>}
|
||||
{showAdvanced && (
|
||||
{note.error && <span className="error">{note.error}</span>}
|
||||
{note.advanced && (
|
||||
<>
|
||||
<button className="secondary" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
@ -423,7 +405,7 @@ export function NoteCreator() {
|
||||
</h4>
|
||||
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
||||
<div className="flex-column g8">
|
||||
{[...(zapSplits ?? [])].map((v, i, arr) => (
|
||||
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
|
||||
<div className="flex f-center g8">
|
||||
<div className="flex-column f-4 g4">
|
||||
<h4>
|
||||
@ -432,11 +414,7 @@ export function NoteCreator() {
|
||||
<input
|
||||
type="text"
|
||||
value={v.value}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setZapSplits(arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
|
||||
)
|
||||
}
|
||||
onChange={e => note.update(v => v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv)))}
|
||||
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
||||
/>
|
||||
</div>
|
||||
@ -448,29 +426,21 @@ export function NoteCreator() {
|
||||
type="number"
|
||||
min={0}
|
||||
value={v.weight}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setZapSplits(
|
||||
arr.map((vv, ii) => (ii === i ? { ...vv, weight: Number(e.target.value) } : vv)),
|
||||
),
|
||||
)
|
||||
}
|
||||
onChange={e => note.update(v => v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, weight: Number(e.target.value) } : vv)))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-column f-shrink g4">
|
||||
<div> </div>
|
||||
<Icon
|
||||
name="close"
|
||||
onClick={() => dispatch(setZapSplits((zapSplits ?? []).filter((_v, ii) => ii !== i)))}
|
||||
onClick={() => note.update(v => v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dispatch(setZapSplits([...(zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
||||
}>
|
||||
onClick={() => note.update(v => v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }])}>
|
||||
<FormattedMessage defaultMessage="Add" />
|
||||
</button>
|
||||
</div>
|
||||
@ -486,8 +456,8 @@ export function NoteCreator() {
|
||||
<input
|
||||
className="w-max"
|
||||
type="text"
|
||||
value={sensitive}
|
||||
onChange={e => dispatch(setSensitive(e.target.value))}
|
||||
value={note.sensitive}
|
||||
onChange={e => note.update(v => v.sensitive = e.target.value)}
|
||||
maxLength={50}
|
||||
minLength={1}
|
||||
placeholder={formatMessage({
|
||||
@ -501,7 +471,5 @@ export function NoteCreator() {
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,18 +1,15 @@
|
||||
import React, { HTMLProps, useContext, useEffect, useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useLongPress } from "use-long-press";
|
||||
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, NostrLink } from "@snort/system";
|
||||
import { SnortContext, useUserProfile } from "@snort/system-react";
|
||||
|
||||
import { formatShort } from "Number";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { delay, findTag, normalizeReaction } from "SnortUtils";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import SendSats from "Element/SendSats";
|
||||
import { ZapsSummary } from "Element/Zap";
|
||||
import { RootState } from "State/Store";
|
||||
import { setReplyTo, setShow, reset } from "State/NoteCreator";
|
||||
import { AsyncIcon } from "Element/AsyncIcon";
|
||||
|
||||
import { useWallet } from "Wallet";
|
||||
@ -22,6 +19,7 @@ import { ZapPoolController } from "ZapPoolController";
|
||||
import { System } from "index";
|
||||
import { Zapper, ZapTarget } from "Zapper";
|
||||
import { getDisplayName } from "./ProfileImage";
|
||||
import { useNoteCreator } from "State/NoteCreator";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -47,7 +45,6 @@ export interface NoteFooterProps {
|
||||
|
||||
export default function NoteFooter(props: NoteFooterProps) {
|
||||
const { ev, positive, reposts, zaps } = props;
|
||||
const dispatch = useDispatch();
|
||||
const system = useContext(SnortContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useLogin();
|
||||
@ -55,9 +52,8 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const author = useUserProfile(ev.pubkey);
|
||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||
const publisher = useEventPublisher();
|
||||
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
|
||||
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
||||
const willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
|
||||
const note = useNoteCreator();
|
||||
const willRenderNoteCreator = note.show && note.replyTo?.id === ev.id;
|
||||
const [tip, setTip] = useState(false);
|
||||
const [zapping, setZapping] = useState(false);
|
||||
const walletState = useWallet();
|
||||
@ -238,7 +234,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
function replyIcon() {
|
||||
return (
|
||||
<AsyncFooterIcon
|
||||
className={showNoteCreatorModal ? "reacted" : ""}
|
||||
className={note.show ? "reacted" : ""}
|
||||
iconName="reply"
|
||||
title={formatMessage({ defaultMessage: "Reply" })}
|
||||
value={0}
|
||||
@ -248,12 +244,13 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
const handleReplyButtonClick = () => {
|
||||
if (replyTo?.id !== ev.id) {
|
||||
dispatch(reset());
|
||||
note.update(v => {
|
||||
if (v.replyTo?.id !== ev.id) {
|
||||
v.reset();
|
||||
}
|
||||
|
||||
dispatch(setReplyTo(ev));
|
||||
dispatch(setShow(!showNoteCreatorModal));
|
||||
v.show = true;
|
||||
v.replyTo = ev;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
7
packages/app/src/Element/PinPrompt.css
Normal file
7
packages/app/src/Element/PinPrompt.css
Normal file
@ -0,0 +1,7 @@
|
||||
.pin-box {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 12px 16px;
|
||||
font-size: 80px;
|
||||
height: 1em;
|
||||
border-radius: 12px;
|
||||
}
|
121
packages/app/src/Element/PinPrompt.tsx
Normal file
121
packages/app/src/Element/PinPrompt.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import "./PinPrompt.css";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { LoginStore, createPublisher, sessionNeedsPin } from "Login";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventPublisher, InvalidPinError, PinEncrypted } from "@snort/system";
|
||||
import { DefaultPowWorker } from "index";
|
||||
import Modal from "./Modal";
|
||||
|
||||
const PinLen = 6;
|
||||
export function PinPrompt({ onResult, onCancel, subTitle }: { onResult: (v: string) => void, onCancel: () => void, subTitle?: ReactNode }) {
|
||||
const [pin, setPin] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
console.debug(e);
|
||||
if (!isNaN(Number(e.key)) && pin.length < PinLen) {
|
||||
setPin(s => s += e.key);
|
||||
} if (e.key === "Backspace") {
|
||||
setPin(s => s.slice(0, -1));
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
const handler = (e: Event) => handleKey(e as KeyboardEvent);
|
||||
document.addEventListener("keydown", handler);
|
||||
return () => document.removeEventListener("keydown", handler);
|
||||
}, [pin]);
|
||||
|
||||
useEffect(() => {
|
||||
setError("");
|
||||
if (pin.length === PinLen) {
|
||||
try {
|
||||
onResult(pin);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof InvalidPinError) {
|
||||
setError(formatMessage({
|
||||
defaultMessage: "Incorrect pin"
|
||||
}));
|
||||
} else if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [pin]);
|
||||
|
||||
const boxes = [];
|
||||
for (let x = 0; x < PinLen; x++) {
|
||||
boxes.push(<div className="pin-box flex f-center f-1">
|
||||
{pin[x]}
|
||||
</div>)
|
||||
}
|
||||
return <Modal id="pin" onClose={() => onCancel()}>
|
||||
<div className="flex-column g12">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Enter Pin" />
|
||||
</h2>
|
||||
{subTitle}
|
||||
<div className="flex g4">
|
||||
{boxes}
|
||||
</div>
|
||||
{error && <b className="error">{error}</b>}
|
||||
<div>
|
||||
<button type="button" onClick={() => onCancel()}>
|
||||
<FormattedMessage defaultMessage="Cancel" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export function LoginUnlock() {
|
||||
const login = useLogin();
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
function encryptMigration(pin: string) {
|
||||
const k = unwrap(login.privateKey);
|
||||
const newPin = PinEncrypted.create(k, pin);
|
||||
|
||||
const pub = EventPublisher.privateKey(k);
|
||||
if (login.preferences.pow) {
|
||||
pub.pow(login.preferences.pow, DefaultPowWorker);
|
||||
}
|
||||
LoginStore.setPublisher(login.id, pub);
|
||||
LoginStore.updateSession({
|
||||
...login,
|
||||
privateKeyData: newPin.toPayload(),
|
||||
privateKey: undefined
|
||||
});
|
||||
}
|
||||
|
||||
function unlockSession(pin: string) {
|
||||
const pub = createPublisher(login, pin);
|
||||
if (pub) {
|
||||
if (login.preferences.pow) {
|
||||
pub.pow(login.preferences.pow, DefaultPowWorker);
|
||||
}
|
||||
LoginStore.setPublisher(login.id, pub);
|
||||
}
|
||||
}
|
||||
|
||||
if (login.publicKey && !publisher && sessionNeedsPin(login)) {
|
||||
if (login.privateKey !== undefined) {
|
||||
return <PinPrompt subTitle={<p>
|
||||
<FormattedMessage defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open Snort." />
|
||||
</p>} onResult={encryptMigration} onCancel={() => {
|
||||
// nothing
|
||||
}} />
|
||||
}
|
||||
return <PinPrompt subTitle={<p>
|
||||
<FormattedMessage defaultMessage="Enter pin to unlock private key" />
|
||||
</p>} onResult={unlockSession} onCancel={() => {
|
||||
//nothing
|
||||
}} />
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { useWallet } from "Wallet";
|
||||
import { unwrap } from "SnortUtils";
|
||||
import { formatShort } from "Number";
|
||||
|
@ -10,7 +10,7 @@ import { Toastore } from "Toaster";
|
||||
import { getDisplayName } from "Element/ProfileImage";
|
||||
import { UserCache } from "Cache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { WalletInvoiceState } from "Wallet";
|
||||
|
||||
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
|
||||
|
@ -1,28 +1,25 @@
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import Modal from "Element/Modal";
|
||||
import type { RootState } from "State/Store";
|
||||
import { setShow, reset, setSelectedCustomRelays } from "State/ReBroadcast";
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { System } from "index";
|
||||
|
||||
export function ReBroadcaster() {
|
||||
export function ReBroadcaster({ onClose, ev }: { onClose: () => void, ev: TaggedNostrEvent }) {
|
||||
const [selected, setSelected] = useState<Array<string>>();
|
||||
const publisher = useEventPublisher();
|
||||
const { note, show, selectedCustomRelays } = useSelector((s: RootState) => s.reBroadcast);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function sendReBroadcast() {
|
||||
if (note && publisher) {
|
||||
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, note));
|
||||
else System.BroadcastEvent(note);
|
||||
dispatch(reset());
|
||||
if (publisher) {
|
||||
if (selected) {
|
||||
await Promise.all(selected.map(r => System.WriteOnceToRelay(r, ev)));
|
||||
} else {
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
dispatch(reset());
|
||||
}
|
||||
|
||||
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||
@ -46,19 +43,11 @@ export function ReBroadcaster() {
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setSelectedCustomRelays(
|
||||
// set false if all relays selected
|
||||
e.target.checked && selectedCustomRelays && selectedCustomRelays.length == a.length - 1
|
||||
? false
|
||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||
a.filter(el =>
|
||||
el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el),
|
||||
),
|
||||
),
|
||||
)
|
||||
checked={!selected || selected.includes(r)}
|
||||
onChange={e => setSelected(
|
||||
e.target.checked && selected && selected.length == a.length - 1
|
||||
? undefined
|
||||
: a.filter(el => el === r ? e.target.checked : !selected || selected.includes(el)))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -70,11 +59,10 @@ export function ReBroadcaster() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{show && (
|
||||
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
||||
<Modal id="broadcaster" className="note-creator-modal" onClose={onClose}>
|
||||
{renderRelayCustomisation()}
|
||||
<div className="note-creator-actions">
|
||||
<button className="secondary" onClick={cancel}>
|
||||
<button className="secondary" onClick={onClose}>
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
</button>
|
||||
<button onClick={onSubmit}>
|
||||
@ -82,7 +70,6 @@ export function ReBroadcaster() {
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
|
||||
}, [show]);
|
||||
|
||||
return show ? (
|
||||
<Modal className="reactions-modal" onClose={onClose}>
|
||||
<Modal id="reactions" className="reactions-modal" onClose={onClose}>
|
||||
<div className="close" onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@ import { LNURLSuccessAction } from "@snort/shared";
|
||||
|
||||
import { formatShort } from "Number";
|
||||
import Icon from "Icons/Icon";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import Modal from "Element/Modal";
|
||||
import QrCode from "Element/QrCode";
|
||||
@ -180,7 +180,7 @@ export default function SendSats(props: SendSatsProps) {
|
||||
|
||||
if (!(props.show ?? false)) return null;
|
||||
return (
|
||||
<Modal className="lnurl-modal" onClose={onClose}>
|
||||
<Modal id="send-sats" className="lnurl-modal" onClose={onClose}>
|
||||
<div className="p flex-column g12">
|
||||
<div className="flex g12">
|
||||
<div className="flex f-grow">{props.title || title()}</div>
|
||||
|
@ -55,7 +55,7 @@ export function SpotlightMedia(props: SpotlightMediaProps) {
|
||||
|
||||
export function SpotlightMediaModal(props: SpotlightMediaProps) {
|
||||
return (
|
||||
<Modal onClose={props.onClose} className="spotlight">
|
||||
<Modal id="spotlight" onClose={props.onClose} className="spotlight">
|
||||
<SpotlightMedia {...props} />
|
||||
</Modal>
|
||||
);
|
||||
|
@ -11,7 +11,7 @@ import Note from "Element/Note";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { FollowsFeed } from "Cache";
|
||||
import { LiveStreams } from "Element/LiveStreams";
|
||||
import { useReactions } from "Feed/FeedReactions";
|
||||
import { useReactions } from "Feed/Reactions";
|
||||
import AsyncButton from "./AsyncButton";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
|
@ -4,7 +4,7 @@ import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
import Note from "Element/Note";
|
||||
import NostrBandApi from "External/NostrBand";
|
||||
import { useReactions } from "Feed/FeedReactions";
|
||||
import { useReactions } from "Feed/Reactions";
|
||||
|
||||
export default function TrendingNotes() {
|
||||
const [posts, setPosts] = useState<Array<NostrEvent>>();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NostrPrefix, NostrEvent, NostrLink } from "@snort/system";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import Icon from "Icons/Icon";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { useState } from "react";
|
||||
|
@ -1,10 +0,0 @@
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { DefaultPowWorker } from "index";
|
||||
|
||||
export default function useEventPublisher() {
|
||||
const { publisher, preferences } = useLogin();
|
||||
if (preferences.pow) {
|
||||
publisher?.pow(preferences.pow, DefaultPowWorker);
|
||||
}
|
||||
return publisher;
|
||||
}
|
@ -4,7 +4,7 @@ import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils";
|
||||
import { makeNotification, sendNotification } from "Notifications";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import { getMutedKeys } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
@ -28,9 +28,7 @@ export default function useLoginFeed() {
|
||||
|
||||
useRefreshFeedCache(Notifications, true);
|
||||
useRefreshFeedCache(FollowsFeed, true);
|
||||
if (publisher?.supports("nip44")) {
|
||||
useRefreshFeedCache(GiftsCache, true);
|
||||
}
|
||||
|
||||
const subLogin = useMemo(() => {
|
||||
if (!pubKey) return null;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RequestBuilder, EventKind, NoteCollection, NostrLink, NostrPrefix } from "@snort/system";
|
||||
import { RequestBuilder, EventKind, NoteCollection, NostrLink } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { useMemo } from "react";
|
||||
@ -8,10 +8,8 @@ export function useReactions(subId: string, ids: Array<NostrLink>, others?: (rb:
|
||||
|
||||
const sub = useMemo(() => {
|
||||
const rb = new RequestBuilder(subId);
|
||||
const eTags = ids.filter(a => a.type === NostrPrefix.Note || a.type === NostrPrefix.Event);
|
||||
const aTags = ids.filter(a => a.type === NostrPrefix.Address);
|
||||
|
||||
if (aTags.length > 0 || eTags.length > 0) {
|
||||
if (ids.length > 0) {
|
||||
const f = rb
|
||||
.withFilter()
|
||||
.kinds(
|
||||
@ -20,8 +18,7 @@ export function useReactions(subId: string, ids: Array<NostrLink>, others?: (rb:
|
||||
: [EventKind.ZapReceipt, EventKind.Repost],
|
||||
);
|
||||
|
||||
aTags.forEach(v => f.replyToLink(v));
|
||||
eTags.forEach(v => f.replyToLink(v));
|
||||
ids.forEach(v => f.replyToLink(v));
|
||||
}
|
||||
others?.(rb);
|
||||
return rb.numFilters > 0 ? rb : null;
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { EventKind, NostrLink, RequestBuilder, NoteCollection } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
import { useReactions } from "./FeedReactions";
|
||||
import { useReactions } from "./Reactions";
|
||||
|
||||
export default function useThreadFeed(link: NostrLink) {
|
||||
const [allEvents, setAllEvents] = useState<Array<NostrLink>>([]);
|
||||
@ -12,9 +12,10 @@ export default function useThreadFeed(link: NostrLink) {
|
||||
sub.withOptions({
|
||||
leaveOpen: true,
|
||||
});
|
||||
sub.withFilter().kinds([EventKind.TextNote]).link(link).replyToLink(link);
|
||||
sub.withFilter().link(link);
|
||||
sub.withFilter().kinds([EventKind.TextNote]).replyToLink(link);
|
||||
allEvents.forEach(x => {
|
||||
sub.withFilter().kinds([EventKind.TextNote]).link(x).replyToLink(x);
|
||||
sub.withFilter().kinds([EventKind.TextNote]).replyToLink(x);
|
||||
});
|
||||
return sub;
|
||||
}, [allEvents.length]);
|
||||
@ -23,8 +24,7 @@ export default function useThreadFeed(link: NostrLink) {
|
||||
|
||||
useEffect(() => {
|
||||
if (store.data) {
|
||||
const mainNotes = store.data?.filter(a => a.kind === EventKind.TextNote || a.kind === EventKind.Polls) ?? [];
|
||||
const links = mainNotes
|
||||
const links = store.data
|
||||
.map(a => [
|
||||
NostrLink.fromEvent(a),
|
||||
...a.tags.filter(a => a[0] === "e" || a[0] === "a").map(v => NostrLink.fromTag(v)),
|
||||
|
21
packages/app/src/Hooks/useEventPublisher.tsx
Normal file
21
packages/app/src/Hooks/useEventPublisher.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { LoginStore, createPublisher, sessionNeedsPin } from "Login";
|
||||
import { DefaultPowWorker } from "index";
|
||||
|
||||
export default function useEventPublisher() {
|
||||
const login = useLogin();
|
||||
|
||||
let existing = LoginStore.getPublisher(login.id);
|
||||
|
||||
if (login.publicKey && !existing && !sessionNeedsPin(login)) {
|
||||
existing = createPublisher(login);
|
||||
if (existing) {
|
||||
if (login.preferences.pow) {
|
||||
existing.pow(login.preferences.pow, DefaultPowWorker);
|
||||
}
|
||||
LoginStore.setPublisher(login.id, existing);
|
||||
}
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
@ -1,17 +1,19 @@
|
||||
import { useIntl } from "react-intl";
|
||||
import { Nip46Signer, PinEncrypted } from "@snort/system";
|
||||
|
||||
import { EmailRegex, MnemonicRegex } from "Const";
|
||||
import { LoginSessionType, LoginStore } from "Login";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { getNip05PubKey } from "Pages/LoginPage";
|
||||
import { bech32ToHex } from "SnortUtils";
|
||||
import { Nip46Signer } from "@snort/system";
|
||||
|
||||
export class PinRequiredError extends Error { }
|
||||
|
||||
export default function useLoginHandler() {
|
||||
const { formatMessage } = useIntl();
|
||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||
|
||||
async function doLogin(key: string) {
|
||||
async function doLogin(key: string, pin?: string) {
|
||||
const insecureMsg = formatMessage({
|
||||
defaultMessage:
|
||||
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
|
||||
@ -23,7 +25,8 @@ export default function useLoginHandler() {
|
||||
}
|
||||
const hexKey = bech32ToHex(key);
|
||||
if (hexKey.length === 64) {
|
||||
LoginStore.loginWithPrivateKey(hexKey);
|
||||
if (!pin) throw new PinRequiredError();
|
||||
LoginStore.loginWithPrivateKey(PinEncrypted.create(hexKey, pin));
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
@ -31,14 +34,16 @@ export default function useLoginHandler() {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
if (!pin) throw new PinRequiredError();
|
||||
const ent = generateBip39Entropy(key);
|
||||
const keyHex = entropyToPrivateKey(ent);
|
||||
LoginStore.loginWithPrivateKey(keyHex);
|
||||
LoginStore.loginWithPrivateKey(PinEncrypted.create(keyHex, pin));
|
||||
} else if (key.length === 64) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
LoginStore.loginWithPrivateKey(key);
|
||||
if (!pin) throw new PinRequiredError();
|
||||
LoginStore.loginWithPrivateKey(PinEncrypted.create(key, pin));
|
||||
}
|
||||
|
||||
// public key logins
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { HexKey } from "@snort/system";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setBlocked, setMuted } from "Login";
|
||||
import { appendDedupe } from "SnortUtils";
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
import { NoopStore, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
import { RefreshFeedCache } from "Cache/RefreshFeedCache";
|
||||
import useLogin from "./useLogin";
|
||||
import useEventPublisher from "./useEventPublisher";
|
||||
|
||||
export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
const sub = useMemo(() => {
|
||||
if (login) {
|
||||
if (login.publicKey) {
|
||||
const rb = new RequestBuilder(`using-${c.name}`);
|
||||
rb.withOptions({
|
||||
leaveOpen,
|
||||
@ -28,11 +29,11 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
|
||||
let t: ReturnType<typeof setTimeout> | undefined;
|
||||
let tBuf: Array<TaggedNostrEvent> = [];
|
||||
const releaseOnEvent = q.feed.onEvent(evs => {
|
||||
if (!t) {
|
||||
if (!t && publisher) {
|
||||
tBuf = [...evs];
|
||||
t = setTimeout(() => {
|
||||
t = undefined;
|
||||
c.onEvent(tBuf, unwrap(login.publisher));
|
||||
c.onEvent(tBuf, publisher);
|
||||
}, 100);
|
||||
} else {
|
||||
tBuf.push(...evs);
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { HexKey, RelaySettings, EventPublisher } from "@snort/system";
|
||||
import { RelaySettings, EventPublisher, PinEncrypted, Nip46Signer, Nip7Signer, PrivateKeySigner } from "@snort/system";
|
||||
import { unixNowMs } from "@snort/shared";
|
||||
import * as secp from "@noble/curves/secp256k1";
|
||||
import * as utils from "@noble/curves/abstract/utils";
|
||||
|
||||
import { DefaultRelays, SnortPubKey } from "Const";
|
||||
import { LoginStore, UserPreferences, LoginSession } from "Login";
|
||||
import { LoginStore, UserPreferences, LoginSession, LoginSessionType } from "Login";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unwrap } from "SnortUtils";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
import { System } from "index";
|
||||
import { Chats, FollowsFeed, GiftsCache, Notifications } from "Cache";
|
||||
import { PinRequiredError } from "Hooks/useLoginHandler";
|
||||
import { Nip7OsSigner } from "./Nip7OsSigner";
|
||||
|
||||
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||
if (state.relays.timestamp >= createdAt) {
|
||||
@ -41,8 +43,8 @@ export function updatePreferences(state: LoginSession, p: UserPreferences) {
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function logout(k: HexKey) {
|
||||
LoginStore.removeSession(k);
|
||||
export function logout(id: string) {
|
||||
LoginStore.removeSession(id);
|
||||
GiftsCache.clear();
|
||||
Notifications.clear();
|
||||
FollowsFeed.clear();
|
||||
@ -62,7 +64,7 @@ export function clearEntropy(state: LoginSession) {
|
||||
/**
|
||||
* Generate a new key and login with this generated key
|
||||
*/
|
||||
export async function generateNewLogin() {
|
||||
export async function generateNewLogin(pin: string) {
|
||||
const ent = generateBip39Entropy();
|
||||
const entropy = utils.bytesToHex(ent);
|
||||
const privateKey = entropyToPrivateKey(ent);
|
||||
@ -88,7 +90,9 @@ export async function generateNewLogin() {
|
||||
const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays);
|
||||
System.BroadcastEvent(ev);
|
||||
|
||||
LoginStore.loginWithPrivateKey(privateKey, entropy, newRelays);
|
||||
const key = PinEncrypted.create(privateKey, pin);
|
||||
key.decrypt(pin);
|
||||
LoginStore.loginWithPrivateKey(key, entropy, newRelays);
|
||||
}
|
||||
|
||||
export function generateRandomKey() {
|
||||
@ -161,3 +165,38 @@ export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionNeedsPin(l: LoginSession) {
|
||||
return l.type === LoginSessionType.PrivateKey || l.type === LoginSessionType.Nip46;
|
||||
}
|
||||
|
||||
export function createPublisher(l: LoginSession, pin?: string) {
|
||||
switch (l.type) {
|
||||
case LoginSessionType.PrivateKey: {
|
||||
if(!pin) throw new PinRequiredError();
|
||||
const v = l.privateKeyData instanceof PinEncrypted ? l.privateKeyData : new PinEncrypted(unwrap(l.privateKeyData));
|
||||
v.decrypt(pin);
|
||||
l.privateKeyData = v;
|
||||
return EventPublisher.privateKey(v.value);
|
||||
}
|
||||
case LoginSessionType.Nip46: {
|
||||
if(!pin) throw new PinRequiredError();
|
||||
const v = l.privateKeyData instanceof PinEncrypted ? l.privateKeyData : new PinEncrypted(unwrap(l.privateKeyData));
|
||||
v.decrypt(pin);
|
||||
l.privateKeyData = v;
|
||||
|
||||
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
|
||||
const inner = new PrivateKeySigner(v.value);
|
||||
const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner);
|
||||
return new EventPublisher(nip46, unwrap(l.publicKey));
|
||||
}
|
||||
case LoginSessionType.Nip7os: {
|
||||
return new EventPublisher(new Nip7OsSigner(), unwrap(l.publicKey));
|
||||
}
|
||||
default: {
|
||||
if (l.publicKey) {
|
||||
return new EventPublisher(new Nip7Signer(), l.publicKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { HexKey, RelaySettings, u256, EventPublisher } from "@snort/system";
|
||||
import { HexKey, RelaySettings, u256, PinEncrypted, PinEncryptedPayload } from "@snort/system";
|
||||
import { UserPreferences } from "Login";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
|
||||
@ -19,6 +19,11 @@ export enum LoginSessionType {
|
||||
}
|
||||
|
||||
export interface LoginSession {
|
||||
/**
|
||||
* Unique ID to identify this session
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Type of login session
|
||||
*/
|
||||
@ -26,9 +31,15 @@ export interface LoginSession {
|
||||
|
||||
/**
|
||||
* Current user private key
|
||||
* @deprecated Moving to pin encrypted storage
|
||||
*/
|
||||
privateKey?: HexKey;
|
||||
|
||||
/**
|
||||
* Encrypted private key
|
||||
*/
|
||||
privateKeyData?: PinEncrypted | PinEncryptedPayload;
|
||||
|
||||
/**
|
||||
* BIP39-generated, hex-encoded entropy
|
||||
*/
|
||||
@ -98,9 +109,4 @@ export interface LoginSession {
|
||||
* Remote signer relays (NIP-46)
|
||||
*/
|
||||
remoteSignerRelays?: Array<string>;
|
||||
|
||||
/**
|
||||
* Instance event publisher
|
||||
*/
|
||||
publisher?: EventPublisher;
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
import * as secp from "@noble/curves/secp256k1";
|
||||
import * as utils from "@noble/curves/abstract/utils";
|
||||
import {v4 as uuid} from "uuid";
|
||||
|
||||
import { HexKey, RelaySettings, EventPublisher, Nip46Signer, Nip7Signer, PrivateKeySigner } from "@snort/system";
|
||||
import { HexKey, RelaySettings, PinEncrypted, EventPublisher } from "@snort/system";
|
||||
import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
|
||||
|
||||
import { DefaultRelays } from "Const";
|
||||
import { LoginSession, LoginSessionType } from "Login";
|
||||
import { DefaultPreferences, UserPreferences } from "./Preferences";
|
||||
import { Nip7OsSigner } from "./Nip7OsSigner";
|
||||
import { LoginSession, LoginSessionType, createPublisher } from "Login";
|
||||
import { DefaultPreferences } from "./Preferences";
|
||||
|
||||
const AccountStoreKey = "sessions";
|
||||
const LoggedOut = {
|
||||
id: "default",
|
||||
type: "public_key",
|
||||
preferences: DefaultPreferences,
|
||||
tags: {
|
||||
@ -45,25 +46,18 @@ const LoggedOut = {
|
||||
readNotifications: 0,
|
||||
subscriptions: [],
|
||||
} as LoginSession;
|
||||
const LegacyKeys = {
|
||||
PrivateKeyItem: "secret",
|
||||
PublicKeyItem: "pubkey",
|
||||
NotificationsReadItem: "notifications-read",
|
||||
UserPreferencesKey: "preferences",
|
||||
RelayListKey: "last-relays",
|
||||
FollowList: "last-follows",
|
||||
};
|
||||
|
||||
export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
#activeAccount?: HexKey;
|
||||
#accounts: Map<string, LoginSession>;
|
||||
#publishers = new Map<string, EventPublisher>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const existing = window.localStorage.getItem(AccountStoreKey);
|
||||
if (existing) {
|
||||
const logins = JSON.parse(existing);
|
||||
this.#accounts = new Map((logins as Array<LoginSession>).map(a => [unwrap(a.publicKey), a]));
|
||||
this.#accounts = new Map((logins as Array<LoginSession>).map(a => [a.id, a]));
|
||||
} else {
|
||||
this.#accounts = new Map();
|
||||
}
|
||||
@ -71,26 +65,32 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
if (!this.#activeAccount) {
|
||||
this.#activeAccount = this.#accounts.keys().next().value;
|
||||
}
|
||||
for (const [, v] of this.#accounts) {
|
||||
v.publisher = this.#createPublisher(v);
|
||||
}
|
||||
}
|
||||
|
||||
getSessions() {
|
||||
return [...this.#accounts.keys()];
|
||||
return [...this.#accounts.values()].map(v => unwrap(v.publicKey));
|
||||
}
|
||||
|
||||
allSubscriptions() {
|
||||
return [...this.#accounts.values()].map(a => a.subscriptions).flat();
|
||||
}
|
||||
|
||||
switchAccount(pk: string) {
|
||||
if (this.#accounts.has(pk)) {
|
||||
this.#activeAccount = pk;
|
||||
switchAccount(id: string) {
|
||||
if (this.#accounts.has(id)) {
|
||||
this.#activeAccount = id;
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
getPublisher(id: string) {
|
||||
return this.#publishers.get(id);
|
||||
}
|
||||
|
||||
setPublisher(id: string, pub: EventPublisher) {
|
||||
this.#publishers.set(id, pub);
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
loginWithPubkey(
|
||||
key: HexKey,
|
||||
type: LoginSessionType,
|
||||
@ -104,6 +104,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
const initRelays = this.decideInitRelays(relays);
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
id: uuid(),
|
||||
type,
|
||||
publicKey: key,
|
||||
relays: {
|
||||
@ -114,10 +115,13 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
remoteSignerRelays,
|
||||
privateKey,
|
||||
} as LoginSession;
|
||||
newSession.publisher = this.#createPublisher(newSession);
|
||||
|
||||
this.#accounts.set(key, newSession);
|
||||
this.#activeAccount = key;
|
||||
const pub = createPublisher(newSession);
|
||||
if(pub) {
|
||||
this.setPublisher(newSession.id, pub);
|
||||
}
|
||||
this.#accounts.set(newSession.id, newSession);
|
||||
this.#activeAccount = newSession.id;
|
||||
this.#save();
|
||||
return newSession;
|
||||
}
|
||||
@ -129,16 +133,17 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
return Object.fromEntries(DefaultRelays.entries());
|
||||
}
|
||||
|
||||
loginWithPrivateKey(key: HexKey, entropy?: string, relays?: Record<string, RelaySettings>) {
|
||||
const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(key));
|
||||
loginWithPrivateKey(key: PinEncrypted, entropy?: string, relays?: Record<string, RelaySettings>) {
|
||||
const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(key.value));
|
||||
if (this.#accounts.has(pubKey)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
}
|
||||
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
id: uuid(),
|
||||
type: LoginSessionType.PrivateKey,
|
||||
privateKey: key,
|
||||
privateKeyData: key,
|
||||
publicKey: pubKey,
|
||||
generatedEntropy: entropy,
|
||||
relays: {
|
||||
@ -149,30 +154,30 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
} as LoginSession;
|
||||
|
||||
if ("nostr_os" in window && window.nostr_os) {
|
||||
window.nostr_os.saveKey(key);
|
||||
window.nostr_os.saveKey(key.value);
|
||||
newSession.type = LoginSessionType.Nip7os;
|
||||
newSession.privateKey = undefined;
|
||||
newSession.privateKeyData = undefined;
|
||||
}
|
||||
newSession.publisher = this.#createPublisher(newSession);
|
||||
const pub = EventPublisher.privateKey(key.value);
|
||||
this.setPublisher(newSession.id, pub);
|
||||
|
||||
this.#accounts.set(pubKey, newSession);
|
||||
this.#activeAccount = pubKey;
|
||||
this.#accounts.set(newSession.id, newSession);
|
||||
this.#activeAccount = newSession.id;
|
||||
this.#save();
|
||||
return newSession;
|
||||
}
|
||||
|
||||
updateSession(s: LoginSession) {
|
||||
const pk = unwrap(s.publicKey);
|
||||
if (this.#accounts.has(pk)) {
|
||||
this.#accounts.set(pk, s);
|
||||
if (this.#accounts.has(s.id)) {
|
||||
this.#accounts.set(s.id, s);
|
||||
console.debug("SET SESSION", s);
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
removeSession(k: string) {
|
||||
if (this.#accounts.delete(k)) {
|
||||
if (this.#activeAccount === k) {
|
||||
removeSession(id: string) {
|
||||
if (this.#accounts.delete(id)) {
|
||||
if (this.#activeAccount === id) {
|
||||
this.#activeAccount = undefined;
|
||||
}
|
||||
this.#save();
|
||||
@ -186,62 +191,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
return {...s};
|
||||
}
|
||||
|
||||
#createPublisher(l: LoginSession) {
|
||||
switch (l.type) {
|
||||
case LoginSessionType.PrivateKey: {
|
||||
return EventPublisher.privateKey(unwrap(l.privateKey));
|
||||
}
|
||||
case LoginSessionType.Nip46: {
|
||||
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
|
||||
const inner = new PrivateKeySigner(unwrap(l.privateKey));
|
||||
const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner);
|
||||
return new EventPublisher(nip46, unwrap(l.publicKey));
|
||||
}
|
||||
case LoginSessionType.Nip7os: {
|
||||
return new EventPublisher(new Nip7OsSigner(), unwrap(l.publicKey));
|
||||
}
|
||||
default: {
|
||||
if (l.publicKey) {
|
||||
return new EventPublisher(new Nip7Signer(), l.publicKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#migrate() {
|
||||
let didMigrate = false;
|
||||
const oldPreferences = window.localStorage.getItem(LegacyKeys.UserPreferencesKey);
|
||||
const pref: UserPreferences = oldPreferences ? JSON.parse(oldPreferences) : deepClone(DefaultPreferences);
|
||||
window.localStorage.removeItem(LegacyKeys.UserPreferencesKey);
|
||||
|
||||
const privKey = window.localStorage.getItem(LegacyKeys.PrivateKeyItem);
|
||||
if (privKey) {
|
||||
const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
|
||||
this.#accounts.set(pubKey, {
|
||||
...LoggedOut,
|
||||
privateKey: privKey,
|
||||
publicKey: pubKey,
|
||||
preferences: pref,
|
||||
} as LoginSession);
|
||||
window.localStorage.removeItem(LegacyKeys.PrivateKeyItem);
|
||||
window.localStorage.removeItem(LegacyKeys.PublicKeyItem);
|
||||
didMigrate = true;
|
||||
}
|
||||
|
||||
const pubKey = window.localStorage.getItem(LegacyKeys.PublicKeyItem);
|
||||
if (pubKey) {
|
||||
this.#accounts.set(pubKey, {
|
||||
...LoggedOut,
|
||||
publicKey: pubKey,
|
||||
preferences: pref,
|
||||
} as LoginSession);
|
||||
window.localStorage.removeItem(LegacyKeys.PublicKeyItem);
|
||||
didMigrate = true;
|
||||
}
|
||||
|
||||
window.localStorage.removeItem(LegacyKeys.RelayListKey);
|
||||
window.localStorage.removeItem(LegacyKeys.FollowList);
|
||||
window.localStorage.removeItem(LegacyKeys.NotificationsReadItem);
|
||||
|
||||
// replace default tab with notes
|
||||
for (const [, v] of this.#accounts) {
|
||||
@ -259,6 +210,14 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
}
|
||||
}
|
||||
|
||||
// add ids
|
||||
for (const [, v] of this.#accounts) {
|
||||
if ((v.id?.length ?? 0) === 0) {
|
||||
v.id = uuid();
|
||||
didMigrate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (didMigrate) {
|
||||
console.debug("Finished migration to MultiAccountStore");
|
||||
this.#save();
|
||||
@ -267,9 +226,16 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
|
||||
#save() {
|
||||
if (!this.#activeAccount && this.#accounts.size > 0) {
|
||||
this.#activeAccount = [...this.#accounts.keys()][0];
|
||||
this.#activeAccount = this.#accounts.keys().next().value;
|
||||
}
|
||||
window.localStorage.setItem(AccountStoreKey, JSON.stringify([...this.#accounts.values()]));
|
||||
const toSave = [...this.#accounts.values()];
|
||||
for(const v of toSave) {
|
||||
if(v.privateKeyData instanceof PinEncrypted) {
|
||||
v.privateKeyData = v.privateKeyData.toPayload();
|
||||
}
|
||||
}
|
||||
|
||||
window.localStorage.setItem(AccountStoreKey, JSON.stringify(toSave));
|
||||
this.notifyChange();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { MultiAccountStore } from "./MultiAccountStore";
|
||||
|
||||
export const LoginStore = new MultiAccountStore();
|
||||
|
||||
export interface Nip7os {
|
||||
|
@ -70,7 +70,7 @@ export function SnortDeckLayout() {
|
||||
</div>
|
||||
{deckScope.thread && (
|
||||
<>
|
||||
<Modal onClose={() => deckScope.setThread(undefined)} className="thread-overlay">
|
||||
<Modal id="thread-overlay" onClose={() => deckScope.setThread(undefined)} className="thread-overlay">
|
||||
<ThreadContextWrapper link={deckScope.thread}>
|
||||
<SpotlightFromThread onClose={() => deckScope.setThread(undefined)} />
|
||||
<div>
|
||||
|
@ -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";
|
||||
|
@ -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,29 +20,19 @@ 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 note = useNoteCreator();
|
||||
const isReplyNoteCreatorShowing = note.replyTo && note.show;
|
||||
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));
|
||||
@ -66,34 +53,22 @@ export default function Layout() {
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
return (<>
|
||||
<div className={pageClass}>
|
||||
{!shouldHideHeader && (
|
||||
<header className="main-content">
|
||||
<Link to="/" className="logo">
|
||||
<h1>Snort</h1>
|
||||
{currentSubscription && (
|
||||
<small className="flex">
|
||||
<Icon name="diamond" size={10} className="mr5" />
|
||||
{mapPlanName(currentSubscription.type)}
|
||||
</small>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{publicKey ? (
|
||||
<LogoHeader />
|
||||
<AccountHeader />
|
||||
) : (
|
||||
<button type="button" onClick={() => navigate("/login")}>
|
||||
<FormattedMessage {...messages.Login} />
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
<Outlet />
|
||||
|
||||
{!shouldHideNoteCreator && (
|
||||
<>
|
||||
<button className="primary note-create-button" onClick={handleNoteCreatorButtonClick}>
|
||||
<button className="primary note-create-button" onClick={() => note.update(v => {
|
||||
v.replyTo = undefined;
|
||||
v.show = true
|
||||
})}>
|
||||
<Icon name="plus" size={16} />
|
||||
</button>
|
||||
<NoteCreator />
|
||||
@ -101,6 +76,8 @@ export default function Layout() {
|
||||
)}
|
||||
<Toaster />
|
||||
</div>
|
||||
<LoginUnlock />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -156,6 +133,13 @@ const AccountHeader = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!publicKey) {
|
||||
return (
|
||||
<button type="button" onClick={() => navigate("/login")}>
|
||||
<FormattedMessage {...messages.Login} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="header-actions">
|
||||
{!location.pathname.startsWith("/search") && (
|
||||
@ -199,3 +183,20 @@ const AccountHeader = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function LogoHeader() {
|
||||
const { subscriptions } = useLogin();
|
||||
const currentSubscription = getCurrentSubscription(subscriptions);
|
||||
|
||||
return (
|
||||
<Link to="/" className="logo">
|
||||
<h1>Snort</h1>
|
||||
{currentSubscription && (
|
||||
<small className="flex">
|
||||
<Icon name="diamond" size={10} className="mr5" />
|
||||
{mapPlanName(currentSubscription.type)}
|
||||
</small>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
@ -12,13 +12,14 @@ 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 {
|
||||
@ -78,6 +79,7 @@ export default function LoginPage() {
|
||||
const login = useLogin();
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [pin, setPin] = useState(false);
|
||||
const [art, setArt] = useState<ArtworkEntry>();
|
||||
const [isMasking, setMasking] = useState(true);
|
||||
const { formatMessage } = useIntl();
|
||||
@ -99,10 +101,13 @@ export default function LoginPage() {
|
||||
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 +121,16 @@ export default function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function makeRandomKey() {
|
||||
await generateNewLogin();
|
||||
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() {
|
||||
@ -157,7 +168,7 @@ export default function LoginPage() {
|
||||
<FormattedMessage defaultMessage="Nostr Connect (NIP-46)" description="Login button for NIP-46 signer app" />
|
||||
</AsyncButton>
|
||||
{nostrConnect && (
|
||||
<Modal onClose={() => setNostrConnect("")}>
|
||||
<Modal id="nostr-connect" onClose={() => setNostrConnect("")}>
|
||||
<div className="flex f-col">
|
||||
<QrCode data={nostrConnect} />
|
||||
<Copy text={nostrConnect} />
|
||||
@ -283,12 +294,19 @@ export default function LoginPage() {
|
||||
/>
|
||||
</p>
|
||||
<div dir="auto" className="login-actions">
|
||||
<AsyncButton type="button" onClick={doLogin}>
|
||||
<AsyncButton type="button" onClick={() => doLogin()}>
|
||||
<FormattedMessage defaultMessage="Login" description="Login button" />
|
||||
</AsyncButton>
|
||||
<AsyncButton onClick={() => makeRandomKey()}>
|
||||
<AsyncButton onClick={() => setPin(true)}>
|
||||
<FormattedMessage defaultMessage="Create Account" />
|
||||
</AsyncButton>
|
||||
{pin && <PinPrompt onResult={pin => {
|
||||
if (key) {
|
||||
doLogin(pin);
|
||||
} else {
|
||||
makeRandomKey(pin);
|
||||
}
|
||||
}} onCancel={() => setPin(false)} />}
|
||||
{altLogins()}
|
||||
</div>
|
||||
{installExtension()}
|
||||
|
@ -210,7 +210,7 @@ function NewChatWindow() {
|
||||
<Icon name="plus" size={16} />
|
||||
</button>
|
||||
{show && (
|
||||
<Modal onClose={() => setShow(false)} className="new-chat-modal">
|
||||
<Modal id="new-chat" onClose={() => setShow(false)} className="new-chat-modal">
|
||||
<div className="flex-column g16">
|
||||
<div className="flex f-space">
|
||||
<h2>
|
||||
|
@ -447,7 +447,7 @@ export default function ProfilePage() {
|
||||
<Icon name="qr" size={16} />
|
||||
</IconButton>
|
||||
{showProfileQr && (
|
||||
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||
<Modal id="profile-qr" className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||
<ProfileImage pubkey={id} />
|
||||
<QrCode data={link} className="m10 align-center" />
|
||||
<Copy text={link} className="align-center" />
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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("/");
|
||||
}
|
||||
|
||||
|
@ -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 }) {
|
||||
|
@ -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() {
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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<string>;
|
||||
zapSplits?: Array<ZapTarget>;
|
||||
sensitive: string;
|
||||
pollOptions?: Array<string>;
|
||||
otherEvents: Array<NostrEvent>;
|
||||
}
|
||||
|
||||
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<boolean>) => {
|
||||
state.show = action.payload;
|
||||
},
|
||||
setNote: (state, action: PayloadAction<string>) => {
|
||||
state.note = action.payload;
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
state.error = action.payload;
|
||||
},
|
||||
setActive: (state, action: PayloadAction<boolean>) => {
|
||||
state.active = action.payload;
|
||||
},
|
||||
setPreview: (state, action: PayloadAction<NostrEvent | undefined>) => {
|
||||
state.preview = action.payload;
|
||||
},
|
||||
setReplyTo: (state, action: PayloadAction<TaggedNostrEvent | undefined>) => {
|
||||
state.replyTo = action.payload;
|
||||
},
|
||||
setShowAdvanced: (state, action: PayloadAction<boolean>) => {
|
||||
state.showAdvanced = action.payload;
|
||||
},
|
||||
setSelectedCustomRelays: (state, action: PayloadAction<false | Array<string>>) => {
|
||||
state.selectedCustomRelays = action.payload;
|
||||
},
|
||||
setSensitive: (state, action: PayloadAction<string>) => {
|
||||
state.sensitive = action.payload;
|
||||
},
|
||||
setPollOptions: (state, action: PayloadAction<Array<string> | undefined>) => {
|
||||
state.pollOptions = action.payload;
|
||||
},
|
||||
setOtherEvents: (state, action: PayloadAction<Array<NostrEvent>>) => {
|
||||
state.otherEvents = action.payload;
|
||||
},
|
||||
setZapSplits: (state, action: PayloadAction<Array<ZapTarget>>) => {
|
||||
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;
|
80
packages/app/src/State/NoteCreator.tsx
Normal file
80
packages/app/src/State/NoteCreator.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||
import { ZapTarget } from "Zapper";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
interface NoteCreatorDataSnapshot {
|
||||
show: boolean;
|
||||
note: string;
|
||||
error: string;
|
||||
active: boolean;
|
||||
advanced: boolean;
|
||||
preview?: NostrEvent;
|
||||
replyTo?: TaggedNostrEvent;
|
||||
selectedCustomRelays?: Array<string>;
|
||||
zapSplits?: Array<ZapTarget>;
|
||||
sensitive?: string;
|
||||
pollOptions?: Array<string>;
|
||||
otherEvents?: Array<NostrEvent>;
|
||||
reset: () => void;
|
||||
update: (fn: (v: NoteCreatorDataSnapshot) => void) => void;
|
||||
}
|
||||
|
||||
class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
||||
#data: NoteCreatorDataSnapshot;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#data = {
|
||||
show: false,
|
||||
note: "",
|
||||
error: "",
|
||||
active: false,
|
||||
advanced: false,
|
||||
reset: () => {
|
||||
this.#reset(this.#data);
|
||||
this.notifyChange();
|
||||
},
|
||||
update: (fn: (v: NoteCreatorDataSnapshot) => void) => {
|
||||
fn(this.#data);
|
||||
this.notifyChange();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#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);
|
||||
console.debug(this.#data);
|
||||
this.notifyChange();
|
||||
}
|
||||
} as NoteCreatorDataSnapshot;
|
||||
return sn;
|
||||
}
|
||||
}
|
||||
|
||||
const NoteCreatorState = new NoteCreatorStore();
|
||||
|
||||
export function useNoteCreator() {
|
||||
return useSyncExternalStore(c => NoteCreatorState.hook(c), () => NoteCreatorState.snapshot());
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { NostrEvent } from "@snort/system";
|
||||
|
||||
interface ReBroadcastStore {
|
||||
show: boolean;
|
||||
selectedCustomRelays: false | Array<string>;
|
||||
note?: NostrEvent;
|
||||
}
|
||||
|
||||
const InitState: ReBroadcastStore = {
|
||||
show: false,
|
||||
selectedCustomRelays: false,
|
||||
};
|
||||
|
||||
const ReBroadcastSlice = createSlice({
|
||||
name: "ReBroadcast",
|
||||
initialState: InitState,
|
||||
reducers: {
|
||||
setShow: (state, action: PayloadAction<boolean>) => {
|
||||
state.show = action.payload;
|
||||
},
|
||||
setNote: (state, action: PayloadAction<NostrEvent>) => {
|
||||
state.note = action.payload;
|
||||
},
|
||||
setSelectedCustomRelays: (state, action: PayloadAction<false | Array<string>>) => {
|
||||
state.selectedCustomRelays = action.payload;
|
||||
},
|
||||
reset: () => InitState,
|
||||
},
|
||||
});
|
||||
|
||||
export const { setShow, setNote, setSelectedCustomRelays, reset } = ReBroadcastSlice.actions;
|
||||
|
||||
export const reducer = ReBroadcastSlice.reducer;
|
@ -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<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export default store;
|
@ -7,7 +7,6 @@ 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,
|
||||
@ -24,7 +23,6 @@ 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";
|
||||
@ -218,12 +216,10 @@ export const router = createBrowserRouter([
|
||||
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Provider store={Store}>
|
||||
<IntlProvider>
|
||||
<SnortContext.Provider value={System}>
|
||||
<RouterProvider router={router} />
|
||||
</SnortContext.Provider>
|
||||
</IntlProvider>
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ export interface HookFilter<TSnapshot> {
|
||||
*/
|
||||
export abstract class ExternalStore<TSnapshot> {
|
||||
#hooks: Array<HookFilter<TSnapshot>> = [];
|
||||
#snapshot: Readonly<TSnapshot> = {} as Readonly<TSnapshot>;
|
||||
#snapshot: TSnapshot = {} as TSnapshot;
|
||||
#changed = true;
|
||||
|
||||
hook(fn: HookFn<TSnapshot>) {
|
||||
@ -35,7 +35,9 @@ export abstract class ExternalStore<TSnapshot> {
|
||||
protected notifyChange(sn?: TSnapshot) {
|
||||
this.#changed = true;
|
||||
if (this.#hooks.length > 0) {
|
||||
queueMicrotask(() => {
|
||||
this.#hooks.forEach(h => h.fn(sn));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
70
packages/system/src/encrypted.ts
Normal file
70
packages/system/src/encrypted.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { pbkdf2 } from "@noble/hashes/pbkdf2";
|
||||
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 = {c: 32, 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);
|
||||
}
|
||||
|
||||
decrypt(pin: string) {
|
||||
const key = pbkdf2(sha256, 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, ciphertext);
|
||||
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 create(content: string, pin: string) {
|
||||
const salt = randomBytes(24);
|
||||
const nonce = randomBytes(24);
|
||||
const plaintext = hexToBytes(content);
|
||||
const key = pbkdf2(sha256, pin, salt, PinEncrypted.#opts);
|
||||
const mac = base64.encode(hmac(sha256, key, plaintext));
|
||||
const ciphertext = xchacha20(key, nonce, plaintext, plaintext);
|
||||
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
|
||||
}
|
||||
|
@ -185,8 +185,9 @@ 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(["e", rootOrReplyAsRoot?.value ?? "", rootOrReplyAsRoot?.relay ?? "", "root"]);
|
||||
}
|
||||
eb.tag(["e", replyTo.id, replyTo.relays?.[0] ?? "", "reply"]);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { MessageEncryptor, MessageEncryptorPayload, MessageEncryptorVersion } from "index";
|
||||
|
||||
import { base64 } from "@scure/base";
|
||||
import { randomBytes } from "@noble/hashes/utils";
|
||||
import { streamXOR as xchacha20 } from "@stablelib/xchacha20";
|
||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||
|
@ -27,6 +27,7 @@ export * from "./text";
|
||||
export * from "./pow";
|
||||
export * from "./pow-util";
|
||||
export * from "./query-optimizer";
|
||||
export * from "./encrypted";
|
||||
|
||||
export * from "./impl/nip4";
|
||||
export * from "./impl/nip44";
|
||||
|
@ -300,7 +300,7 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
||||
{
|
||||
filters: [{ ...f, ids: [...resultIds] }],
|
||||
strategy: RequestStrategy.ExplicitRelays,
|
||||
relay: "",
|
||||
relay: qSend.relay,
|
||||
},
|
||||
cacheResults as Array<TaggedNostrEvent>,
|
||||
);
|
||||
|
@ -202,7 +202,7 @@ export class Query implements QueryBase {
|
||||
*/
|
||||
insertCompletedTrace(subq: BuiltRawReqFilter, data: Readonly<Array<TaggedNostrEvent>>) {
|
||||
const qt = new QueryTrace(
|
||||
"",
|
||||
subq.relay,
|
||||
subq.filters,
|
||||
"",
|
||||
() => {
|
||||
|
@ -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 {
|
||||
|
@ -6,19 +6,8 @@ import { ReqFilter } from "nostr";
|
||||
export function trimFilters(filters: Array<ReqFilter>) {
|
||||
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<string | number>).length > 0)) {
|
||||
fNew.push(f);
|
||||
}
|
||||
}
|
||||
|
104
yarn.lock
104
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
|
||||
@ -2751,7 +2730,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
|
||||
@ -3590,13 +3568,6 @@ __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
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/uuid@npm:^9.0.2":
|
||||
version: 9.0.2
|
||||
resolution: "@types/uuid@npm:9.0.2"
|
||||
@ -7614,13 +7585,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 +11237,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 +11454,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 +11584,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,15 +13331,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-sync-external-store@npm:^1.0.0":
|
||||
version: 1.2.0
|
||||
resolution: "use-sync-external-store@npm:1.2.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "util-deprecate@npm:1.0.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user