Various
This commit is contained in:
parent
4b57d57f94
commit
71f7f728fd
@ -6,7 +6,6 @@
|
|||||||
"@lightninglabs/lnc-web": "^0.2.3-alpha",
|
"@lightninglabs/lnc-web": "^0.2.3-alpha",
|
||||||
"@noble/curves": "^1.0.0",
|
"@noble/curves": "^1.0.0",
|
||||||
"@noble/hashes": "^1.2.0",
|
"@noble/hashes": "^1.2.0",
|
||||||
"@reduxjs/toolkit": "^1.9.1",
|
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"@scure/bip32": "^1.3.0",
|
"@scure/bip32": "^1.3.0",
|
||||||
"@scure/bip39": "^1.1.1",
|
"@scure/bip39": "^1.1.1",
|
||||||
@ -27,7 +26,6 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-intersection-observer": "^9.4.1",
|
"react-intersection-observer": "^9.4.1",
|
||||||
"react-intl": "^6.4.4",
|
"react-intl": "^6.4.4",
|
||||||
"react-redux": "^8.0.5",
|
|
||||||
"react-router-dom": "^6.5.0",
|
"react-router-dom": "^6.5.0",
|
||||||
"react-textarea-autosize": "^8.4.0",
|
"react-textarea-autosize": "^8.4.0",
|
||||||
"react-twitter-embed": "^4.0.4",
|
"react-twitter-embed": "^4.0.4",
|
||||||
|
@ -2,7 +2,7 @@ import { EventKind, EventPublisher, RequestBuilder, TaggedNostrEvent } from "@sn
|
|||||||
import { UnwrappedGift, db } from "Db";
|
import { UnwrappedGift, db } from "Db";
|
||||||
import { findTag, unwrap } from "SnortUtils";
|
import { findTag, unwrap } from "SnortUtils";
|
||||||
import { RefreshFeedCache } from "./RefreshFeedCache";
|
import { RefreshFeedCache } from "./RefreshFeedCache";
|
||||||
import { LoginSession } from "Login";
|
import { LoginSession, LoginSessionType } from "Login";
|
||||||
|
|
||||||
export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -15,7 +15,7 @@ export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
|||||||
|
|
||||||
buildSub(session: LoginSession, rb: RequestBuilder): void {
|
buildSub(session: LoginSession, rb: RequestBuilder): void {
|
||||||
const pubkey = session.publicKey;
|
const pubkey = session.publicKey;
|
||||||
if (pubkey) {
|
if (pubkey && session.type === LoginSessionType.PrivateKey) {
|
||||||
rb.withFilter().kinds([EventKind.GiftWrap]).tag("p", [pubkey]).since(this.newest());
|
rb.withFilter().kinds([EventKind.GiftWrap]).tag("p", [pubkey]).since(this.newest());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ export default function BadgeList({ badges }: { badges: TaggedNostrEvent[] }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<Modal className="reactions-modal" onClose={() => setShowModal(false)}>
|
<Modal id="badges" className="reactions-modal" onClose={() => setShowModal(false)}>
|
||||||
<div className="reactions-view">
|
<div className="reactions-view">
|
||||||
<div className="close" onClick={() => setShowModal(false)}>
|
<div className="close" onClick={() => setShowModal(false)}>
|
||||||
<Icon name="close" />
|
<Icon name="close" />
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import NoteTime from "Element/NoteTime";
|
import NoteTime from "Element/NoteTime";
|
||||||
import Text from "Element/Text";
|
import Text from "Element/Text";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
|
@ -2,7 +2,7 @@ import { NostrLink } from "@snort/system";
|
|||||||
import { useArticles } from "Feed/ArticlesFeed";
|
import { useArticles } from "Feed/ArticlesFeed";
|
||||||
import { orderDescending } from "SnortUtils";
|
import { orderDescending } from "SnortUtils";
|
||||||
import Note from "../Note";
|
import Note from "../Note";
|
||||||
import { useReactions } from "Feed/FeedReactions";
|
import { useReactions } from "Feed/Reactions";
|
||||||
|
|
||||||
export default function Articles() {
|
export default function Articles() {
|
||||||
const data = useArticles();
|
const data = useArticles();
|
||||||
|
@ -2,7 +2,7 @@ import "./FollowButton.css";
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { HexKey } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { parseId } from "SnortUtils";
|
import { parseId } from "SnortUtils";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
|
@ -2,13 +2,16 @@ import { ReactNode } from "react";
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { HexKey } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
import { FollowsFeed } from "Cache";
|
import { FollowsFeed } from "Cache";
|
||||||
|
import AsyncButton from "./AsyncButton";
|
||||||
|
import { setFollows } from "Login";
|
||||||
|
import { dedupe } from "@snort/shared";
|
||||||
|
|
||||||
export interface FollowListBaseProps {
|
export interface FollowListBaseProps {
|
||||||
pubkeys: HexKey[];
|
pubkeys: HexKey[];
|
||||||
@ -30,13 +33,15 @@ export default function FollowListBase({
|
|||||||
profileActions,
|
profileActions,
|
||||||
}: FollowListBaseProps) {
|
}: FollowListBaseProps) {
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const { follows, relays } = useLogin();
|
const login = useLogin();
|
||||||
|
|
||||||
async function followAll() {
|
async function followAll() {
|
||||||
if (publisher) {
|
if (publisher) {
|
||||||
const ev = await publisher.contactList([...pubkeys, ...follows.item], relays.item);
|
const newFollows = dedupe([...pubkeys, ...login.follows.item]);
|
||||||
await FollowsFeed.backFill(System, pubkeys);
|
const ev = await publisher.contactList(newFollows, login.relays.item);
|
||||||
System.BroadcastEvent(ev);
|
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="flex mt10 mb10">
|
||||||
<div className="f-grow bold">{title}</div>
|
<div className="f-grow bold">{title}</div>
|
||||||
{actions}
|
{actions}
|
||||||
<button className="transparent" type="button" onClick={() => followAll()}>
|
<AsyncButton className="transparent" type="button" onClick={() => followAll()}>
|
||||||
<FormattedMessage {...messages.FollowAll} />
|
<FormattedMessage {...messages.FollowAll} />
|
||||||
</button>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{pubkeys?.map(a => (
|
{pubkeys?.map(a => (
|
||||||
|
@ -7,15 +7,15 @@ import messages from "./messages";
|
|||||||
|
|
||||||
export default function LogoutButton() {
|
export default function LogoutButton() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const publicKey = useLogin().publicKey;
|
const login = useLogin();
|
||||||
|
|
||||||
if (!publicKey) return;
|
if (!login.publicKey) return;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="secondary"
|
className="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logout(publicKey);
|
logout(login.id);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}}>
|
}}>
|
||||||
<FormattedMessage {...messages.Logout} />
|
<FormattedMessage {...messages.Logout} />
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
width: 500px;
|
width: 500px;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
|
--border-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body button.secondary:hover {
|
.modal-body button.secondary:hover {
|
||||||
|
@ -1,26 +1,17 @@
|
|||||||
import "./Modal.css";
|
import "./Modal.css";
|
||||||
import { useEffect, MouseEventHandler, ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
|
id: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClose?: MouseEventHandler;
|
onClose?: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal(props: ModalProps) {
|
export default function Modal(props: ModalProps) {
|
||||||
const onClose = props.onClose || (() => undefined);
|
return <div className={`modal${props.className ? ` ${props.className}` : ""}`} onClick={props.onClose}>
|
||||||
const className = props.className || "";
|
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
||||||
|
{props.children}
|
||||||
useEffect(() => {
|
|
||||||
document.body.classList.add("scroll-lock");
|
|
||||||
return () => document.body.classList.remove("scroll-lock");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`modal ${className}`} onClick={onClose}>
|
|
||||||
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import AsyncButton from "Element/AsyncButton";
|
|||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import Copy from "Element/Copy";
|
import Copy from "Element/Copy";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { debounce } from "SnortUtils";
|
import { debounce } from "SnortUtils";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import SnortServiceProvider from "Nip05/SnortServiceProvider";
|
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 { TaggedNostrEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt, parseZap, NostrLink } from "@snort/system";
|
||||||
|
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import Text from "Element/Text";
|
import Text from "Element/Text";
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
|
|
||||||
import { TranslateHost } from "Const";
|
import { TranslateHost } from "Const";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { setPinned, setBookmarked } from "Login";
|
import { setPinned, setBookmarked } from "Login";
|
||||||
import {
|
|
||||||
setNote as setReBroadcastNote,
|
|
||||||
setShow as setReBroadcastShow,
|
|
||||||
reset as resetReBroadcast,
|
|
||||||
} from "State/ReBroadcast";
|
|
||||||
import messages from "Element/messages";
|
import messages from "Element/messages";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { RootState } from "State/Store";
|
|
||||||
import { ReBroadcaster } from "./ReBroadcaster";
|
import { ReBroadcaster } from "./ReBroadcaster";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface NoteTranslation {
|
export interface NoteTranslation {
|
||||||
text: string;
|
text: string;
|
||||||
@ -33,15 +27,12 @@ interface NosteContextMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const { pinned, bookmarked, publicKey, preferences: prefs } = login;
|
const { pinned, bookmarked, publicKey, preferences: prefs } = login;
|
||||||
const { mute, block } = useModeration();
|
const { mute, block } = useModeration();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const showReBroadcastModal = useSelector((s: RootState) => s.reBroadcast.show);
|
const [showBroadcast, setShowBroadcast] = useState(false);
|
||||||
const reBroadcastNote = useSelector((s: RootState) => s.reBroadcast.note);
|
|
||||||
const willRenderReBroadcast = showReBroadcastModal && reBroadcastNote && reBroadcastNote?.id === ev.id;
|
|
||||||
const lang = window.navigator.language;
|
const lang = window.navigator.language;
|
||||||
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
||||||
type: "language",
|
type: "language",
|
||||||
@ -119,12 +110,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleReBroadcastButtonClick = () => {
|
const handleReBroadcastButtonClick = () => {
|
||||||
if (reBroadcastNote?.id !== ev.id) {
|
setShowBroadcast(true);
|
||||||
dispatch(resetReBroadcast());
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setReBroadcastNote(ev));
|
|
||||||
dispatch(setReBroadcastShow(!showReBroadcastModal));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function menuItems() {
|
function menuItems() {
|
||||||
@ -214,7 +200,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
|
|||||||
menuClassName="ctx-menu">
|
menuClassName="ctx-menu">
|
||||||
{menuItems()}
|
{menuItems()}
|
||||||
</Menu>
|
</Menu>
|
||||||
{willRenderReBroadcast && <ReBroadcaster />}
|
{showBroadcast && <ReBroadcaster ev={ev} onClose={() => setShowBroadcast(false)} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,15 @@
|
|||||||
import "./NoteCreator.css";
|
import "./NoteCreator.css";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink, NostrEvent } from "@snort/system";
|
||||||
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
|
|
||||||
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { openFile } from "SnortUtils";
|
import { openFile } from "SnortUtils";
|
||||||
import Textarea from "Element/Textarea";
|
import Textarea from "Element/Textarea";
|
||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import useFileUpload from "Upload";
|
import useFileUpload from "Upload";
|
||||||
import Note from "Element/Note";
|
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 { ClipboardEventHandler } from "react";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
@ -34,37 +18,24 @@ import AsyncButton from "Element/AsyncButton";
|
|||||||
import { AsyncIcon } from "Element/AsyncIcon";
|
import { AsyncIcon } from "Element/AsyncIcon";
|
||||||
import { fetchNip05Pubkey } from "@snort/shared";
|
import { fetchNip05Pubkey } from "@snort/shared";
|
||||||
import { ZapTarget } from "Zapper";
|
import { ZapTarget } from "Zapper";
|
||||||
|
import { useNoteCreator } from "State/NoteCreator";
|
||||||
|
|
||||||
export function NoteCreator() {
|
export function NoteCreator() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const uploader = useFileUpload();
|
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 login = useLogin();
|
||||||
|
const note = useNoteCreator();
|
||||||
const relays = login.relays;
|
const relays = login.relays;
|
||||||
|
|
||||||
async function buildNote() {
|
async function buildNote() {
|
||||||
try {
|
try {
|
||||||
dispatch(setError(""));
|
note.update(v => v.error = "");
|
||||||
if (note && publisher) {
|
if (note && publisher) {
|
||||||
let extraTags: Array<Array<string>> | undefined;
|
let extraTags: Array<Array<string>> | undefined;
|
||||||
if (zapSplits) {
|
if (note.zapSplits) {
|
||||||
const parsedSplits = [] as Array<ZapTarget>;
|
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)) {
|
if (s.value.startsWith(NostrPrefix.PublicKey) || s.value.startsWith(NostrPrefix.Profile)) {
|
||||||
const link = tryParseNostrLink(s.value);
|
const link = tryParseNostrLink(s.value);
|
||||||
if (link) {
|
if (link) {
|
||||||
@ -114,43 +85,53 @@ export function NoteCreator() {
|
|||||||
extraTags = parsedSplits.map(v => ["zap", v.value, "", String(v.weight)]);
|
extraTags = parsedSplits.map(v => ["zap", v.value, "", String(v.weight)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sensitive) {
|
if (note.sensitive) {
|
||||||
extraTags ??= [];
|
extraTags ??= [];
|
||||||
extraTags.push(["content-warning", sensitive]);
|
extraTags.push(["content-warning", note.sensitive]);
|
||||||
}
|
}
|
||||||
const kind = pollOptions ? EventKind.Polls : EventKind.TextNote;
|
const kind = note.pollOptions ? EventKind.Polls : EventKind.TextNote;
|
||||||
if (pollOptions) {
|
if (note.pollOptions) {
|
||||||
extraTags ??= [];
|
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) => {
|
const hk = (eb: EventBuilder) => {
|
||||||
extraTags?.forEach(t => eb.tag(t));
|
extraTags?.forEach(t => eb.tag(t));
|
||||||
eb.kind(kind);
|
eb.kind(kind);
|
||||||
return eb;
|
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;
|
return ev;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
note.update(v => {
|
||||||
dispatch(setError(e.message));
|
if (e instanceof Error) {
|
||||||
} else {
|
v.error = e.message;
|
||||||
dispatch(setError(e as string));
|
} else {
|
||||||
}
|
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() {
|
async function sendNote() {
|
||||||
const ev = await buildNote();
|
const ev = await buildNote();
|
||||||
if (ev) {
|
if (ev) {
|
||||||
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, ev));
|
await sendEventToRelays(ev);
|
||||||
else System.BroadcastEvent(ev);
|
for (const oe of note.otherEvents ?? []) {
|
||||||
dispatch(reset());
|
await sendEventToRelays(oe);
|
||||||
for (const oe of otherEvents) {
|
|
||||||
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, oe));
|
|
||||||
else System.BroadcastEvent(oe);
|
|
||||||
}
|
}
|
||||||
dispatch(reset());
|
note.update(v => {
|
||||||
|
v.reset();
|
||||||
|
v.show = false;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,10 +141,14 @@ export function NoteCreator() {
|
|||||||
if (file) {
|
if (file) {
|
||||||
uploadFile(file);
|
uploadFile(file);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (e) {
|
||||||
if (error instanceof Error) {
|
note.update(v => {
|
||||||
dispatch(setError(error?.message));
|
if (e instanceof Error) {
|
||||||
}
|
v.error = e.message;
|
||||||
|
} else {
|
||||||
|
v.error = e as string;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,35 +156,39 @@ export function NoteCreator() {
|
|||||||
try {
|
try {
|
||||||
if (file) {
|
if (file) {
|
||||||
const rx = await uploader.upload(file, file.name);
|
const rx = await uploader.upload(file, file.name);
|
||||||
if (rx.header) {
|
note.update(v => {
|
||||||
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`;
|
if (rx.header) {
|
||||||
dispatch(setNote(`${note ? `${note}\n` : ""}${link}`));
|
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`;
|
||||||
dispatch(setOtherEvents([...otherEvents, rx.header]));
|
v.note = `${v.note ? `${v.note}\n` : ""}${link}`;
|
||||||
} else if (rx.url) {
|
v.otherEvents = [...(v.otherEvents ?? []), rx.header];
|
||||||
dispatch(setNote(`${note ? `${note}\n` : ""}${rx.url}`));
|
} else if (rx.url) {
|
||||||
} else if (rx?.error) {
|
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
|
||||||
dispatch(setError(rx.error));
|
} else if (rx?.error) {
|
||||||
|
v.error = rx.error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
note.update(v => {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
v.error = e.message;
|
||||||
|
} else {
|
||||||
|
v.error = e as string;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
dispatch(setError(error?.message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
const { value } = ev.target;
|
const { value } = ev.target;
|
||||||
dispatch(setNote(value));
|
note.update(n => n.note = value);
|
||||||
if (value) {
|
|
||||||
dispatch(setActive(true));
|
|
||||||
} else {
|
|
||||||
dispatch(setActive(false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
dispatch(reset());
|
note.update(v => {
|
||||||
|
v.show = false;
|
||||||
|
v.reset();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
async function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||||
@ -208,21 +197,19 @@ export function NoteCreator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPreview() {
|
async function loadPreview() {
|
||||||
if (preview) {
|
if (note.preview) {
|
||||||
dispatch(setPreview(undefined));
|
note.update(v => v.preview = undefined);
|
||||||
} else if (publisher) {
|
} else if (publisher) {
|
||||||
const tmpNote = await buildNote();
|
const tmpNote = await buildNote();
|
||||||
if (tmpNote) {
|
note.update(v => v.preview = tmpNote);
|
||||||
dispatch(setPreview(tmpNote));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreviewNote() {
|
function getPreviewNote() {
|
||||||
if (preview) {
|
if (note.preview) {
|
||||||
return (
|
return (
|
||||||
<Note
|
<Note
|
||||||
data={preview as TaggedNostrEvent}
|
data={note.preview as TaggedNostrEvent}
|
||||||
related={[]}
|
related={[]}
|
||||||
options={{
|
options={{
|
||||||
showContextMenu: false,
|
showContextMenu: false,
|
||||||
@ -236,13 +223,13 @@ export function NoteCreator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPollOptions() {
|
function renderPollOptions() {
|
||||||
if (pollOptions) {
|
if (note.pollOptions) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Poll Options" />
|
<FormattedMessage defaultMessage="Poll Options" />
|
||||||
</h4>
|
</h4>
|
||||||
{pollOptions?.map((a, i) => (
|
{note.pollOptions?.map((a, i) => (
|
||||||
<div className="form-group w-max" key={`po-${i}`}>
|
<div className="form-group w-max" key={`po-${i}`}>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage defaultMessage="Option: {n}" values={{ n: i + 1 }} />
|
<FormattedMessage defaultMessage="Option: {n}" values={{ n: i + 1 }} />
|
||||||
@ -257,7 +244,7 @@ export function NoteCreator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button onClick={() => dispatch(setPollOptions([...pollOptions, ""]))}>
|
<button onClick={() => note.update(v => v.pollOptions = [...(note.pollOptions ?? []), ""])}>
|
||||||
<Icon name="plus" size={14} />
|
<Icon name="plus" size={14} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@ -266,18 +253,18 @@ export function NoteCreator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changePollOption(i: number, v: string) {
|
function changePollOption(i: number, v: string) {
|
||||||
if (pollOptions) {
|
if (note.pollOptions) {
|
||||||
const copy = [...pollOptions];
|
const copy = [...note.pollOptions];
|
||||||
copy[i] = v;
|
copy[i] = v;
|
||||||
dispatch(setPollOptions(copy));
|
note.update(v => v.pollOptions = copy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePollOption(i: number) {
|
function removePollOption(i: number) {
|
||||||
if (pollOptions) {
|
if (note.pollOptions) {
|
||||||
const copy = [...pollOptions];
|
const copy = [...note.pollOptions];
|
||||||
copy.splice(i, 1);
|
copy.splice(i, 1);
|
||||||
dispatch(setPollOptions(copy));
|
note.update(v => v.pollOptions = copy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,19 +279,16 @@ export function NoteCreator() {
|
|||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
checked={!note.selectedCustomRelays || note.selectedCustomRelays.includes(r)}
|
||||||
onChange={e =>
|
onChange={e => {
|
||||||
dispatch(
|
note.update(v => v.selectedCustomRelays = (
|
||||||
setSelectedCustomRelays(
|
// set false if all relays selected
|
||||||
// set false if all relays selected
|
e.target.checked && note.selectedCustomRelays && note.selectedCustomRelays.length == a.length - 1
|
||||||
e.target.checked && selectedCustomRelays && selectedCustomRelays.length == a.length - 1
|
? undefined
|
||||||
? false
|
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
a.filter(el => el === r ? e.target.checked : !note.selectedCustomRelays || note.selectedCustomRelays.includes(el))
|
||||||
a.filter(el =>
|
));
|
||||||
el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el),
|
}
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -345,163 +329,147 @@ export function NoteCreator() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (!note.show) return null;
|
||||||
<>
|
return (<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => v.show = false)}>
|
||||||
{show && (
|
{note.replyTo && (
|
||||||
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
<Note
|
||||||
{replyTo && (
|
data={note.replyTo}
|
||||||
<Note
|
related={[]}
|
||||||
data={replyTo}
|
options={{
|
||||||
related={[]}
|
showFooter: false,
|
||||||
options={{
|
showContextMenu: false,
|
||||||
showFooter: false,
|
showTime: false,
|
||||||
showContextMenu: false,
|
canClick: false,
|
||||||
showTime: false,
|
showMedia: false,
|
||||||
canClick: false,
|
}}
|
||||||
showMedia: false,
|
/>
|
||||||
}}
|
)}
|
||||||
/>
|
{note.preview && getPreviewNote()}
|
||||||
)}
|
{!note.preview && (
|
||||||
{preview && getPreviewNote()}
|
<div onPaste={handlePaste} className={`note-creator${note.pollOptions ? " poll" : ""}`}>
|
||||||
{!preview && (
|
<Textarea
|
||||||
<div onPaste={handlePaste} className={`note-creator${pollOptions ? " poll" : ""}`}>
|
autoFocus
|
||||||
<Textarea
|
className={`textarea ${note.active ? "textarea--focused" : ""}`}
|
||||||
autoFocus
|
onChange={c => onChange(c)}
|
||||||
className={`textarea ${active ? "textarea--focused" : ""}`}
|
value={note.note}
|
||||||
onChange={onChange}
|
onFocus={() => note.update(v => v.active = true)}
|
||||||
value={note}
|
onKeyDown={e => {
|
||||||
onFocus={() => dispatch(setActive(true))}
|
if (e.key === "Enter" && e.metaKey) {
|
||||||
onKeyDown={e => {
|
sendNote().catch(console.warn);
|
||||||
if (e.key === "Enter" && e.metaKey) {
|
}
|
||||||
sendNote().catch(console.warn);
|
}}
|
||||||
}
|
/>
|
||||||
}}
|
{renderPollOptions()}
|
||||||
/>
|
</div>
|
||||||
{renderPollOptions()}
|
)}
|
||||||
</div>
|
<div className="flex f-space">
|
||||||
)}
|
<div className="flex g8">
|
||||||
<div className="flex f-space">
|
<ProfileImage pubkey={login.publicKey ?? ""} className="note-creator-icon" link="" showUsername={false} showFollowingMark={false} />
|
||||||
<div className="flex g8">
|
{note.pollOptions === undefined && !note.replyTo && (
|
||||||
<ProfileImage pubkey={login.publicKey ?? ""} className="note-creator-icon" link="" showUsername={false} />
|
<div className="note-creator-icon">
|
||||||
{pollOptions === undefined && !replyTo && (
|
<Icon name="pie-chart" onClick={() => note.update(v => v.pollOptions = ["A", "B"])} size={24} />
|
||||||
<div className="note-creator-icon">
|
|
||||||
<Icon name="pie-chart" onClick={() => dispatch(setPollOptions(["A", "B"]))} size={24} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
|
|
||||||
<button className="secondary" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
|
||||||
<FormattedMessage defaultMessage="Advanced" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex g8">
|
|
||||||
<button className="secondary" onClick={cancel}>
|
|
||||||
<FormattedMessage defaultMessage="Cancel" />
|
|
||||||
</button>
|
|
||||||
<AsyncButton className="primary" onClick={onSubmit}>
|
|
||||||
{replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{error && <span className="error">{error}</span>}
|
)}
|
||||||
{showAdvanced && (
|
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
|
||||||
<>
|
<button className="secondary" onClick={() => note.update(v => v.advanced = !v.advanced)}>
|
||||||
<button className="secondary" onClick={loadPreview}>
|
<FormattedMessage defaultMessage="Advanced" />
|
||||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<div>
|
<div className="flex g8">
|
||||||
<h4>
|
<button className="secondary" onClick={cancel}>
|
||||||
<FormattedMessage defaultMessage="Custom Relays" />
|
<FormattedMessage defaultMessage="Cancel" />
|
||||||
</h4>
|
</button>
|
||||||
<p>
|
<AsyncButton onClick={onSubmit}>
|
||||||
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
{note.replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
|
||||||
</p>
|
</AsyncButton>
|
||||||
{renderRelayCustomisation()}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-column g8">
|
{note.error && <span className="error">{note.error}</span>}
|
||||||
<h4>
|
{note.advanced && (
|
||||||
<FormattedMessage defaultMessage="Zap Splits" />
|
<>
|
||||||
</h4>
|
<button className="secondary" onClick={loadPreview}>
|
||||||
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||||
<div className="flex-column g8">
|
</button>
|
||||||
{[...(zapSplits ?? [])].map((v, i, arr) => (
|
<div>
|
||||||
<div className="flex f-center g8">
|
<h4>
|
||||||
<div className="flex-column f-4 g4">
|
<FormattedMessage defaultMessage="Custom Relays" />
|
||||||
<h4>
|
</h4>
|
||||||
<FormattedMessage defaultMessage="Recipient" />
|
<p>
|
||||||
</h4>
|
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
||||||
<input
|
</p>
|
||||||
type="text"
|
{renderRelayCustomisation()}
|
||||||
value={v.value}
|
</div>
|
||||||
onChange={e =>
|
<div className="flex-column g8">
|
||||||
dispatch(
|
<h4>
|
||||||
setZapSplits(arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
|
<FormattedMessage defaultMessage="Zap Splits" />
|
||||||
)
|
</h4>
|
||||||
}
|
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
||||||
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
<div className="flex-column g8">
|
||||||
/>
|
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
|
||||||
</div>
|
<div className="flex f-center g8">
|
||||||
<div className="flex-column f-1 g4">
|
<div className="flex-column f-4 g4">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Weight" />
|
<FormattedMessage defaultMessage="Recipient" />
|
||||||
</h4>
|
</h4>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
min={0}
|
value={v.value}
|
||||||
value={v.weight}
|
onChange={e => note.update(v => v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv)))}
|
||||||
onChange={e =>
|
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
||||||
dispatch(
|
/>
|
||||||
setZapSplits(
|
</div>
|
||||||
arr.map((vv, ii) => (ii === i ? { ...vv, weight: Number(e.target.value) } : vv)),
|
<div className="flex-column f-1 g4">
|
||||||
),
|
<h4>
|
||||||
)
|
<FormattedMessage defaultMessage="Weight" />
|
||||||
}
|
</h4>
|
||||||
/>
|
<input
|
||||||
</div>
|
type="number"
|
||||||
<div className="flex-column f-shrink g4">
|
min={0}
|
||||||
<div> </div>
|
value={v.weight}
|
||||||
<Icon
|
onChange={e => note.update(v => v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, weight: Number(e.target.value) } : vv)))}
|
||||||
name="close"
|
/>
|
||||||
onClick={() => dispatch(setZapSplits((zapSplits ?? []).filter((_v, ii) => ii !== i)))}
|
</div>
|
||||||
/>
|
<div className="flex-column f-shrink g4">
|
||||||
</div>
|
<div> </div>
|
||||||
</div>
|
<Icon
|
||||||
))}
|
name="close"
|
||||||
<button
|
onClick={() => note.update(v => v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i))}
|
||||||
type="button"
|
/>
|
||||||
onClick={() =>
|
|
||||||
dispatch(setZapSplits([...(zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
|
||||||
}>
|
|
||||||
<FormattedMessage defaultMessage="Add" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="warning">
|
|
||||||
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-column g8">
|
))}
|
||||||
<h4>
|
<button
|
||||||
<FormattedMessage defaultMessage="Sensitive Content" />
|
type="button"
|
||||||
</h4>
|
onClick={() => note.update(v => v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }])}>
|
||||||
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
|
<FormattedMessage defaultMessage="Add" />
|
||||||
<input
|
</button>
|
||||||
className="w-max"
|
</div>
|
||||||
type="text"
|
<span className="warning">
|
||||||
value={sensitive}
|
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
|
||||||
onChange={e => dispatch(setSensitive(e.target.value))}
|
</span>
|
||||||
maxLength={50}
|
</div>
|
||||||
minLength={1}
|
<div className="flex-column g8">
|
||||||
placeholder={formatMessage({
|
<h4>
|
||||||
defaultMessage: "Reason",
|
<FormattedMessage defaultMessage="Sensitive Content" />
|
||||||
})}
|
</h4>
|
||||||
/>
|
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
|
||||||
<span className="warning">
|
<input
|
||||||
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
className="w-max"
|
||||||
</span>
|
type="text"
|
||||||
</div>
|
value={note.sensitive}
|
||||||
</>
|
onChange={e => note.update(v => v.sensitive = e.target.value)}
|
||||||
)}
|
maxLength={50}
|
||||||
</Modal>
|
minLength={1}
|
||||||
)}
|
placeholder={formatMessage({
|
||||||
</>
|
defaultMessage: "Reason",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<span className="warning">
|
||||||
|
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import React, { HTMLProps, useContext, useEffect, useState } from "react";
|
import React, { HTMLProps, useContext, useEffect, useState } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { useLongPress } from "use-long-press";
|
import { useLongPress } from "use-long-press";
|
||||||
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, NostrLink } from "@snort/system";
|
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, NostrLink } from "@snort/system";
|
||||||
import { SnortContext, useUserProfile } from "@snort/system-react";
|
import { SnortContext, useUserProfile } from "@snort/system-react";
|
||||||
|
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { delay, findTag, normalizeReaction } from "SnortUtils";
|
import { delay, findTag, normalizeReaction } from "SnortUtils";
|
||||||
import { NoteCreator } from "Element/NoteCreator";
|
import { NoteCreator } from "Element/NoteCreator";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import { ZapsSummary } from "Element/Zap";
|
import { ZapsSummary } from "Element/Zap";
|
||||||
import { RootState } from "State/Store";
|
|
||||||
import { setReplyTo, setShow, reset } from "State/NoteCreator";
|
|
||||||
import { AsyncIcon } from "Element/AsyncIcon";
|
import { AsyncIcon } from "Element/AsyncIcon";
|
||||||
|
|
||||||
import { useWallet } from "Wallet";
|
import { useWallet } from "Wallet";
|
||||||
@ -22,6 +19,7 @@ import { ZapPoolController } from "ZapPoolController";
|
|||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import { Zapper, ZapTarget } from "Zapper";
|
import { Zapper, ZapTarget } from "Zapper";
|
||||||
import { getDisplayName } from "./ProfileImage";
|
import { getDisplayName } from "./ProfileImage";
|
||||||
|
import { useNoteCreator } from "State/NoteCreator";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -47,7 +45,6 @@ export interface NoteFooterProps {
|
|||||||
|
|
||||||
export default function NoteFooter(props: NoteFooterProps) {
|
export default function NoteFooter(props: NoteFooterProps) {
|
||||||
const { ev, positive, reposts, zaps } = props;
|
const { ev, positive, reposts, zaps } = props;
|
||||||
const dispatch = useDispatch();
|
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
@ -55,9 +52,8 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const author = useUserProfile(ev.pubkey);
|
const author = useUserProfile(ev.pubkey);
|
||||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
|
const note = useNoteCreator();
|
||||||
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
const willRenderNoteCreator = note.show && note.replyTo?.id === ev.id;
|
||||||
const willRenderNoteCreator = showNoteCreatorModal && replyTo?.id === ev.id;
|
|
||||||
const [tip, setTip] = useState(false);
|
const [tip, setTip] = useState(false);
|
||||||
const [zapping, setZapping] = useState(false);
|
const [zapping, setZapping] = useState(false);
|
||||||
const walletState = useWallet();
|
const walletState = useWallet();
|
||||||
@ -238,7 +234,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
function replyIcon() {
|
function replyIcon() {
|
||||||
return (
|
return (
|
||||||
<AsyncFooterIcon
|
<AsyncFooterIcon
|
||||||
className={showNoteCreatorModal ? "reacted" : ""}
|
className={note.show ? "reacted" : ""}
|
||||||
iconName="reply"
|
iconName="reply"
|
||||||
title={formatMessage({ defaultMessage: "Reply" })}
|
title={formatMessage({ defaultMessage: "Reply" })}
|
||||||
value={0}
|
value={0}
|
||||||
@ -248,12 +244,13 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleReplyButtonClick = () => {
|
const handleReplyButtonClick = () => {
|
||||||
if (replyTo?.id !== ev.id) {
|
note.update(v => {
|
||||||
dispatch(reset());
|
if (v.replyTo?.id !== ev.id) {
|
||||||
}
|
v.reset();
|
||||||
|
}
|
||||||
dispatch(setReplyTo(ev));
|
v.show = true;
|
||||||
dispatch(setShow(!showNoteCreatorModal));
|
v.replyTo = ev;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { useWallet } from "Wallet";
|
import { useWallet } from "Wallet";
|
||||||
import { unwrap } from "SnortUtils";
|
import { unwrap } from "SnortUtils";
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
|
@ -10,7 +10,7 @@ import { Toastore } from "Toaster";
|
|||||||
import { getDisplayName } from "Element/ProfileImage";
|
import { getDisplayName } from "Element/ProfileImage";
|
||||||
import { UserCache } from "Cache";
|
import { UserCache } from "Cache";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { WalletInvoiceState } from "Wallet";
|
import { WalletInvoiceState } from "Wallet";
|
||||||
|
|
||||||
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
|
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
|
||||||
|
@ -1,30 +1,27 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { TaggedNostrEvent } from "@snort/system";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
|
||||||
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import type { RootState } from "State/Store";
|
|
||||||
import { setShow, reset, setSelectedCustomRelays } from "State/ReBroadcast";
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { System } from "index";
|
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 publisher = useEventPublisher();
|
||||||
const { note, show, selectedCustomRelays } = useSelector((s: RootState) => s.reBroadcast);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
async function sendReBroadcast() {
|
async function sendReBroadcast() {
|
||||||
if (note && publisher) {
|
if (publisher) {
|
||||||
if (selectedCustomRelays) selectedCustomRelays.forEach(r => System.WriteOnceToRelay(r, note));
|
if (selected) {
|
||||||
else System.BroadcastEvent(note);
|
await Promise.all(selected.map(r => System.WriteOnceToRelay(r, ev)));
|
||||||
dispatch(reset());
|
} else {
|
||||||
|
System.BroadcastEvent(ev);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
dispatch(reset());
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
sendReBroadcast().catch(console.warn);
|
sendReBroadcast().catch(console.warn);
|
||||||
@ -46,19 +43,11 @@ export function ReBroadcaster() {
|
|||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!selectedCustomRelays || selectedCustomRelays.includes(r)}
|
checked={!selected || selected.includes(r)}
|
||||||
onChange={e =>
|
onChange={e => setSelected(
|
||||||
dispatch(
|
e.target.checked && selected && selected.length == a.length - 1
|
||||||
setSelectedCustomRelays(
|
? undefined
|
||||||
// set false if all relays selected
|
: a.filter(el => el === r ? e.target.checked : !selected || selected.includes(el)))
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -70,19 +59,17 @@ export function ReBroadcaster() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{show && (
|
<Modal id="broadcaster" className="note-creator-modal" onClose={onClose}>
|
||||||
<Modal className="note-creator-modal" onClose={() => dispatch(setShow(false))}>
|
{renderRelayCustomisation()}
|
||||||
{renderRelayCustomisation()}
|
<div className="note-creator-actions">
|
||||||
<div className="note-creator-actions">
|
<button className="secondary" onClick={onClose}>
|
||||||
<button className="secondary" onClick={cancel}>
|
<FormattedMessage {...messages.Cancel} />
|
||||||
<FormattedMessage {...messages.Cancel} />
|
</button>
|
||||||
</button>
|
<button onClick={onSubmit}>
|
||||||
<button onClick={onSubmit}>
|
<FormattedMessage {...messages.ReBroadcast} />
|
||||||
<FormattedMessage {...messages.ReBroadcast} />
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
|
|||||||
}, [show]);
|
}, [show]);
|
||||||
|
|
||||||
return show ? (
|
return show ? (
|
||||||
<Modal className="reactions-modal" onClose={onClose}>
|
<Modal id="reactions" className="reactions-modal" onClose={onClose}>
|
||||||
<div className="close" onClick={onClose}>
|
<div className="close" onClick={onClose}>
|
||||||
<Icon name="close" />
|
<Icon name="close" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import { LNURLSuccessAction } from "@snort/shared";
|
|||||||
|
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import QrCode from "Element/QrCode";
|
import QrCode from "Element/QrCode";
|
||||||
@ -180,7 +180,7 @@ export default function SendSats(props: SendSatsProps) {
|
|||||||
|
|
||||||
if (!(props.show ?? false)) return null;
|
if (!(props.show ?? false)) return null;
|
||||||
return (
|
return (
|
||||||
<Modal className="lnurl-modal" onClose={onClose}>
|
<Modal id="send-sats" className="lnurl-modal" onClose={onClose}>
|
||||||
<div className="p flex-column g12">
|
<div className="p flex-column g12">
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
<div className="flex f-grow">{props.title || title()}</div>
|
<div className="flex f-grow">{props.title || title()}</div>
|
||||||
|
@ -55,7 +55,7 @@ export function SpotlightMedia(props: SpotlightMediaProps) {
|
|||||||
|
|
||||||
export function SpotlightMediaModal(props: SpotlightMediaProps) {
|
export function SpotlightMediaModal(props: SpotlightMediaProps) {
|
||||||
return (
|
return (
|
||||||
<Modal onClose={props.onClose} className="spotlight">
|
<Modal id="spotlight" onClose={props.onClose} className="spotlight">
|
||||||
<SpotlightMedia {...props} />
|
<SpotlightMedia {...props} />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,7 @@ import Note from "Element/Note";
|
|||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import { FollowsFeed } from "Cache";
|
import { FollowsFeed } from "Cache";
|
||||||
import { LiveStreams } from "Element/LiveStreams";
|
import { LiveStreams } from "Element/LiveStreams";
|
||||||
import { useReactions } from "Feed/FeedReactions";
|
import { useReactions } from "Feed/Reactions";
|
||||||
import AsyncButton from "./AsyncButton";
|
import AsyncButton from "./AsyncButton";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
|
@ -4,7 +4,7 @@ import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
|||||||
import PageSpinner from "Element/PageSpinner";
|
import PageSpinner from "Element/PageSpinner";
|
||||||
import Note from "Element/Note";
|
import Note from "Element/Note";
|
||||||
import NostrBandApi from "External/NostrBand";
|
import NostrBandApi from "External/NostrBand";
|
||||||
import { useReactions } from "Feed/FeedReactions";
|
import { useReactions } from "Feed/Reactions";
|
||||||
|
|
||||||
export default function TrendingNotes() {
|
export default function TrendingNotes() {
|
||||||
const [posts, setPosts] = useState<Array<NostrEvent>>();
|
const [posts, setPosts] = useState<Array<NostrEvent>>();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { NostrPrefix, NostrEvent, NostrLink } from "@snort/system";
|
import { NostrPrefix, NostrEvent, NostrLink } from "@snort/system";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import Spinner from "Icons/Spinner";
|
import Spinner from "Icons/Spinner";
|
||||||
import { useState } from "react";
|
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 { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils";
|
||||||
import { makeNotification, sendNotification } from "Notifications";
|
import { makeNotification, sendNotification } from "Notifications";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { getMutedKeys } from "Feed/MuteList";
|
import { getMutedKeys } from "Feed/MuteList";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
@ -28,9 +28,7 @@ export default function useLoginFeed() {
|
|||||||
|
|
||||||
useRefreshFeedCache(Notifications, true);
|
useRefreshFeedCache(Notifications, true);
|
||||||
useRefreshFeedCache(FollowsFeed, true);
|
useRefreshFeedCache(FollowsFeed, true);
|
||||||
if (publisher?.supports("nip44")) {
|
useRefreshFeedCache(GiftsCache, true);
|
||||||
useRefreshFeedCache(GiftsCache, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const subLogin = useMemo(() => {
|
const subLogin = useMemo(() => {
|
||||||
if (!pubKey) return null;
|
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 { useRequestBuilder } from "@snort/system-react";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@ -8,10 +8,8 @@ export function useReactions(subId: string, ids: Array<NostrLink>, others?: (rb:
|
|||||||
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const rb = new RequestBuilder(subId);
|
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
|
const f = rb
|
||||||
.withFilter()
|
.withFilter()
|
||||||
.kinds(
|
.kinds(
|
||||||
@ -20,8 +18,7 @@ export function useReactions(subId: string, ids: Array<NostrLink>, others?: (rb:
|
|||||||
: [EventKind.ZapReceipt, EventKind.Repost],
|
: [EventKind.ZapReceipt, EventKind.Repost],
|
||||||
);
|
);
|
||||||
|
|
||||||
aTags.forEach(v => f.replyToLink(v));
|
ids.forEach(v => f.replyToLink(v));
|
||||||
eTags.forEach(v => f.replyToLink(v));
|
|
||||||
}
|
}
|
||||||
others?.(rb);
|
others?.(rb);
|
||||||
return rb.numFilters > 0 ? rb : null;
|
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 { EventKind, NostrLink, RequestBuilder, NoteCollection } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { useReactions } from "./FeedReactions";
|
import { useReactions } from "./Reactions";
|
||||||
|
|
||||||
export default function useThreadFeed(link: NostrLink) {
|
export default function useThreadFeed(link: NostrLink) {
|
||||||
const [allEvents, setAllEvents] = useState<Array<NostrLink>>([]);
|
const [allEvents, setAllEvents] = useState<Array<NostrLink>>([]);
|
||||||
@ -12,9 +12,10 @@ export default function useThreadFeed(link: NostrLink) {
|
|||||||
sub.withOptions({
|
sub.withOptions({
|
||||||
leaveOpen: true,
|
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 => {
|
allEvents.forEach(x => {
|
||||||
sub.withFilter().kinds([EventKind.TextNote]).link(x).replyToLink(x);
|
sub.withFilter().kinds([EventKind.TextNote]).replyToLink(x);
|
||||||
});
|
});
|
||||||
return sub;
|
return sub;
|
||||||
}, [allEvents.length]);
|
}, [allEvents.length]);
|
||||||
@ -23,8 +24,7 @@ export default function useThreadFeed(link: NostrLink) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (store.data) {
|
if (store.data) {
|
||||||
const mainNotes = store.data?.filter(a => a.kind === EventKind.TextNote || a.kind === EventKind.Polls) ?? [];
|
const links = store.data
|
||||||
const links = mainNotes
|
|
||||||
.map(a => [
|
.map(a => [
|
||||||
NostrLink.fromEvent(a),
|
NostrLink.fromEvent(a),
|
||||||
...a.tags.filter(a => a[0] === "e" || a[0] === "a").map(v => NostrLink.fromTag(v)),
|
...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 { useIntl } from "react-intl";
|
||||||
|
import { Nip46Signer, PinEncrypted } from "@snort/system";
|
||||||
|
|
||||||
import { EmailRegex, MnemonicRegex } from "Const";
|
import { EmailRegex, MnemonicRegex } from "Const";
|
||||||
import { LoginSessionType, LoginStore } from "Login";
|
import { LoginSessionType, LoginStore } from "Login";
|
||||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||||
import { getNip05PubKey } from "Pages/LoginPage";
|
import { getNip05PubKey } from "Pages/LoginPage";
|
||||||
import { bech32ToHex } from "SnortUtils";
|
import { bech32ToHex } from "SnortUtils";
|
||||||
import { Nip46Signer } from "@snort/system";
|
|
||||||
|
export class PinRequiredError extends Error { }
|
||||||
|
|
||||||
export default function useLoginHandler() {
|
export default function useLoginHandler() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||||
|
|
||||||
async function doLogin(key: string) {
|
async function doLogin(key: string, pin?: string) {
|
||||||
const insecureMsg = formatMessage({
|
const insecureMsg = formatMessage({
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
|
"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);
|
const hexKey = bech32ToHex(key);
|
||||||
if (hexKey.length === 64) {
|
if (hexKey.length === 64) {
|
||||||
LoginStore.loginWithPrivateKey(hexKey);
|
if (!pin) throw new PinRequiredError();
|
||||||
|
LoginStore.loginWithPrivateKey(PinEncrypted.create(hexKey, pin));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("INVALID PRIVATE KEY");
|
throw new Error("INVALID PRIVATE KEY");
|
||||||
}
|
}
|
||||||
@ -31,14 +34,16 @@ export default function useLoginHandler() {
|
|||||||
if (!hasSubtleCrypto) {
|
if (!hasSubtleCrypto) {
|
||||||
throw new Error(insecureMsg);
|
throw new Error(insecureMsg);
|
||||||
}
|
}
|
||||||
|
if (!pin) throw new PinRequiredError();
|
||||||
const ent = generateBip39Entropy(key);
|
const ent = generateBip39Entropy(key);
|
||||||
const keyHex = entropyToPrivateKey(ent);
|
const keyHex = entropyToPrivateKey(ent);
|
||||||
LoginStore.loginWithPrivateKey(keyHex);
|
LoginStore.loginWithPrivateKey(PinEncrypted.create(keyHex, pin));
|
||||||
} else if (key.length === 64) {
|
} else if (key.length === 64) {
|
||||||
if (!hasSubtleCrypto) {
|
if (!hasSubtleCrypto) {
|
||||||
throw new Error(insecureMsg);
|
throw new Error(insecureMsg);
|
||||||
}
|
}
|
||||||
LoginStore.loginWithPrivateKey(key);
|
if (!pin) throw new PinRequiredError();
|
||||||
|
LoginStore.loginWithPrivateKey(PinEncrypted.create(key, pin));
|
||||||
}
|
}
|
||||||
|
|
||||||
// public key logins
|
// public key logins
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { HexKey } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { setBlocked, setMuted } from "Login";
|
import { setBlocked, setMuted } from "Login";
|
||||||
import { appendDedupe } from "SnortUtils";
|
import { appendDedupe } from "SnortUtils";
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { useContext, useEffect, useMemo } from "react";
|
import { useContext, useEffect, useMemo } from "react";
|
||||||
import { NoopStore, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
import { NoopStore, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||||
import { unwrap } from "@snort/shared";
|
|
||||||
|
|
||||||
import { RefreshFeedCache } from "Cache/RefreshFeedCache";
|
import { RefreshFeedCache } from "Cache/RefreshFeedCache";
|
||||||
import useLogin from "./useLogin";
|
import useLogin from "./useLogin";
|
||||||
|
import useEventPublisher from "./useEventPublisher";
|
||||||
|
|
||||||
export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) {
|
export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
if (login) {
|
if (login.publicKey) {
|
||||||
const rb = new RequestBuilder(`using-${c.name}`);
|
const rb = new RequestBuilder(`using-${c.name}`);
|
||||||
rb.withOptions({
|
rb.withOptions({
|
||||||
leaveOpen,
|
leaveOpen,
|
||||||
@ -28,11 +29,11 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
|
|||||||
let t: ReturnType<typeof setTimeout> | undefined;
|
let t: ReturnType<typeof setTimeout> | undefined;
|
||||||
let tBuf: Array<TaggedNostrEvent> = [];
|
let tBuf: Array<TaggedNostrEvent> = [];
|
||||||
const releaseOnEvent = q.feed.onEvent(evs => {
|
const releaseOnEvent = q.feed.onEvent(evs => {
|
||||||
if (!t) {
|
if (!t && publisher) {
|
||||||
tBuf = [...evs];
|
tBuf = [...evs];
|
||||||
t = setTimeout(() => {
|
t = setTimeout(() => {
|
||||||
t = undefined;
|
t = undefined;
|
||||||
c.onEvent(tBuf, unwrap(login.publisher));
|
c.onEvent(tBuf, publisher);
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
tBuf.push(...evs);
|
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 { unixNowMs } from "@snort/shared";
|
||||||
import * as secp from "@noble/curves/secp256k1";
|
import * as secp from "@noble/curves/secp256k1";
|
||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils";
|
||||||
|
|
||||||
import { DefaultRelays, SnortPubKey } from "Const";
|
import { DefaultRelays, SnortPubKey } from "Const";
|
||||||
import { LoginStore, UserPreferences, LoginSession } from "Login";
|
import { LoginStore, UserPreferences, LoginSession, LoginSessionType } from "Login";
|
||||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unwrap } from "SnortUtils";
|
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unwrap } from "SnortUtils";
|
||||||
import { SubscriptionEvent } from "Subscription";
|
import { SubscriptionEvent } from "Subscription";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import { Chats, FollowsFeed, GiftsCache, Notifications } from "Cache";
|
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) {
|
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||||
if (state.relays.timestamp >= createdAt) {
|
if (state.relays.timestamp >= createdAt) {
|
||||||
@ -41,8 +43,8 @@ export function updatePreferences(state: LoginSession, p: UserPreferences) {
|
|||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout(k: HexKey) {
|
export function logout(id: string) {
|
||||||
LoginStore.removeSession(k);
|
LoginStore.removeSession(id);
|
||||||
GiftsCache.clear();
|
GiftsCache.clear();
|
||||||
Notifications.clear();
|
Notifications.clear();
|
||||||
FollowsFeed.clear();
|
FollowsFeed.clear();
|
||||||
@ -62,7 +64,7 @@ export function clearEntropy(state: LoginSession) {
|
|||||||
/**
|
/**
|
||||||
* Generate a new key and login with this generated key
|
* Generate a new key and login with this generated key
|
||||||
*/
|
*/
|
||||||
export async function generateNewLogin() {
|
export async function generateNewLogin(pin: string) {
|
||||||
const ent = generateBip39Entropy();
|
const ent = generateBip39Entropy();
|
||||||
const entropy = utils.bytesToHex(ent);
|
const entropy = utils.bytesToHex(ent);
|
||||||
const privateKey = entropyToPrivateKey(ent);
|
const privateKey = entropyToPrivateKey(ent);
|
||||||
@ -88,7 +90,9 @@ export async function generateNewLogin() {
|
|||||||
const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays);
|
const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays);
|
||||||
System.BroadcastEvent(ev);
|
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() {
|
export function generateRandomKey() {
|
||||||
@ -161,3 +165,38 @@ export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[
|
|||||||
LoginStore.updateSession(state);
|
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 { UserPreferences } from "Login";
|
||||||
import { SubscriptionEvent } from "Subscription";
|
import { SubscriptionEvent } from "Subscription";
|
||||||
|
|
||||||
@ -19,6 +19,11 @@ export enum LoginSessionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginSession {
|
export interface LoginSession {
|
||||||
|
/**
|
||||||
|
* Unique ID to identify this session
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type of login session
|
* Type of login session
|
||||||
*/
|
*/
|
||||||
@ -26,9 +31,15 @@ export interface LoginSession {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Current user private key
|
* Current user private key
|
||||||
|
* @deprecated Moving to pin encrypted storage
|
||||||
*/
|
*/
|
||||||
privateKey?: HexKey;
|
privateKey?: HexKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypted private key
|
||||||
|
*/
|
||||||
|
privateKeyData?: PinEncrypted | PinEncryptedPayload;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BIP39-generated, hex-encoded entropy
|
* BIP39-generated, hex-encoded entropy
|
||||||
*/
|
*/
|
||||||
@ -98,9 +109,4 @@ export interface LoginSession {
|
|||||||
* Remote signer relays (NIP-46)
|
* Remote signer relays (NIP-46)
|
||||||
*/
|
*/
|
||||||
remoteSignerRelays?: Array<string>;
|
remoteSignerRelays?: Array<string>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Instance event publisher
|
|
||||||
*/
|
|
||||||
publisher?: EventPublisher;
|
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import * as secp from "@noble/curves/secp256k1";
|
import * as secp from "@noble/curves/secp256k1";
|
||||||
import * as utils from "@noble/curves/abstract/utils";
|
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 { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
|
||||||
|
|
||||||
import { DefaultRelays } from "Const";
|
import { DefaultRelays } from "Const";
|
||||||
import { LoginSession, LoginSessionType } from "Login";
|
import { LoginSession, LoginSessionType, createPublisher } from "Login";
|
||||||
import { DefaultPreferences, UserPreferences } from "./Preferences";
|
import { DefaultPreferences } from "./Preferences";
|
||||||
import { Nip7OsSigner } from "./Nip7OsSigner";
|
|
||||||
|
|
||||||
const AccountStoreKey = "sessions";
|
const AccountStoreKey = "sessions";
|
||||||
const LoggedOut = {
|
const LoggedOut = {
|
||||||
|
id: "default",
|
||||||
type: "public_key",
|
type: "public_key",
|
||||||
preferences: DefaultPreferences,
|
preferences: DefaultPreferences,
|
||||||
tags: {
|
tags: {
|
||||||
@ -45,25 +46,18 @@ const LoggedOut = {
|
|||||||
readNotifications: 0,
|
readNotifications: 0,
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
} as LoginSession;
|
} 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> {
|
export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||||
#activeAccount?: HexKey;
|
#activeAccount?: HexKey;
|
||||||
#accounts: Map<string, LoginSession>;
|
#accounts: Map<string, LoginSession>;
|
||||||
|
#publishers = new Map<string, EventPublisher>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const existing = window.localStorage.getItem(AccountStoreKey);
|
const existing = window.localStorage.getItem(AccountStoreKey);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const logins = JSON.parse(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 {
|
} else {
|
||||||
this.#accounts = new Map();
|
this.#accounts = new Map();
|
||||||
}
|
}
|
||||||
@ -71,26 +65,32 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
if (!this.#activeAccount) {
|
if (!this.#activeAccount) {
|
||||||
this.#activeAccount = this.#accounts.keys().next().value;
|
this.#activeAccount = this.#accounts.keys().next().value;
|
||||||
}
|
}
|
||||||
for (const [, v] of this.#accounts) {
|
|
||||||
v.publisher = this.#createPublisher(v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSessions() {
|
getSessions() {
|
||||||
return [...this.#accounts.keys()];
|
return [...this.#accounts.values()].map(v => unwrap(v.publicKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
allSubscriptions() {
|
allSubscriptions() {
|
||||||
return [...this.#accounts.values()].map(a => a.subscriptions).flat();
|
return [...this.#accounts.values()].map(a => a.subscriptions).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
switchAccount(pk: string) {
|
switchAccount(id: string) {
|
||||||
if (this.#accounts.has(pk)) {
|
if (this.#accounts.has(id)) {
|
||||||
this.#activeAccount = pk;
|
this.#activeAccount = id;
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPublisher(id: string) {
|
||||||
|
return this.#publishers.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPublisher(id: string, pub: EventPublisher) {
|
||||||
|
this.#publishers.set(id, pub);
|
||||||
|
this.notifyChange();
|
||||||
|
}
|
||||||
|
|
||||||
loginWithPubkey(
|
loginWithPubkey(
|
||||||
key: HexKey,
|
key: HexKey,
|
||||||
type: LoginSessionType,
|
type: LoginSessionType,
|
||||||
@ -104,6 +104,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
const initRelays = this.decideInitRelays(relays);
|
const initRelays = this.decideInitRelays(relays);
|
||||||
const newSession = {
|
const newSession = {
|
||||||
...LoggedOut,
|
...LoggedOut,
|
||||||
|
id: uuid(),
|
||||||
type,
|
type,
|
||||||
publicKey: key,
|
publicKey: key,
|
||||||
relays: {
|
relays: {
|
||||||
@ -114,10 +115,13 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
remoteSignerRelays,
|
remoteSignerRelays,
|
||||||
privateKey,
|
privateKey,
|
||||||
} as LoginSession;
|
} as LoginSession;
|
||||||
newSession.publisher = this.#createPublisher(newSession);
|
|
||||||
|
|
||||||
this.#accounts.set(key, newSession);
|
const pub = createPublisher(newSession);
|
||||||
this.#activeAccount = key;
|
if(pub) {
|
||||||
|
this.setPublisher(newSession.id, pub);
|
||||||
|
}
|
||||||
|
this.#accounts.set(newSession.id, newSession);
|
||||||
|
this.#activeAccount = newSession.id;
|
||||||
this.#save();
|
this.#save();
|
||||||
return newSession;
|
return newSession;
|
||||||
}
|
}
|
||||||
@ -129,16 +133,17 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
return Object.fromEntries(DefaultRelays.entries());
|
return Object.fromEntries(DefaultRelays.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
loginWithPrivateKey(key: HexKey, entropy?: string, relays?: Record<string, RelaySettings>) {
|
loginWithPrivateKey(key: PinEncrypted, entropy?: string, relays?: Record<string, RelaySettings>) {
|
||||||
const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(key));
|
const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(key.value));
|
||||||
if (this.#accounts.has(pubKey)) {
|
if (this.#accounts.has(pubKey)) {
|
||||||
throw new Error("Already logged in with this pubkey");
|
throw new Error("Already logged in with this pubkey");
|
||||||
}
|
}
|
||||||
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
||||||
const newSession = {
|
const newSession = {
|
||||||
...LoggedOut,
|
...LoggedOut,
|
||||||
|
id: uuid(),
|
||||||
type: LoginSessionType.PrivateKey,
|
type: LoginSessionType.PrivateKey,
|
||||||
privateKey: key,
|
privateKeyData: key,
|
||||||
publicKey: pubKey,
|
publicKey: pubKey,
|
||||||
generatedEntropy: entropy,
|
generatedEntropy: entropy,
|
||||||
relays: {
|
relays: {
|
||||||
@ -149,30 +154,30 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
} as LoginSession;
|
} as LoginSession;
|
||||||
|
|
||||||
if ("nostr_os" in window && window.nostr_os) {
|
if ("nostr_os" in window && window.nostr_os) {
|
||||||
window.nostr_os.saveKey(key);
|
window.nostr_os.saveKey(key.value);
|
||||||
newSession.type = LoginSessionType.Nip7os;
|
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.#accounts.set(newSession.id, newSession);
|
||||||
this.#activeAccount = pubKey;
|
this.#activeAccount = newSession.id;
|
||||||
this.#save();
|
this.#save();
|
||||||
return newSession;
|
return newSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSession(s: LoginSession) {
|
updateSession(s: LoginSession) {
|
||||||
const pk = unwrap(s.publicKey);
|
if (this.#accounts.has(s.id)) {
|
||||||
if (this.#accounts.has(pk)) {
|
this.#accounts.set(s.id, s);
|
||||||
this.#accounts.set(pk, s);
|
|
||||||
console.debug("SET SESSION", s);
|
console.debug("SET SESSION", s);
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSession(k: string) {
|
removeSession(id: string) {
|
||||||
if (this.#accounts.delete(k)) {
|
if (this.#accounts.delete(id)) {
|
||||||
if (this.#activeAccount === k) {
|
if (this.#activeAccount === id) {
|
||||||
this.#activeAccount = undefined;
|
this.#activeAccount = undefined;
|
||||||
}
|
}
|
||||||
this.#save();
|
this.#save();
|
||||||
@ -183,65 +188,11 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
|
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
|
||||||
if (!s) return LoggedOut;
|
if (!s) return LoggedOut;
|
||||||
|
|
||||||
return { ...s };
|
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() {
|
#migrate() {
|
||||||
let didMigrate = false;
|
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
|
// replace default tab with notes
|
||||||
for (const [, v] of this.#accounts) {
|
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) {
|
if (didMigrate) {
|
||||||
console.debug("Finished migration to MultiAccountStore");
|
console.debug("Finished migration to MultiAccountStore");
|
||||||
this.#save();
|
this.#save();
|
||||||
@ -267,9 +226,16 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
|
|
||||||
#save() {
|
#save() {
|
||||||
if (!this.#activeAccount && this.#accounts.size > 0) {
|
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();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MultiAccountStore } from "./MultiAccountStore";
|
import { MultiAccountStore } from "./MultiAccountStore";
|
||||||
|
|
||||||
export const LoginStore = new MultiAccountStore();
|
export const LoginStore = new MultiAccountStore();
|
||||||
|
|
||||||
export interface Nip7os {
|
export interface Nip7os {
|
||||||
|
@ -70,7 +70,7 @@ export function SnortDeckLayout() {
|
|||||||
</div>
|
</div>
|
||||||
{deckScope.thread && (
|
{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}>
|
<ThreadContextWrapper link={deckScope.thread}>
|
||||||
<SpotlightFromThread onClose={() => deckScope.setThread(undefined)} />
|
<SpotlightFromThread onClose={() => deckScope.setThread(undefined)} />
|
||||||
<div>
|
<div>
|
||||||
|
@ -3,7 +3,7 @@ import { useParams } from "react-router-dom";
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import Timeline from "Element/Timeline";
|
import Timeline from "Element/Timeline";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { setTags } from "Login";
|
import { setTags } from "Login";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import "./Layout.css";
|
import "./Layout.css";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
@ -9,8 +8,6 @@ import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system";
|
|||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { RootState } from "State/Store";
|
|
||||||
import { setShow, reset } from "State/NoteCreator";
|
|
||||||
import useLoginFeed from "Feed/LoginFeed";
|
import useLoginFeed from "Feed/LoginFeed";
|
||||||
import { NoteCreator } from "Element/NoteCreator";
|
import { NoteCreator } from "Element/NoteCreator";
|
||||||
import { mapPlanName } from "./subscribe";
|
import { mapPlanName } from "./subscribe";
|
||||||
@ -23,29 +20,19 @@ import Spinner from "Icons/Spinner";
|
|||||||
import { fetchNip05Pubkey } from "Nip05/Verifier";
|
import { fetchNip05Pubkey } from "Nip05/Verifier";
|
||||||
import { useTheme } from "Hooks/useTheme";
|
import { useTheme } from "Hooks/useTheme";
|
||||||
import { useLoginRelays } from "Hooks/useLoginRelays";
|
import { useLoginRelays } from "Hooks/useLoginRelays";
|
||||||
|
import { useNoteCreator } from "State/NoteCreator";
|
||||||
|
import { LoginUnlock } from "Element/PinPrompt";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo);
|
const note = useNoteCreator();
|
||||||
const isNoteCreatorShowing = useSelector((s: RootState) => s.noteCreator.show);
|
const isReplyNoteCreatorShowing = note.replyTo && note.show;
|
||||||
const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { publicKey, subscriptions } = useLogin();
|
|
||||||
const currentSubscription = getCurrentSubscription(subscriptions);
|
|
||||||
const [pageClass, setPageClass] = useState("page");
|
const [pageClass, setPageClass] = useState("page");
|
||||||
|
|
||||||
useLoginFeed();
|
useLoginFeed();
|
||||||
useTheme();
|
useTheme();
|
||||||
useLoginRelays();
|
useLoginRelays();
|
||||||
|
|
||||||
const handleNoteCreatorButtonClick = () => {
|
|
||||||
if (replyTo) {
|
|
||||||
dispatch(reset());
|
|
||||||
}
|
|
||||||
dispatch(setShow(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldHideNoteCreator = useMemo(() => {
|
const shouldHideNoteCreator = useMemo(() => {
|
||||||
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
|
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/e", "/subscribe"];
|
||||||
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
||||||
@ -66,34 +53,22 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (<>
|
||||||
<div className={pageClass}>
|
<div className={pageClass}>
|
||||||
{!shouldHideHeader && (
|
{!shouldHideHeader && (
|
||||||
<header className="main-content">
|
<header className="main-content">
|
||||||
<Link to="/" className="logo">
|
<LogoHeader />
|
||||||
<h1>Snort</h1>
|
<AccountHeader />
|
||||||
{currentSubscription && (
|
|
||||||
<small className="flex">
|
|
||||||
<Icon name="diamond" size={10} className="mr5" />
|
|
||||||
{mapPlanName(currentSubscription.type)}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{publicKey ? (
|
|
||||||
<AccountHeader />
|
|
||||||
) : (
|
|
||||||
<button type="button" onClick={() => navigate("/login")}>
|
|
||||||
<FormattedMessage {...messages.Login} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
||||||
{!shouldHideNoteCreator && (
|
{!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} />
|
<Icon name="plus" size={16} />
|
||||||
</button>
|
</button>
|
||||||
<NoteCreator />
|
<NoteCreator />
|
||||||
@ -101,6 +76,8 @@ export default function Layout() {
|
|||||||
)}
|
)}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
|
<LoginUnlock />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +133,13 @@ const AccountHeader = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!publicKey) {
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={() => navigate("/login")}>
|
||||||
|
<FormattedMessage {...messages.Login} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
{!location.pathname.startsWith("/search") && (
|
{!location.pathname.startsWith("/search") && (
|
||||||
@ -199,3 +183,20 @@ const AccountHeader = () => {
|
|||||||
</div>
|
</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 useLogin from "Hooks/useLogin";
|
||||||
import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
|
import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import useLoginHandler from "Hooks/useLoginHandler";
|
import useLoginHandler, { PinRequiredError } from "Hooks/useLoginHandler";
|
||||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||||
import { bytesToHex } from "@noble/curves/abstract/utils";
|
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import QrCode from "Element/QrCode";
|
import QrCode from "Element/QrCode";
|
||||||
import Copy from "Element/Copy";
|
import Copy from "Element/Copy";
|
||||||
import { delay } from "SnortUtils";
|
import { delay } from "SnortUtils";
|
||||||
|
import { PinPrompt } from "Element/PinPrompt";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -78,6 +79,7 @@ export default function LoginPage() {
|
|||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [key, setKey] = useState("");
|
const [key, setKey] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [pin, setPin] = useState(false);
|
||||||
const [art, setArt] = useState<ArtworkEntry>();
|
const [art, setArt] = useState<ArtworkEntry>();
|
||||||
const [isMasking, setMasking] = useState(true);
|
const [isMasking, setMasking] = useState(true);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -99,10 +101,13 @@ export default function LoginPage() {
|
|||||||
setArt({ ...ret, link: url });
|
setArt({ ...ret, link: url });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function doLogin() {
|
async function doLogin(pin?: string) {
|
||||||
try {
|
try {
|
||||||
await loginHandler.doLogin(key);
|
await loginHandler.doLogin(key, pin);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e instanceof PinRequiredError) {
|
||||||
|
setPin(true);
|
||||||
|
}
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
} else {
|
} else {
|
||||||
@ -116,10 +121,16 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeRandomKey() {
|
async function makeRandomKey(pin: string) {
|
||||||
await generateNewLogin();
|
try {
|
||||||
window.plausible?.("Generate Account");
|
await generateNewLogin(pin);
|
||||||
navigate("/new");
|
window.plausible?.("Generate Account");
|
||||||
|
navigate("/new");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
setError(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doNip07Login() {
|
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" />
|
<FormattedMessage defaultMessage="Nostr Connect (NIP-46)" description="Login button for NIP-46 signer app" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
{nostrConnect && (
|
{nostrConnect && (
|
||||||
<Modal onClose={() => setNostrConnect("")}>
|
<Modal id="nostr-connect" onClose={() => setNostrConnect("")}>
|
||||||
<div className="flex f-col">
|
<div className="flex f-col">
|
||||||
<QrCode data={nostrConnect} />
|
<QrCode data={nostrConnect} />
|
||||||
<Copy text={nostrConnect} />
|
<Copy text={nostrConnect} />
|
||||||
@ -283,12 +294,19 @@ export default function LoginPage() {
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<div dir="auto" className="login-actions">
|
<div dir="auto" className="login-actions">
|
||||||
<AsyncButton type="button" onClick={doLogin}>
|
<AsyncButton type="button" onClick={() => doLogin()}>
|
||||||
<FormattedMessage defaultMessage="Login" description="Login button" />
|
<FormattedMessage defaultMessage="Login" description="Login button" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
<AsyncButton onClick={() => makeRandomKey()}>
|
<AsyncButton onClick={() => setPin(true)}>
|
||||||
<FormattedMessage defaultMessage="Create Account" />
|
<FormattedMessage defaultMessage="Create Account" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
{pin && <PinPrompt onResult={pin => {
|
||||||
|
if (key) {
|
||||||
|
doLogin(pin);
|
||||||
|
} else {
|
||||||
|
makeRandomKey(pin);
|
||||||
|
}
|
||||||
|
}} onCancel={() => setPin(false)} />}
|
||||||
{altLogins()}
|
{altLogins()}
|
||||||
</div>
|
</div>
|
||||||
{installExtension()}
|
{installExtension()}
|
||||||
|
@ -210,7 +210,7 @@ function NewChatWindow() {
|
|||||||
<Icon name="plus" size={16} />
|
<Icon name="plus" size={16} />
|
||||||
</button>
|
</button>
|
||||||
{show && (
|
{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-column g16">
|
||||||
<div className="flex f-space">
|
<div className="flex f-space">
|
||||||
<h2>
|
<h2>
|
||||||
|
@ -326,14 +326,14 @@ export default function ProfilePage() {
|
|||||||
targets={
|
targets={
|
||||||
lnurl?.lnurl && id
|
lnurl?.lnurl && id
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
type: "lnurl",
|
type: "lnurl",
|
||||||
value: lnurl?.lnurl,
|
value: lnurl?.lnurl,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
name: user?.display_name || user?.name,
|
name: user?.display_name || user?.name,
|
||||||
zap: { pubkey: id },
|
zap: { pubkey: id },
|
||||||
} as ZapTarget,
|
} as ZapTarget,
|
||||||
]
|
]
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
show={showLnQr}
|
show={showLnQr}
|
||||||
@ -447,7 +447,7 @@ export default function ProfilePage() {
|
|||||||
<Icon name="qr" size={16} />
|
<Icon name="qr" size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{showProfileQr && (
|
{showProfileQr && (
|
||||||
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
<Modal id="profile-qr" className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||||
<ProfileImage pubkey={id} />
|
<ProfileImage pubkey={id} />
|
||||||
<QrCode data={link} className="m10 align-center" />
|
<QrCode data={link} className="m10 align-center" />
|
||||||
<Copy text={link} className="align-center" />
|
<Copy text={link} className="align-center" />
|
||||||
|
@ -5,7 +5,7 @@ import { mapEventToProfile } from "@snort/system";
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
|
||||||
import Logo from "Element/Logo";
|
import Logo from "Element/Logo";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { UserCache } from "Cache";
|
import { UserCache } from "Cache";
|
||||||
import AvatarEditor from "Element/AvatarEditor";
|
import AvatarEditor from "Element/AvatarEditor";
|
||||||
|
@ -6,7 +6,7 @@ import { mapEventToProfile } from "@snort/system";
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { openFile } from "SnortUtils";
|
import { openFile } from "SnortUtils";
|
||||||
import useFileUpload from "Upload";
|
import useFileUpload from "Upload";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
|
@ -4,7 +4,7 @@ import { unixNowMs } from "@snort/shared";
|
|||||||
|
|
||||||
import { randomSample } from "SnortUtils";
|
import { randomSample } from "SnortUtils";
|
||||||
import Relay from "Element/Relay";
|
import Relay from "Element/Relay";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { setRelays } from "Login";
|
import { setRelays } from "Login";
|
||||||
|
@ -5,7 +5,6 @@ import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
|||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { LoginStore, logout } from "Login";
|
import { LoginStore, logout } from "Login";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { unwrap } from "SnortUtils";
|
|
||||||
import { getCurrentSubscription } from "Subscription";
|
import { getCurrentSubscription } from "Subscription";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
@ -19,7 +18,7 @@ const SettingsIndex = () => {
|
|||||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
logout(unwrap(login.publicKey));
|
logout(login.id);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { LNURL } from "@snort/shared";
|
|||||||
|
|
||||||
import { ApiHost } from "Const";
|
import { ApiHost } from "Const";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
|
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
|
||||||
|
|
||||||
export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
|
export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
|
||||||
|
@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { ApiHost } from "Const";
|
import { ApiHost } from "Const";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
|
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
|
||||||
|
|
||||||
export default function ListHandles() {
|
export default function ListHandles() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ApiHost } from "Const";
|
import { ApiHost } from "Const";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { ServiceError } from "Nip05/ServiceProvider";
|
import { ServiceError } from "Nip05/ServiceProvider";
|
||||||
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
|
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import PageSpinner from "Element/PageSpinner";
|
import PageSpinner from "Element/PageSpinner";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
||||||
import { mapSubscriptionErrorCode } from ".";
|
import { mapSubscriptionErrorCode } from ".";
|
||||||
import SubscriptionCard from "./SubscriptionCard";
|
import SubscriptionCard from "./SubscriptionCard";
|
||||||
|
@ -5,7 +5,7 @@ import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
|||||||
import { mapPlanName, mapSubscriptionErrorCode } from ".";
|
import { mapPlanName, mapSubscriptionErrorCode } from ".";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import Nip5Service from "Element/Nip5Service";
|
import Nip5Service from "Element/Nip5Service";
|
||||||
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
|
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
|
||||||
|
@ -8,7 +8,7 @@ import { formatShort } from "Number";
|
|||||||
import { LockedFeatures, Plans, SubscriptionType } from "Subscription";
|
import { LockedFeatures, Plans, SubscriptionType } from "Subscription";
|
||||||
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
|
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "SnortApi";
|
import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "SnortApi";
|
||||||
import SendSats from "Element/SendSats";
|
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 { StrictMode } from "react";
|
||||||
import * as ReactDOM from "react-dom/client";
|
import * as ReactDOM from "react-dom/client";
|
||||||
import { Provider } from "react-redux";
|
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
EventPublisher,
|
EventPublisher,
|
||||||
@ -24,7 +23,6 @@ import { SnortContext } from "@snort/system-react";
|
|||||||
import * as serviceWorkerRegistration from "serviceWorkerRegistration";
|
import * as serviceWorkerRegistration from "serviceWorkerRegistration";
|
||||||
import { IntlProvider } from "IntlProvider";
|
import { IntlProvider } from "IntlProvider";
|
||||||
import { unwrap } from "SnortUtils";
|
import { unwrap } from "SnortUtils";
|
||||||
import Store from "State/Store";
|
|
||||||
import Layout from "Pages/Layout";
|
import Layout from "Pages/Layout";
|
||||||
import LoginPage from "Pages/LoginPage";
|
import LoginPage from "Pages/LoginPage";
|
||||||
import ProfilePage from "Pages/ProfilePage";
|
import ProfilePage from "Pages/ProfilePage";
|
||||||
@ -218,12 +216,10 @@ export const router = createBrowserRouter([
|
|||||||
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
|
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Provider store={Store}>
|
<IntlProvider>
|
||||||
<IntlProvider>
|
<SnortContext.Provider value={System}>
|
||||||
<SnortContext.Provider value={System}>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
</SnortContext.Provider>
|
||||||
</SnortContext.Provider>
|
</IntlProvider>
|
||||||
</IntlProvider>
|
|
||||||
</Provider>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ export interface HookFilter<TSnapshot> {
|
|||||||
*/
|
*/
|
||||||
export abstract class ExternalStore<TSnapshot> {
|
export abstract class ExternalStore<TSnapshot> {
|
||||||
#hooks: Array<HookFilter<TSnapshot>> = [];
|
#hooks: Array<HookFilter<TSnapshot>> = [];
|
||||||
#snapshot: Readonly<TSnapshot> = {} as Readonly<TSnapshot>;
|
#snapshot: TSnapshot = {} as TSnapshot;
|
||||||
#changed = true;
|
#changed = true;
|
||||||
|
|
||||||
hook(fn: HookFn<TSnapshot>) {
|
hook(fn: HookFn<TSnapshot>) {
|
||||||
@ -35,7 +35,9 @@ export abstract class ExternalStore<TSnapshot> {
|
|||||||
protected notifyChange(sn?: TSnapshot) {
|
protected notifyChange(sn?: TSnapshot) {
|
||||||
this.#changed = true;
|
this.#changed = true;
|
||||||
if (this.#hooks.length > 0) {
|
if (this.#hooks.length > 0) {
|
||||||
this.#hooks.forEach(h => h.fn(sn));
|
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);
|
const thread = EventExt.extractThread(replyTo);
|
||||||
if (thread) {
|
if (thread) {
|
||||||
if (thread.root || thread.replyTo) {
|
const rootOrReplyAsRoot = thread.root || thread.replyTo;
|
||||||
eb.tag(["e", thread.root?.value ?? thread.replyTo?.value ?? "", "", "root"]);
|
if (rootOrReplyAsRoot) {
|
||||||
|
eb.tag(["e", rootOrReplyAsRoot?.value ?? "", rootOrReplyAsRoot?.relay ?? "", "root"]);
|
||||||
}
|
}
|
||||||
eb.tag(["e", replyTo.id, replyTo.relays?.[0] ?? "", "reply"]);
|
eb.tag(["e", replyTo.id, replyTo.relays?.[0] ?? "", "reply"]);
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { MessageEncryptor, MessageEncryptorPayload, MessageEncryptorVersion } from "index";
|
import { MessageEncryptor, MessageEncryptorPayload, MessageEncryptorVersion } from "index";
|
||||||
|
|
||||||
import { base64 } from "@scure/base";
|
|
||||||
import { randomBytes } from "@noble/hashes/utils";
|
import { randomBytes } from "@noble/hashes/utils";
|
||||||
import { streamXOR as xchacha20 } from "@stablelib/xchacha20";
|
import { streamXOR as xchacha20 } from "@stablelib/xchacha20";
|
||||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||||
|
@ -27,6 +27,7 @@ export * from "./text";
|
|||||||
export * from "./pow";
|
export * from "./pow";
|
||||||
export * from "./pow-util";
|
export * from "./pow-util";
|
||||||
export * from "./query-optimizer";
|
export * from "./query-optimizer";
|
||||||
|
export * from "./encrypted";
|
||||||
|
|
||||||
export * from "./impl/nip4";
|
export * from "./impl/nip4";
|
||||||
export * from "./impl/nip44";
|
export * from "./impl/nip44";
|
||||||
|
@ -300,7 +300,7 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
|||||||
{
|
{
|
||||||
filters: [{ ...f, ids: [...resultIds] }],
|
filters: [{ ...f, ids: [...resultIds] }],
|
||||||
strategy: RequestStrategy.ExplicitRelays,
|
strategy: RequestStrategy.ExplicitRelays,
|
||||||
relay: "",
|
relay: qSend.relay,
|
||||||
},
|
},
|
||||||
cacheResults as Array<TaggedNostrEvent>,
|
cacheResults as Array<TaggedNostrEvent>,
|
||||||
);
|
);
|
||||||
|
@ -202,7 +202,7 @@ export class Query implements QueryBase {
|
|||||||
*/
|
*/
|
||||||
insertCompletedTrace(subq: BuiltRawReqFilter, data: Readonly<Array<TaggedNostrEvent>>) {
|
insertCompletedTrace(subq: BuiltRawReqFilter, data: Readonly<Array<TaggedNostrEvent>>) {
|
||||||
const qt = new QueryTrace(
|
const qt = new QueryTrace(
|
||||||
"",
|
subq.relay,
|
||||||
subq.filters,
|
subq.filters,
|
||||||
"",
|
"",
|
||||||
() => {
|
() => {
|
||||||
|
@ -113,7 +113,7 @@ export class RequestBuilder {
|
|||||||
|
|
||||||
const diff = system.QueryOptimizer.getDiff(prev, this.buildRaw());
|
const diff = system.QueryOptimizer.getDiff(prev, this.buildRaw());
|
||||||
const ts = unixNowMs() - start;
|
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) {
|
if (diff.length > 0) {
|
||||||
return splitFlatByWriteRelays(system.RelayCache, diff).map(a => {
|
return splitFlatByWriteRelays(system.RelayCache, diff).map(a => {
|
||||||
return {
|
return {
|
||||||
|
@ -6,19 +6,8 @@ import { ReqFilter } from "nostr";
|
|||||||
export function trimFilters(filters: Array<ReqFilter>) {
|
export function trimFilters(filters: Array<ReqFilter>) {
|
||||||
const fNew = [];
|
const fNew = [];
|
||||||
for (const f of filters) {
|
for (const f of filters) {
|
||||||
let arrays = 0;
|
const ent = Object.entries(f).filter(([,v]) => Array.isArray(v));
|
||||||
for (const [k, v] of Object.entries(f)) {
|
if(ent.every(([,v]) => (v as Array<string | number>).length > 0)) {
|
||||||
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) {
|
|
||||||
fNew.push(f);
|
fNew.push(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
104
yarn.lock
104
yarn.lock
@ -1376,7 +1376,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 7.22.11
|
||||||
resolution: "@babel/runtime@npm:7.22.11"
|
resolution: "@babel/runtime@npm:7.22.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2507,26 +2507,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@remix-run/router@npm:1.8.0":
|
||||||
version: 1.8.0
|
version: 1.8.0
|
||||||
resolution: "@remix-run/router@npm:1.8.0"
|
resolution: "@remix-run/router@npm:1.8.0"
|
||||||
@ -2705,7 +2685,6 @@ __metadata:
|
|||||||
"@lightninglabs/lnc-web": ^0.2.3-alpha
|
"@lightninglabs/lnc-web": ^0.2.3-alpha
|
||||||
"@noble/curves": ^1.0.0
|
"@noble/curves": ^1.0.0
|
||||||
"@noble/hashes": ^1.2.0
|
"@noble/hashes": ^1.2.0
|
||||||
"@reduxjs/toolkit": ^1.9.1
|
|
||||||
"@scure/base": ^1.1.1
|
"@scure/base": ^1.1.1
|
||||||
"@scure/bip32": ^1.3.0
|
"@scure/bip32": ^1.3.0
|
||||||
"@scure/bip39": ^1.1.1
|
"@scure/bip39": ^1.1.1
|
||||||
@ -2751,7 +2730,6 @@ __metadata:
|
|||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
react-intersection-observer: ^9.4.1
|
react-intersection-observer: ^9.4.1
|
||||||
react-intl: ^6.4.4
|
react-intl: ^6.4.4
|
||||||
react-redux: ^8.0.5
|
|
||||||
react-router-dom: ^6.5.0
|
react-router-dom: ^6.5.0
|
||||||
react-textarea-autosize: ^8.4.0
|
react-textarea-autosize: ^8.4.0
|
||||||
react-twitter-embed: ^4.0.4
|
react-twitter-embed: ^4.0.4
|
||||||
@ -3590,13 +3568,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/uuid@npm:^9.0.2":
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
resolution: "@types/uuid@npm:9.0.2"
|
resolution: "@types/uuid@npm:9.0.2"
|
||||||
@ -7614,13 +7585,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"import-fresh@npm:^3.2.1":
|
||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
resolution: "import-fresh@npm:3.3.0"
|
resolution: "import-fresh@npm:3.3.0"
|
||||||
@ -11273,38 +11237,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-router-dom@npm:^6.5.0":
|
||||||
version: 6.15.0
|
version: 6.15.0
|
||||||
resolution: "react-router-dom@npm:6.15.0"
|
resolution: "react-router-dom@npm:6.15.0"
|
||||||
@ -11522,24 +11454,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"regenerate-unicode-properties@npm:^10.1.0":
|
||||||
version: 10.1.0
|
version: 10.1.0
|
||||||
resolution: "regenerate-unicode-properties@npm:10.1.0"
|
resolution: "regenerate-unicode-properties@npm:10.1.0"
|
||||||
@ -11670,13 +11584,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"resolve-cwd@npm:^3.0.0":
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
resolution: "resolve-cwd@npm:3.0.0"
|
resolution: "resolve-cwd@npm:3.0.0"
|
||||||
@ -13424,15 +13331,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
resolution: "util-deprecate@npm:1.0.2"
|
resolution: "util-deprecate@npm:1.0.2"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user