Cleanup
This commit is contained in:
parent
8244441929
commit
96d4e4bcc5
@ -9,9 +9,11 @@ export interface ModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal(props: ModalProps) {
|
export default function Modal(props: ModalProps) {
|
||||||
return <div className={`modal${props.className ? ` ${props.className}` : ""}`} onClick={props.onClose}>
|
return (
|
||||||
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
<div className={`modal${props.className ? ` ${props.className}` : ""}`} onClick={props.onClose}>
|
||||||
{props.children}
|
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import "./NoteCreator.css";
|
import "./NoteCreator.css";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink, NostrEvent } from "@snort/system";
|
import {
|
||||||
|
EventKind,
|
||||||
|
NostrPrefix,
|
||||||
|
TaggedNostrEvent,
|
||||||
|
EventBuilder,
|
||||||
|
tryParseNostrLink,
|
||||||
|
NostrLink,
|
||||||
|
NostrEvent,
|
||||||
|
} from "@snort/system";
|
||||||
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
@ -30,7 +38,7 @@ export function NoteCreator() {
|
|||||||
|
|
||||||
async function buildNote() {
|
async function buildNote() {
|
||||||
try {
|
try {
|
||||||
note.update(v => v.error = "");
|
note.update(v => (v.error = ""));
|
||||||
if (note && publisher) {
|
if (note && publisher) {
|
||||||
let extraTags: Array<Array<string>> | undefined;
|
let extraTags: Array<Array<string>> | undefined;
|
||||||
if (note.zapSplits) {
|
if (note.zapSplits) {
|
||||||
@ -99,7 +107,9 @@ export function NoteCreator() {
|
|||||||
eb.kind(kind);
|
eb.kind(kind);
|
||||||
return eb;
|
return eb;
|
||||||
};
|
};
|
||||||
const ev = note.replyTo ? await publisher.reply(note.replyTo, note.note, hk) : await publisher.note(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) {
|
||||||
@ -131,7 +141,7 @@ export function NoteCreator() {
|
|||||||
note.update(v => {
|
note.update(v => {
|
||||||
v.reset();
|
v.reset();
|
||||||
v.show = false;
|
v.show = false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +191,7 @@ export function NoteCreator() {
|
|||||||
|
|
||||||
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
const { value } = ev.target;
|
const { value } = ev.target;
|
||||||
note.update(n => n.note = value);
|
note.update(n => (n.note = value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
@ -198,10 +208,10 @@ export function NoteCreator() {
|
|||||||
|
|
||||||
async function loadPreview() {
|
async function loadPreview() {
|
||||||
if (note.preview) {
|
if (note.preview) {
|
||||||
note.update(v => v.preview = undefined);
|
note.update(v => (v.preview = undefined));
|
||||||
} else if (publisher) {
|
} else if (publisher) {
|
||||||
const tmpNote = await buildNote();
|
const tmpNote = await buildNote();
|
||||||
note.update(v => v.preview = tmpNote);
|
note.update(v => (v.preview = tmpNote));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +254,7 @@ export function NoteCreator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button onClick={() => note.update(v => v.pollOptions = [...(note.pollOptions ?? []), ""])}>
|
<button onClick={() => note.update(v => (v.pollOptions = [...(note.pollOptions ?? []), ""]))}>
|
||||||
<Icon name="plus" size={14} />
|
<Icon name="plus" size={14} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@ -256,7 +266,7 @@ export function NoteCreator() {
|
|||||||
if (note.pollOptions) {
|
if (note.pollOptions) {
|
||||||
const copy = [...note.pollOptions];
|
const copy = [...note.pollOptions];
|
||||||
copy[i] = v;
|
copy[i] = v;
|
||||||
note.update(v => v.pollOptions = copy);
|
note.update(v => (v.pollOptions = copy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,7 +274,7 @@ export function NoteCreator() {
|
|||||||
if (note.pollOptions) {
|
if (note.pollOptions) {
|
||||||
const copy = [...note.pollOptions];
|
const copy = [...note.pollOptions];
|
||||||
copy.splice(i, 1);
|
copy.splice(i, 1);
|
||||||
note.update(v => v.pollOptions = copy);
|
note.update(v => (v.pollOptions = copy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,15 +291,22 @@ export function NoteCreator() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!note.selectedCustomRelays || note.selectedCustomRelays.includes(r)}
|
checked={!note.selectedCustomRelays || note.selectedCustomRelays.includes(r)}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
note.update(v => v.selectedCustomRelays = (
|
note.update(
|
||||||
// set false if all relays selected
|
v =>
|
||||||
e.target.checked && note.selectedCustomRelays && note.selectedCustomRelays.length == a.length - 1
|
(v.selectedCustomRelays =
|
||||||
? undefined
|
// set false if all relays selected
|
||||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
e.target.checked &&
|
||||||
a.filter(el => el === r ? e.target.checked : !note.selectedCustomRelays || note.selectedCustomRelays.includes(el))
|
note.selectedCustomRelays &&
|
||||||
));
|
note.selectedCustomRelays.length == a.length - 1
|
||||||
}
|
? undefined
|
||||||
}
|
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||||
|
a.filter(el =>
|
||||||
|
el === r
|
||||||
|
? e.target.checked
|
||||||
|
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -330,146 +347,166 @@ export function NoteCreator() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!note.show) return null;
|
if (!note.show) return null;
|
||||||
return (<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => v.show = false)}>
|
return (
|
||||||
{note.replyTo && (
|
<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => (v.show = false))}>
|
||||||
<Note
|
{note.replyTo && (
|
||||||
data={note.replyTo}
|
<Note
|
||||||
related={[]}
|
data={note.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 && (
|
|
||||||
<div onPaste={handlePaste} className={`note-creator${note.pollOptions ? " poll" : ""}`}>
|
|
||||||
<Textarea
|
|
||||||
autoFocus
|
|
||||||
className={`textarea ${note.active ? "textarea--focused" : ""}`}
|
|
||||||
onChange={c => onChange(c)}
|
|
||||||
value={note.note}
|
|
||||||
onFocus={() => note.update(v => v.active = true)}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === "Enter" && e.metaKey) {
|
|
||||||
sendNote().catch(console.warn);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{renderPollOptions()}
|
)}
|
||||||
</div>
|
{note.preview && getPreviewNote()}
|
||||||
)}
|
{!note.preview && (
|
||||||
<div className="flex f-space">
|
<div onPaste={handlePaste} className={`note-creator${note.pollOptions ? " poll" : ""}`}>
|
||||||
<div className="flex g8">
|
<Textarea
|
||||||
<ProfileImage pubkey={login.publicKey ?? ""} className="note-creator-icon" link="" showUsername={false} showFollowingMark={false} />
|
autoFocus
|
||||||
{note.pollOptions === undefined && !note.replyTo && (
|
className={`textarea ${note.active ? "textarea--focused" : ""}`}
|
||||||
<div className="note-creator-icon">
|
onChange={c => onChange(c)}
|
||||||
<Icon name="pie-chart" onClick={() => note.update(v => v.pollOptions = ["A", "B"])} size={24} />
|
value={note.note}
|
||||||
</div>
|
onFocus={() => note.update(v => (v.active = true))}
|
||||||
)}
|
onKeyDown={e => {
|
||||||
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
|
if (e.key === "Enter" && e.metaKey) {
|
||||||
<button className="secondary" onClick={() => note.update(v => v.advanced = !v.advanced)}>
|
sendNote().catch(console.warn);
|
||||||
<FormattedMessage defaultMessage="Advanced" />
|
}
|
||||||
</button>
|
}}
|
||||||
</div>
|
|
||||||
<div className="flex g8">
|
|
||||||
<button className="secondary" onClick={cancel}>
|
|
||||||
<FormattedMessage defaultMessage="Cancel" />
|
|
||||||
</button>
|
|
||||||
<AsyncButton onClick={onSubmit}>
|
|
||||||
{note.replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{note.error && <span className="error">{note.error}</span>}
|
|
||||||
{note.advanced && (
|
|
||||||
<>
|
|
||||||
<button className="secondary" onClick={loadPreview}>
|
|
||||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage defaultMessage="Custom Relays" />
|
|
||||||
</h4>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
|
||||||
</p>
|
|
||||||
{renderRelayCustomisation()}
|
|
||||||
</div>
|
|
||||||
<div className="flex-column g8">
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage defaultMessage="Zap Splits" />
|
|
||||||
</h4>
|
|
||||||
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
|
||||||
<div className="flex-column g8">
|
|
||||||
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
|
|
||||||
<div className="flex f-center g8">
|
|
||||||
<div className="flex-column f-4 g4">
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage defaultMessage="Recipient" />
|
|
||||||
</h4>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={v.value}
|
|
||||||
onChange={e => note.update(v => v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv)))}
|
|
||||||
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-column f-1 g4">
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage defaultMessage="Weight" />
|
|
||||||
</h4>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
value={v.weight}
|
|
||||||
onChange={e => note.update(v => v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, weight: Number(e.target.value) } : vv)))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-column f-shrink g4">
|
|
||||||
<div> </div>
|
|
||||||
<Icon
|
|
||||||
name="close"
|
|
||||||
onClick={() => note.update(v => v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => note.update(v => v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }])}>
|
|
||||||
<FormattedMessage defaultMessage="Add" />
|
|
||||||
</button>
|
|
||||||
</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 className="flex-column g8">
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage defaultMessage="Sensitive Content" />
|
|
||||||
</h4>
|
|
||||||
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
|
|
||||||
<input
|
|
||||||
className="w-max"
|
|
||||||
type="text"
|
|
||||||
value={note.sensitive}
|
|
||||||
onChange={e => note.update(v => v.sensitive = e.target.value)}
|
|
||||||
maxLength={50}
|
|
||||||
minLength={1}
|
|
||||||
placeholder={formatMessage({
|
|
||||||
defaultMessage: "Reason",
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<span className="warning">
|
{renderPollOptions()}
|
||||||
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
<div className="flex f-space">
|
||||||
</Modal>
|
<div className="flex g8">
|
||||||
|
<ProfileImage
|
||||||
|
pubkey={login.publicKey ?? ""}
|
||||||
|
className="note-creator-icon"
|
||||||
|
link=""
|
||||||
|
showUsername={false}
|
||||||
|
showFollowingMark={false}
|
||||||
|
/>
|
||||||
|
{note.pollOptions === undefined && !note.replyTo && (
|
||||||
|
<div className="note-creator-icon">
|
||||||
|
<Icon name="pie-chart" onClick={() => note.update(v => (v.pollOptions = ["A", "B"]))} size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
|
||||||
|
<button className="secondary" onClick={() => note.update(v => (v.advanced = !v.advanced))}>
|
||||||
|
<FormattedMessage defaultMessage="Advanced" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex g8">
|
||||||
|
<button className="secondary" onClick={cancel}>
|
||||||
|
<FormattedMessage defaultMessage="Cancel" />
|
||||||
|
</button>
|
||||||
|
<AsyncButton onClick={onSubmit}>
|
||||||
|
{note.replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{note.error && <span className="error">{note.error}</span>}
|
||||||
|
{note.advanced && (
|
||||||
|
<>
|
||||||
|
<button className="secondary" onClick={loadPreview}>
|
||||||
|
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Custom Relays" />
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
||||||
|
</p>
|
||||||
|
{renderRelayCustomisation()}
|
||||||
|
</div>
|
||||||
|
<div className="flex-column g8">
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Zap Splits" />
|
||||||
|
</h4>
|
||||||
|
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
||||||
|
<div className="flex-column g8">
|
||||||
|
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
|
||||||
|
<div className="flex f-center g8">
|
||||||
|
<div className="flex-column f-4 g4">
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Recipient" />
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={v.value}
|
||||||
|
onChange={e =>
|
||||||
|
note.update(
|
||||||
|
v => (v.zapSplits = arr.map((vv, ii) => (ii === i ? { ...vv, value: e.target.value } : vv))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address" })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-column f-1 g4">
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Weight" />
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={v.weight}
|
||||||
|
onChange={e =>
|
||||||
|
note.update(
|
||||||
|
v =>
|
||||||
|
(v.zapSplits = arr.map((vv, ii) =>
|
||||||
|
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-column f-shrink g4">
|
||||||
|
<div> </div>
|
||||||
|
<Icon
|
||||||
|
name="close"
|
||||||
|
onClick={() => note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
||||||
|
}>
|
||||||
|
<FormattedMessage defaultMessage="Add" />
|
||||||
|
</button>
|
||||||
|
</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 className="flex-column g8">
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Sensitive Content" />
|
||||||
|
</h4>
|
||||||
|
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
|
||||||
|
<input
|
||||||
|
className="w-max"
|
||||||
|
type="text"
|
||||||
|
value={note.sensitive}
|
||||||
|
onChange={e => note.update(v => (v.sensitive = e.target.value))}
|
||||||
|
maxLength={50}
|
||||||
|
minLength={1}
|
||||||
|
placeholder={formatMessage({
|
||||||
|
defaultMessage: "Reason",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<span className="warning">
|
||||||
|
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.pin-box {
|
.pin-box {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
font-size: 80px;
|
font-size: 80px;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
@ -11,120 +11,145 @@ import Modal from "./Modal";
|
|||||||
import Spinner from "Icons/Spinner";
|
import Spinner from "Icons/Spinner";
|
||||||
|
|
||||||
const PinLen = 6;
|
const PinLen = 6;
|
||||||
export function PinPrompt({ onResult, onCancel, subTitle }: { onResult: (v: string) => Promise<void>, onCancel: () => void, subTitle?: ReactNode }) {
|
export function PinPrompt({
|
||||||
const [pin, setPin] = useState("");
|
onResult,
|
||||||
const [error, setError] = useState("");
|
onCancel,
|
||||||
const { formatMessage } = useIntl();
|
subTitle,
|
||||||
|
}: {
|
||||||
|
onResult: (v: string) => Promise<void>;
|
||||||
|
onCancel: () => void;
|
||||||
|
subTitle?: ReactNode;
|
||||||
|
}) {
|
||||||
|
const [pin, setPin] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKey = (e: KeyboardEvent) => {
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
console.debug(e);
|
console.debug(e);
|
||||||
if (!isNaN(Number(e.key)) && pin.length < PinLen) {
|
if (!isNaN(Number(e.key)) && pin.length < PinLen) {
|
||||||
setPin(s => s += e.key);
|
setPin(s => (s += e.key));
|
||||||
} if (e.key === "Backspace") {
|
}
|
||||||
setPin(s => s.slice(0, -1));
|
if (e.key === "Backspace") {
|
||||||
} else {
|
setPin(s => s.slice(0, -1));
|
||||||
e.preventDefault();
|
} else {
|
||||||
}
|
e.preventDefault();
|
||||||
};
|
}
|
||||||
const handler = (e: Event) => handleKey(e as KeyboardEvent);
|
};
|
||||||
document.addEventListener("keydown", handler);
|
const handler = (e: Event) => handleKey(e as KeyboardEvent);
|
||||||
return () => document.removeEventListener("keydown", handler);
|
document.addEventListener("keydown", handler);
|
||||||
}, [pin]);
|
return () => document.removeEventListener("keydown", handler);
|
||||||
|
}, [pin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pin.length > 0) {
|
if (pin.length > 0) {
|
||||||
setError("");
|
setError("");
|
||||||
}
|
|
||||||
|
|
||||||
if (pin.length === PinLen) {
|
|
||||||
onResult(pin).catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
setPin("");
|
|
||||||
if (e instanceof InvalidPinError) {
|
|
||||||
setError(formatMessage({
|
|
||||||
defaultMessage: "Incorrect pin"
|
|
||||||
}));
|
|
||||||
} else if (e instanceof Error) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [pin]);
|
|
||||||
|
|
||||||
const boxes = [];
|
|
||||||
if (pin.length === PinLen) {
|
|
||||||
boxes.push(<Spinner className="flex f-center f-1" />);
|
|
||||||
} else {
|
|
||||||
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">
|
if (pin.length === PinLen) {
|
||||||
<h2>
|
onResult(pin).catch(e => {
|
||||||
<FormattedMessage defaultMessage="Enter Pin" />
|
console.error(e);
|
||||||
</h2>
|
setPin("");
|
||||||
{subTitle}
|
if (e instanceof InvalidPinError) {
|
||||||
<div className="flex g4">
|
setError(
|
||||||
{boxes}
|
formatMessage({
|
||||||
</div>
|
defaultMessage: "Incorrect pin",
|
||||||
{error && <b className="error">{error}</b>}
|
}),
|
||||||
<div>
|
);
|
||||||
<button type="button" onClick={() => onCancel()}>
|
} else if (e instanceof Error) {
|
||||||
<FormattedMessage defaultMessage="Cancel" />
|
setError(e.message);
|
||||||
</button>
|
}
|
||||||
</div>
|
});
|
||||||
|
}
|
||||||
|
}, [pin]);
|
||||||
|
|
||||||
|
const boxes = [];
|
||||||
|
if (pin.length === PinLen) {
|
||||||
|
boxes.push(<Spinner className="flex f-center f-1" />);
|
||||||
|
} else {
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoginUnlock() {
|
export function LoginUnlock() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
async function encryptMigration(pin: string) {
|
async function encryptMigration(pin: string) {
|
||||||
const k = unwrap(login.privateKey);
|
const k = unwrap(login.privateKey);
|
||||||
const newPin = await PinEncrypted.create(k, pin);
|
const newPin = await PinEncrypted.create(k, pin);
|
||||||
|
|
||||||
const pub = EventPublisher.privateKey(k);
|
const pub = EventPublisher.privateKey(k);
|
||||||
if (login.preferences.pow) {
|
if (login.preferences.pow) {
|
||||||
pub.pow(login.preferences.pow, DefaultPowWorker);
|
pub.pow(login.preferences.pow, DefaultPowWorker);
|
||||||
}
|
|
||||||
LoginStore.setPublisher(login.id, pub);
|
|
||||||
LoginStore.updateSession({
|
|
||||||
...login,
|
|
||||||
privateKeyData: newPin,
|
|
||||||
privateKey: undefined
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
LoginStore.setPublisher(login.id, pub);
|
||||||
|
LoginStore.updateSession({
|
||||||
|
...login,
|
||||||
|
privateKeyData: newPin,
|
||||||
|
privateKey: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function unlockSession(pin: string) {
|
async function unlockSession(pin: string) {
|
||||||
const key = new PinEncrypted(unwrap(login.privateKeyData) as PinEncryptedPayload);
|
const key = new PinEncrypted(unwrap(login.privateKeyData) as PinEncryptedPayload);
|
||||||
await key.decrypt(pin);
|
await key.decrypt(pin);
|
||||||
const pub = createPublisher(login, key);
|
const pub = createPublisher(login, key);
|
||||||
if (pub) {
|
if (pub) {
|
||||||
if (login.preferences.pow) {
|
if (login.preferences.pow) {
|
||||||
pub.pow(login.preferences.pow, DefaultPowWorker);
|
pub.pow(login.preferences.pow, DefaultPowWorker);
|
||||||
}
|
}
|
||||||
LoginStore.setPublisher(login.id, pub);
|
LoginStore.setPublisher(login.id, pub);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (login.publicKey && !publisher && sessionNeedsPin(login)) {
|
if (login.publicKey && !publisher && sessionNeedsPin(login)) {
|
||||||
if (login.privateKey !== undefined) {
|
if (login.privateKey !== undefined) {
|
||||||
return <PinPrompt subTitle={<p>
|
return (
|
||||||
<FormattedMessage defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open Snort." />
|
<PinPrompt
|
||||||
</p>} onResult={encryptMigration} onCancel={() => {
|
subTitle={
|
||||||
// nothing
|
<p>
|
||||||
}} />
|
<FormattedMessage defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open Snort." />
|
||||||
}
|
</p>
|
||||||
return <PinPrompt subTitle={<p>
|
}
|
||||||
|
onResult={encryptMigration}
|
||||||
|
onCancel={() => {
|
||||||
|
// nothing
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PinPrompt
|
||||||
|
subTitle={
|
||||||
|
<p>
|
||||||
<FormattedMessage defaultMessage="Enter pin to unlock private key" />
|
<FormattedMessage defaultMessage="Enter pin to unlock private key" />
|
||||||
</p>} onResult={unlockSession} onCancel={() => {
|
</p>
|
||||||
//nothing
|
}
|
||||||
}} />
|
onResult={unlockSession}
|
||||||
}
|
onCancel={() => {
|
||||||
}
|
//nothing
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ 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({ onClose, ev }: { onClose: () => void, ev: TaggedNostrEvent }) {
|
export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: TaggedNostrEvent }) {
|
||||||
const [selected, setSelected] = useState<Array<string>>();
|
const [selected, setSelected] = useState<Array<string>>();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
@ -44,10 +44,12 @@ export function ReBroadcaster({ onClose, ev }: { onClose: () => void, ev: Tagged
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!selected || selected.includes(r)}
|
checked={!selected || selected.includes(r)}
|
||||||
onChange={e => setSelected(
|
onChange={e =>
|
||||||
e.target.checked && selected && selected.length == a.length - 1
|
setSelected(
|
||||||
? undefined
|
e.target.checked && selected && selected.length == a.length - 1
|
||||||
: a.filter(el => el === r ? e.target.checked : !selected || selected.includes(el)))
|
? undefined
|
||||||
|
: a.filter(el => (el === r ? e.target.checked : !selected || selected.includes(el))),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
|||||||
import { getNip05PubKey } from "Pages/LoginPage";
|
import { getNip05PubKey } from "Pages/LoginPage";
|
||||||
import { bech32ToHex } from "SnortUtils";
|
import { bech32ToHex } from "SnortUtils";
|
||||||
|
|
||||||
export class PinRequiredError extends Error { }
|
export class PinRequiredError extends Error {}
|
||||||
|
|
||||||
export default function useLoginHandler() {
|
export default function useLoginHandler() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -172,12 +172,12 @@ export function sessionNeedsPin(l: LoginSession) {
|
|||||||
export function createPublisher(l: LoginSession, pin?: PinEncrypted) {
|
export function createPublisher(l: LoginSession, pin?: PinEncrypted) {
|
||||||
switch (l.type) {
|
switch (l.type) {
|
||||||
case LoginSessionType.PrivateKey: {
|
case LoginSessionType.PrivateKey: {
|
||||||
if(!pin) throw new PinRequiredError();
|
if (!pin) throw new PinRequiredError();
|
||||||
l.privateKeyData = pin;
|
l.privateKeyData = pin;
|
||||||
return EventPublisher.privateKey(pin.value);
|
return EventPublisher.privateKey(pin.value);
|
||||||
}
|
}
|
||||||
case LoginSessionType.Nip46: {
|
case LoginSessionType.Nip46: {
|
||||||
if(!pin) throw new PinRequiredError();
|
if (!pin) throw new PinRequiredError();
|
||||||
l.privateKeyData = pin;
|
l.privateKeyData = pin;
|
||||||
|
|
||||||
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
|
const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
|
||||||
@ -194,4 +194,4 @@ export function createPublisher(l: LoginSession, pin?: PinEncrypted) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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 { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import { HexKey, RelaySettings, PinEncrypted, EventPublisher } 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";
|
||||||
@ -117,7 +117,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
} as LoginSession;
|
} as LoginSession;
|
||||||
|
|
||||||
const pub = createPublisher(newSession);
|
const pub = createPublisher(newSession);
|
||||||
if(pub) {
|
if (pub) {
|
||||||
this.setPublisher(newSession.id, pub);
|
this.setPublisher(newSession.id, pub);
|
||||||
}
|
}
|
||||||
this.#accounts.set(newSession.id, newSession);
|
this.#accounts.set(newSession.id, newSession);
|
||||||
@ -188,7 +188,7 @@ 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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
#migrate() {
|
#migrate() {
|
||||||
@ -229,8 +229,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
this.#activeAccount = this.#accounts.keys().next().value;
|
this.#activeAccount = this.#accounts.keys().next().value;
|
||||||
}
|
}
|
||||||
const toSave = [...this.#accounts.values()];
|
const toSave = [...this.#accounts.values()];
|
||||||
for(const v of toSave) {
|
for (const v of toSave) {
|
||||||
if(v.privateKeyData instanceof PinEncrypted) {
|
if (v.privateKeyData instanceof PinEncrypted) {
|
||||||
v.privateKeyData = v.privateKeyData.toPayload();
|
v.privateKeyData = v.privateKeyData.toPayload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,31 +53,35 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (<>
|
return (
|
||||||
<div className={pageClass}>
|
<>
|
||||||
{!shouldHideHeader && (
|
<div className={pageClass}>
|
||||||
<header className="main-content">
|
{!shouldHideHeader && (
|
||||||
<LogoHeader />
|
<header className="main-content">
|
||||||
<AccountHeader />
|
<LogoHeader />
|
||||||
</header>
|
<AccountHeader />
|
||||||
)}
|
</header>
|
||||||
<Outlet />
|
)}
|
||||||
|
<Outlet />
|
||||||
{!shouldHideNoteCreator && (
|
{!shouldHideNoteCreator && (
|
||||||
<>
|
<>
|
||||||
<button className="primary note-create-button" onClick={() => note.update(v => {
|
<button
|
||||||
v.replyTo = undefined;
|
className="primary note-create-button"
|
||||||
v.show = true
|
onClick={() =>
|
||||||
})}>
|
note.update(v => {
|
||||||
<Icon name="plus" size={16} />
|
v.replyTo = undefined;
|
||||||
</button>
|
v.show = true;
|
||||||
<NoteCreator />
|
})
|
||||||
</>
|
}>
|
||||||
)}
|
<Icon name="plus" size={16} />
|
||||||
<Toaster />
|
</button>
|
||||||
</div>
|
<NoteCreator />
|
||||||
<LoginUnlock />
|
</>
|
||||||
</>
|
)}
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
<LoginUnlock />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +142,7 @@ const AccountHeader = () => {
|
|||||||
<button type="button" onClick={() => navigate("/login")}>
|
<button type="button" onClick={() => navigate("/login")}>
|
||||||
<FormattedMessage {...messages.Login} />
|
<FormattedMessage {...messages.Login} />
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
@ -199,4 +203,4 @@ function LogoHeader() {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -300,15 +300,23 @@ export default function LoginPage() {
|
|||||||
<AsyncButton onClick={() => setPin(true)}>
|
<AsyncButton onClick={() => setPin(true)}>
|
||||||
<FormattedMessage defaultMessage="Create Account" />
|
<FormattedMessage defaultMessage="Create Account" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
{pin && <PinPrompt subTitle={<p>
|
{pin && (
|
||||||
<FormattedMessage defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open Snort." />
|
<PinPrompt
|
||||||
</p>} onResult={async pin => {
|
subTitle={
|
||||||
if (key) {
|
<p>
|
||||||
await doLogin(pin);
|
<FormattedMessage defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open Snort." />
|
||||||
} else {
|
</p>
|
||||||
await makeRandomKey(pin);
|
}
|
||||||
}
|
onResult={async pin => {
|
||||||
}} onCancel={() => setPin(false)} />}
|
if (key) {
|
||||||
|
await doLogin(pin);
|
||||||
|
} else {
|
||||||
|
await makeRandomKey(pin);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => setPin(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{altLogins()}
|
{altLogins()}
|
||||||
</div>
|
</div>
|
||||||
{installExtension()}
|
{installExtension()}
|
||||||
|
@ -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}
|
||||||
|
@ -38,7 +38,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
|||||||
update: (fn: (v: NoteCreatorDataSnapshot) => void) => {
|
update: (fn: (v: NoteCreatorDataSnapshot) => void) => {
|
||||||
fn(this.#data);
|
fn(this.#data);
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
|||||||
fn(this.#data);
|
fn(this.#data);
|
||||||
console.debug(this.#data);
|
console.debug(this.#data);
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
}
|
},
|
||||||
} as NoteCreatorDataSnapshot;
|
} as NoteCreatorDataSnapshot;
|
||||||
return sn;
|
return sn;
|
||||||
}
|
}
|
||||||
@ -76,5 +76,8 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
|||||||
const NoteCreatorState = new NoteCreatorStore();
|
const NoteCreatorState = new NoteCreatorStore();
|
||||||
|
|
||||||
export function useNoteCreator() {
|
export function useNoteCreator() {
|
||||||
return useSyncExternalStore(c => NoteCreatorState.hook(c), () => NoteCreatorState.snapshot());
|
return useSyncExternalStore(
|
||||||
}
|
c => NoteCreatorState.hook(c),
|
||||||
|
() => NoteCreatorState.snapshot(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -99,6 +99,9 @@
|
|||||||
"25V4l1": {
|
"25V4l1": {
|
||||||
"defaultMessage": "Banner"
|
"defaultMessage": "Banner"
|
||||||
},
|
},
|
||||||
|
"27jzYm": {
|
||||||
|
"defaultMessage": "Enter pin to unlock private key"
|
||||||
|
},
|
||||||
"2IFGap": {
|
"2IFGap": {
|
||||||
"defaultMessage": "Donate"
|
"defaultMessage": "Donate"
|
||||||
},
|
},
|
||||||
@ -539,6 +542,9 @@
|
|||||||
"KoFlZg": {
|
"KoFlZg": {
|
||||||
"defaultMessage": "Enter mint URL"
|
"defaultMessage": "Enter mint URL"
|
||||||
},
|
},
|
||||||
|
"KtsyO0": {
|
||||||
|
"defaultMessage": "Enter Pin"
|
||||||
|
},
|
||||||
"LF5kYT": {
|
"LF5kYT": {
|
||||||
"defaultMessage": "Other Connections"
|
"defaultMessage": "Other Connections"
|
||||||
},
|
},
|
||||||
@ -887,6 +893,9 @@
|
|||||||
"defaultMessage": "Install Extension",
|
"defaultMessage": "Install Extension",
|
||||||
"description": "Heading for install key manager extension"
|
"description": "Heading for install key manager extension"
|
||||||
},
|
},
|
||||||
|
"c2DTVd": {
|
||||||
|
"defaultMessage": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort."
|
||||||
|
},
|
||||||
"c35bj2": {
|
"c35bj2": {
|
||||||
"defaultMessage": "If you have an enquiry about your NIP-05 order please DM {link}"
|
"defaultMessage": "If you have an enquiry about your NIP-05 order please DM {link}"
|
||||||
},
|
},
|
||||||
@ -1230,6 +1239,9 @@
|
|||||||
"qtWLmt": {
|
"qtWLmt": {
|
||||||
"defaultMessage": "Like"
|
"defaultMessage": "Like"
|
||||||
},
|
},
|
||||||
|
"qz9fty": {
|
||||||
|
"defaultMessage": "Incorrect pin"
|
||||||
|
},
|
||||||
"r3C4x/": {
|
"r3C4x/": {
|
||||||
"defaultMessage": "Software"
|
"defaultMessage": "Software"
|
||||||
},
|
},
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"1udzha": "Conversations",
|
"1udzha": "Conversations",
|
||||||
"2/2yg+": "Add",
|
"2/2yg+": "Add",
|
||||||
"25V4l1": "Banner",
|
"25V4l1": "Banner",
|
||||||
|
"27jzYm": "Enter pin to unlock private key",
|
||||||
"2IFGap": "Donate",
|
"2IFGap": "Donate",
|
||||||
"2LbrkB": "Enter password",
|
"2LbrkB": "Enter password",
|
||||||
"2a2YiP": "{n} Bookmarks",
|
"2a2YiP": "{n} Bookmarks",
|
||||||
@ -177,6 +178,7 @@
|
|||||||
"KWuDfz": "I have saved my keys, continue",
|
"KWuDfz": "I have saved my keys, continue",
|
||||||
"KahimY": "Unknown event kind: {kind}",
|
"KahimY": "Unknown event kind: {kind}",
|
||||||
"KoFlZg": "Enter mint URL",
|
"KoFlZg": "Enter mint URL",
|
||||||
|
"KtsyO0": "Enter Pin",
|
||||||
"LF5kYT": "Other Connections",
|
"LF5kYT": "Other Connections",
|
||||||
"LXxsbk": "Anonymous",
|
"LXxsbk": "Anonymous",
|
||||||
"LgbKvU": "Comment",
|
"LgbKvU": "Comment",
|
||||||
@ -290,6 +292,7 @@
|
|||||||
"brAXSu": "Pick a username",
|
"brAXSu": "Pick a username",
|
||||||
"bxv59V": "Just now",
|
"bxv59V": "Just now",
|
||||||
"c+oiJe": "Install Extension",
|
"c+oiJe": "Install Extension",
|
||||||
|
"c2DTVd": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort.",
|
||||||
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
|
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
|
||||||
"c3g2hL": "Broadcast Again",
|
"c3g2hL": "Broadcast Again",
|
||||||
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
|
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
|
||||||
@ -402,6 +405,7 @@
|
|||||||
"qkvYUb": "Add to Profile",
|
"qkvYUb": "Add to Profile",
|
||||||
"qmJ8kD": "Translation failed",
|
"qmJ8kD": "Translation failed",
|
||||||
"qtWLmt": "Like",
|
"qtWLmt": "Like",
|
||||||
|
"qz9fty": "Incorrect pin",
|
||||||
"r3C4x/": "Software",
|
"r3C4x/": "Software",
|
||||||
"r5srDR": "Enter wallet password",
|
"r5srDR": "Enter wallet password",
|
||||||
"rT14Ow": "Add Relays",
|
"rT14Ow": "Add Relays",
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { scryptAsync } from "@noble/hashes/scrypt";
|
import { scryptAsync } from "@noble/hashes/scrypt";
|
||||||
import { sha256 } from '@noble/hashes/sha256';
|
import { sha256 } from "@noble/hashes/sha256";
|
||||||
import { hmac } from "@noble/hashes/hmac";
|
import { hmac } from "@noble/hashes/hmac";
|
||||||
import { bytesToHex, hexToBytes, randomBytes } from "@noble/hashes/utils";
|
import { bytesToHex, hexToBytes, randomBytes } from "@noble/hashes/utils";
|
||||||
import { base64 } from "@scure/base";
|
import { base64 } from "@scure/base";
|
||||||
import { streamXOR as xchacha20 } from "@stablelib/xchacha20";
|
import { streamXOR as xchacha20 } from "@stablelib/xchacha20";
|
||||||
|
|
||||||
export class InvalidPinError extends Error {
|
export class InvalidPinError extends Error {
|
||||||
constructor(){
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,56 +15,55 @@ export class InvalidPinError extends Error {
|
|||||||
* Pin protected data
|
* Pin protected data
|
||||||
*/
|
*/
|
||||||
export class PinEncrypted {
|
export class PinEncrypted {
|
||||||
static readonly #opts = {N: 2**20, r: 8, p: 1, dkLen: 32}
|
static readonly #opts = { N: 2 ** 20, r: 8, p: 1, dkLen: 32 };
|
||||||
#decrypted?: Uint8Array
|
#decrypted?: Uint8Array;
|
||||||
#encrypted: PinEncryptedPayload
|
#encrypted: PinEncryptedPayload;
|
||||||
|
|
||||||
constructor(enc: PinEncryptedPayload) {
|
|
||||||
this.#encrypted = enc;
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
if(!this.#decrypted) throw new Error("Content has not been decrypted yet");
|
|
||||||
return bytesToHex(this.#decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(pin: string) {
|
|
||||||
const key = await scryptAsync(pin, base64.decode(this.#encrypted.salt), PinEncrypted.#opts);
|
|
||||||
const ciphertext = base64.decode(this.#encrypted.ciphertext);
|
|
||||||
const nonce = base64.decode(this.#encrypted.iv);
|
|
||||||
const plaintext = xchacha20(key, nonce, ciphertext, new Uint8Array(32));
|
|
||||||
if(plaintext.length !== 32) throw new InvalidPinError();
|
|
||||||
const mac = base64.encode(hmac(sha256, key, plaintext));
|
|
||||||
if(mac !== this.#encrypted.mac) throw new InvalidPinError();
|
|
||||||
this.#decrypted = plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
toPayload() {
|
constructor(enc: PinEncryptedPayload) {
|
||||||
return this.#encrypted;
|
this.#encrypted = enc;
|
||||||
}
|
|
||||||
|
|
||||||
static async create(content: string, pin: string) {
|
|
||||||
const salt = randomBytes(24);
|
|
||||||
const nonce = randomBytes(24);
|
|
||||||
const plaintext = hexToBytes(content);
|
|
||||||
const key = await scryptAsync(pin, salt, PinEncrypted.#opts);
|
|
||||||
const mac = base64.encode(hmac(sha256, key, plaintext));
|
|
||||||
const ciphertext = xchacha20(key, nonce, plaintext, new Uint8Array(32));
|
|
||||||
const ret = new PinEncrypted({
|
|
||||||
salt: base64.encode(salt),
|
|
||||||
ciphertext: base64.encode(ciphertext),
|
|
||||||
iv: base64.encode(nonce),
|
|
||||||
mac
|
|
||||||
});
|
|
||||||
ret.#decrypted = plaintext;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PinEncryptedPayload {
|
get value() {
|
||||||
salt: string, // for KDF
|
if (!this.#decrypted) throw new Error("Content has not been decrypted yet");
|
||||||
ciphertext: string
|
return bytesToHex(this.#decrypted);
|
||||||
iv: string,
|
|
||||||
mac: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async decrypt(pin: string) {
|
||||||
|
const key = await scryptAsync(pin, base64.decode(this.#encrypted.salt), PinEncrypted.#opts);
|
||||||
|
const ciphertext = base64.decode(this.#encrypted.ciphertext);
|
||||||
|
const nonce = base64.decode(this.#encrypted.iv);
|
||||||
|
const plaintext = xchacha20(key, nonce, ciphertext, new Uint8Array(32));
|
||||||
|
if (plaintext.length !== 32) throw new InvalidPinError();
|
||||||
|
const mac = base64.encode(hmac(sha256, key, plaintext));
|
||||||
|
if (mac !== this.#encrypted.mac) throw new InvalidPinError();
|
||||||
|
this.#decrypted = plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
toPayload() {
|
||||||
|
return this.#encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(content: string, pin: string) {
|
||||||
|
const salt = randomBytes(24);
|
||||||
|
const nonce = randomBytes(24);
|
||||||
|
const plaintext = hexToBytes(content);
|
||||||
|
const key = await scryptAsync(pin, salt, PinEncrypted.#opts);
|
||||||
|
const mac = base64.encode(hmac(sha256, key, plaintext));
|
||||||
|
const ciphertext = xchacha20(key, nonce, plaintext, new Uint8Array(32));
|
||||||
|
const ret = new PinEncrypted({
|
||||||
|
salt: base64.encode(salt),
|
||||||
|
ciphertext: base64.encode(ciphertext),
|
||||||
|
iv: base64.encode(nonce),
|
||||||
|
mac,
|
||||||
|
});
|
||||||
|
ret.#decrypted = plaintext;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PinEncryptedPayload {
|
||||||
|
salt: string; // for KDF
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
mac: string;
|
||||||
|
}
|
||||||
|
@ -6,8 +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) {
|
||||||
const ent = Object.entries(f).filter(([,v]) => Array.isArray(v));
|
const ent = Object.entries(f).filter(([, v]) => Array.isArray(v));
|
||||||
if(ent.every(([,v]) => (v as Array<string | number>).length > 0)) {
|
if (ent.every(([, v]) => (v as Array<string | number>).length > 0)) {
|
||||||
fNew.push(f);
|
fNew.push(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user