This commit is contained in:
Kieran 2023-09-21 22:01:39 +01:00
parent 8244441929
commit 96d4e4bcc5
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
16 changed files with 480 additions and 384 deletions

View File

@ -9,9 +9,11 @@ export interface ModalProps {
}
export default function Modal(props: ModalProps) {
return <div className={`modal${props.className ? ` ${props.className}` : ""}`} onClick={props.onClose}>
return (
<div className={`modal${props.className ? ` ${props.className}` : ""}`} onClick={props.onClose}>
<div className="modal-body" onClick={e => e.stopPropagation()}>
{props.children}
</div>
</div>;
</div>
);
}

View File

@ -1,6 +1,14 @@
import "./NoteCreator.css";
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 useEventPublisher from "Hooks/useEventPublisher";
@ -30,7 +38,7 @@ export function NoteCreator() {
async function buildNote() {
try {
note.update(v => v.error = "");
note.update(v => (v.error = ""));
if (note && publisher) {
let extraTags: Array<Array<string>> | undefined;
if (note.zapSplits) {
@ -99,7 +107,9 @@ export function NoteCreator() {
eb.kind(kind);
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;
}
} catch (e) {
@ -131,7 +141,7 @@ export function NoteCreator() {
note.update(v => {
v.reset();
v.show = false;
})
});
}
}
@ -181,7 +191,7 @@ export function NoteCreator() {
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
const { value } = ev.target;
note.update(n => n.note = value);
note.update(n => (n.note = value));
}
function cancel() {
@ -198,10 +208,10 @@ export function NoteCreator() {
async function loadPreview() {
if (note.preview) {
note.update(v => v.preview = undefined);
note.update(v => (v.preview = undefined));
} else if (publisher) {
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>
))}
<button onClick={() => note.update(v => v.pollOptions = [...(note.pollOptions ?? []), ""])}>
<button onClick={() => note.update(v => (v.pollOptions = [...(note.pollOptions ?? []), ""]))}>
<Icon name="plus" size={14} />
</button>
</>
@ -256,7 +266,7 @@ export function NoteCreator() {
if (note.pollOptions) {
const copy = [...note.pollOptions];
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) {
const copy = [...note.pollOptions];
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"
checked={!note.selectedCustomRelays || note.selectedCustomRelays.includes(r)}
onChange={e => {
note.update(v => v.selectedCustomRelays = (
note.update(
v =>
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked && note.selectedCustomRelays && note.selectedCustomRelays.length == a.length - 1
e.target.checked &&
note.selectedCustomRelays &&
note.selectedCustomRelays.length == a.length - 1
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el => el === r ? e.target.checked : !note.selectedCustomRelays || note.selectedCustomRelays.includes(el))
));
}
}
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
);
}}
/>
</div>
</div>
@ -330,7 +347,8 @@ export function NoteCreator() {
};
if (!note.show) return null;
return (<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => v.show = false)}>
return (
<Modal id="note-creator" className="note-creator-modal" onClose={() => note.update(v => (v.show = false))}>
{note.replyTo && (
<Note
data={note.replyTo}
@ -352,7 +370,7 @@ export function NoteCreator() {
className={`textarea ${note.active ? "textarea--focused" : ""}`}
onChange={c => onChange(c)}
value={note.note}
onFocus={() => note.update(v => v.active = true)}
onFocus={() => note.update(v => (v.active = true))}
onKeyDown={e => {
if (e.key === "Enter" && e.metaKey) {
sendNote().catch(console.warn);
@ -364,14 +382,20 @@ export function NoteCreator() {
)}
<div className="flex f-space">
<div className="flex g8">
<ProfileImage pubkey={login.publicKey ?? ""} className="note-creator-icon" link="" showUsername={false} showFollowingMark={false} />
<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} />
<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)}>
<button className="secondary" onClick={() => note.update(v => (v.advanced = !v.advanced))}>
<FormattedMessage defaultMessage="Advanced" />
</button>
</div>
@ -414,7 +438,11 @@ export function NoteCreator() {
<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)))}
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>
@ -426,21 +454,30 @@ export function NoteCreator() {
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)))}
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>&nbsp;</div>
<Icon
name="close"
onClick={() => note.update(v => v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i))}
onClick={() => note.update(v => (v.zapSplits = (v.zapSplits ?? []).filter((_v, ii) => ii !== i)))}
/>
</div>
</div>
))}
<button
type="button"
onClick={() => note.update(v => v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }])}>
onClick={() =>
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
}>
<FormattedMessage defaultMessage="Add" />
</button>
</div>
@ -457,7 +494,7 @@ export function NoteCreator() {
className="w-max"
type="text"
value={note.sensitive}
onChange={e => note.update(v => v.sensitive = e.target.value)}
onChange={e => note.update(v => (v.sensitive = e.target.value))}
maxLength={50}
minLength={1}
placeholder={formatMessage({

View File

@ -11,7 +11,15 @@ import Modal from "./Modal";
import Spinner from "Icons/Spinner";
const PinLen = 6;
export function PinPrompt({ onResult, onCancel, subTitle }: { onResult: (v: string) => Promise<void>, onCancel: () => void, subTitle?: ReactNode }) {
export function PinPrompt({
onResult,
onCancel,
subTitle,
}: {
onResult: (v: string) => Promise<void>;
onCancel: () => void;
subTitle?: ReactNode;
}) {
const [pin, setPin] = useState("");
const [error, setError] = useState("");
const { formatMessage } = useIntl();
@ -20,8 +28,9 @@ export function PinPrompt({ onResult, onCancel, subTitle }: { onResult: (v: stri
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 += e.key));
}
if (e.key === "Backspace") {
setPin(s => s.slice(0, -1));
} else {
e.preventDefault();
@ -42,13 +51,15 @@ export function PinPrompt({ onResult, onCancel, subTitle }: { onResult: (v: stri
console.error(e);
setPin("");
if (e instanceof InvalidPinError) {
setError(formatMessage({
defaultMessage: "Incorrect pin"
}));
setError(
formatMessage({
defaultMessage: "Incorrect pin",
}),
);
} else if (e instanceof Error) {
setError(e.message);
}
})
});
}
}, [pin]);
@ -57,20 +68,17 @@ export function PinPrompt({ onResult, onCancel, subTitle }: { onResult: (v: stri
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>)
boxes.push(<div className="pin-box flex f-center f-1">{pin[x]}</div>);
}
}
return <Modal id="pin" onClose={() => onCancel()}>
return (
<Modal id="pin" onClose={() => onCancel()}>
<div className="flex-column g12">
<h2>
<FormattedMessage defaultMessage="Enter Pin" />
</h2>
{subTitle}
<div className="flex g4">
{boxes}
</div>
<div className="flex g4">{boxes}</div>
{error && <b className="error">{error}</b>}
<div>
<button type="button" onClick={() => onCancel()}>
@ -79,6 +87,7 @@ export function PinPrompt({ onResult, onCancel, subTitle }: { onResult: (v: stri
</div>
</div>
</Modal>
);
}
export function LoginUnlock() {
@ -97,7 +106,7 @@ export function LoginUnlock() {
LoginStore.updateSession({
...login,
privateKeyData: newPin,
privateKey: undefined
privateKey: undefined,
});
}
@ -115,16 +124,32 @@ export function LoginUnlock() {
if (login.publicKey && !publisher && sessionNeedsPin(login)) {
if (login.privateKey !== undefined) {
return <PinPrompt subTitle={<p>
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
}} />
</p>
}
return <PinPrompt subTitle={<p>
onResult={encryptMigration}
onCancel={() => {
// nothing
}}
/>
);
}
return (
<PinPrompt
subTitle={
<p>
<FormattedMessage defaultMessage="Enter pin to unlock private key" />
</p>} onResult={unlockSession} onCancel={() => {
</p>
}
onResult={unlockSession}
onCancel={() => {
//nothing
}} />
}}
/>
);
}
}

View File

@ -8,7 +8,7 @@ import messages from "./messages";
import useLogin from "Hooks/useLogin";
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 publisher = useEventPublisher();
@ -44,10 +44,12 @@ export function ReBroadcaster({ onClose, ev }: { onClose: () => void, ev: Tagged
<input
type="checkbox"
checked={!selected || selected.includes(r)}
onChange={e => setSelected(
onChange={e =>
setSelected(
e.target.checked && selected && selected.length == a.length - 1
? undefined
: a.filter(el => el === r ? e.target.checked : !selected || selected.includes(el)))
: a.filter(el => (el === r ? e.target.checked : !selected || selected.includes(el))),
)
}
/>
</div>

View File

@ -53,7 +53,8 @@ export default function Layout() {
}
}, [location]);
return (<>
return (
<>
<div className={pageClass}>
{!shouldHideHeader && (
<header className="main-content">
@ -62,13 +63,16 @@ export default function Layout() {
</header>
)}
<Outlet />
{!shouldHideNoteCreator && (
<>
<button className="primary note-create-button" onClick={() => note.update(v => {
<button
className="primary note-create-button"
onClick={() =>
note.update(v => {
v.replyTo = undefined;
v.show = true
})}>
v.show = true;
})
}>
<Icon name="plus" size={16} />
</button>
<NoteCreator />
@ -138,7 +142,7 @@ const AccountHeader = () => {
<button type="button" onClick={() => navigate("/login")}>
<FormattedMessage {...messages.Login} />
</button>
)
);
}
return (
<div className="header-actions">

View File

@ -300,15 +300,23 @@ export default function LoginPage() {
<AsyncButton onClick={() => setPin(true)}>
<FormattedMessage defaultMessage="Create Account" />
</AsyncButton>
{pin && <PinPrompt subTitle={<p>
{pin && (
<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={async pin => {
</p>
}
onResult={async pin => {
if (key) {
await doLogin(pin);
} else {
await makeRandomKey(pin);
}
}} onCancel={() => setPin(false)} />}
}}
onCancel={() => setPin(false)}
/>
)}
{altLogins()}
</div>
{installExtension()}

View File

@ -38,7 +38,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
update: (fn: (v: NoteCreatorDataSnapshot) => void) => {
fn(this.#data);
this.notifyChange();
}
},
};
}
@ -67,7 +67,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
fn(this.#data);
console.debug(this.#data);
this.notifyChange();
}
},
} as NoteCreatorDataSnapshot;
return sn;
}
@ -76,5 +76,8 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
const NoteCreatorState = new NoteCreatorStore();
export function useNoteCreator() {
return useSyncExternalStore(c => NoteCreatorState.hook(c), () => NoteCreatorState.snapshot());
return useSyncExternalStore(
c => NoteCreatorState.hook(c),
() => NoteCreatorState.snapshot(),
);
}

View File

@ -99,6 +99,9 @@
"25V4l1": {
"defaultMessage": "Banner"
},
"27jzYm": {
"defaultMessage": "Enter pin to unlock private key"
},
"2IFGap": {
"defaultMessage": "Donate"
},
@ -539,6 +542,9 @@
"KoFlZg": {
"defaultMessage": "Enter mint URL"
},
"KtsyO0": {
"defaultMessage": "Enter Pin"
},
"LF5kYT": {
"defaultMessage": "Other Connections"
},
@ -887,6 +893,9 @@
"defaultMessage": "Install Extension",
"description": "Heading for install key manager extension"
},
"c2DTVd": {
"defaultMessage": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort."
},
"c35bj2": {
"defaultMessage": "If you have an enquiry about your NIP-05 order please DM {link}"
},
@ -1230,6 +1239,9 @@
"qtWLmt": {
"defaultMessage": "Like"
},
"qz9fty": {
"defaultMessage": "Incorrect pin"
},
"r3C4x/": {
"defaultMessage": "Software"
},

View File

@ -32,6 +32,7 @@
"1udzha": "Conversations",
"2/2yg+": "Add",
"25V4l1": "Banner",
"27jzYm": "Enter pin to unlock private key",
"2IFGap": "Donate",
"2LbrkB": "Enter password",
"2a2YiP": "{n} Bookmarks",
@ -177,6 +178,7 @@
"KWuDfz": "I have saved my keys, continue",
"KahimY": "Unknown event kind: {kind}",
"KoFlZg": "Enter mint URL",
"KtsyO0": "Enter Pin",
"LF5kYT": "Other Connections",
"LXxsbk": "Anonymous",
"LgbKvU": "Comment",
@ -290,6 +292,7 @@
"brAXSu": "Pick a username",
"bxv59V": "Just now",
"c+oiJe": "Install Extension",
"c2DTVd": "Enter a pin to encrypt your private key, you must enter this pin every time you open Snort.",
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
"c3g2hL": "Broadcast Again",
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
@ -402,6 +405,7 @@
"qkvYUb": "Add to Profile",
"qmJ8kD": "Translation failed",
"qtWLmt": "Like",
"qz9fty": "Incorrect pin",
"r3C4x/": "Software",
"r5srDR": "Enter wallet password",
"rT14Ow": "Add Relays",

View File

@ -1,5 +1,5 @@
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 { bytesToHex, hexToBytes, randomBytes } from "@noble/hashes/utils";
import { base64 } from "@scure/base";
@ -15,9 +15,9 @@ export class InvalidPinError extends Error {
* Pin protected data
*/
export class PinEncrypted {
static readonly #opts = {N: 2**20, r: 8, p: 1, dkLen: 32}
#decrypted?: Uint8Array
#encrypted: PinEncryptedPayload
static readonly #opts = { N: 2 ** 20, r: 8, p: 1, dkLen: 32 };
#decrypted?: Uint8Array;
#encrypted: PinEncryptedPayload;
constructor(enc: PinEncryptedPayload) {
this.#encrypted = enc;
@ -54,7 +54,7 @@ export class PinEncrypted {
salt: base64.encode(salt),
ciphertext: base64.encode(ciphertext),
iv: base64.encode(nonce),
mac
mac,
});
ret.#decrypted = plaintext;
return ret;
@ -62,9 +62,8 @@ export class PinEncrypted {
}
export interface PinEncryptedPayload {
salt: string, // for KDF
ciphertext: string
iv: string,
mac: string
salt: string; // for KDF
ciphertext: string;
iv: string;
mac: string;
}