Better POW UX

This commit is contained in:
Kieran 2023-08-18 19:01:34 +01:00
parent 1c5e61e020
commit a5be4da2e8
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
4 changed files with 78 additions and 28 deletions

View File

@ -36,6 +36,7 @@ import { LoginStore } from "Login";
import { getCurrentSubscription } from "Subscription"; import { getCurrentSubscription } from "Subscription";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index"; import { System } from "index";
import AsyncButton from "Element/AsyncButton";
interface NotePreviewProps { interface NotePreviewProps {
note: TaggedNostrEvent; note: TaggedNostrEvent;
@ -174,9 +175,9 @@ export function NoteCreator() {
dispatch(reset()); dispatch(reset());
} }
function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) { async function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
ev.stopPropagation(); ev.stopPropagation();
sendNote().catch(console.warn); await sendNote();
} }
async function loadPreview() { async function loadPreview() {
@ -374,9 +375,9 @@ export function NoteCreator() {
<button className="secondary" onClick={cancel}> <button className="secondary" onClick={cancel}>
<FormattedMessage {...messages.Cancel} /> <FormattedMessage {...messages.Cancel} />
</button> </button>
<button onClick={onSubmit}> <AsyncButton onClick={onSubmit}>
{replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />} {replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
</button> </AsyncButton>
</div> </div>
{showAdvanced && ( {showAdvanced && (
<div> <div>

View File

@ -1,8 +1,8 @@
import React, { useEffect, useState } from "react"; import React, { HTMLProps, useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux"; 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, HexKey, u256, ParsedZap } from "@snort/system"; import { TaggedNostrEvent, HexKey, u256, ParsedZap, countLeadingZeros } from "@snort/system";
import { LNURL } from "@snort/shared"; import { LNURL } from "@snort/shared";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
@ -11,7 +11,7 @@ import Spinner from "Icons/Spinner";
import { formatShort } from "Number"; import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
import { delay, normalizeReaction, unwrap } from "SnortUtils"; import { delay, findTag, normalizeReaction, unwrap } 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";
@ -173,16 +173,26 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
}, [prefs.autoZap, author, zapping]); }, [prefs.autoZap, author, zapping]);
function powIcon() {
const pow = findTag(ev, "nonce") ? countLeadingZeros(ev.id) : undefined;
if (pow) {
return (
<AsyncFooterIcon title={formatMessage({ defaultMessage: "Proof of Work" })} iconName="diamond" value={pow} />
);
}
}
function tipButton() { function tipButton() {
const service = getLNURL(); const service = getLNURL();
if (service) { if (service) {
return ( return (
<> <AsyncFooterIcon
<div className={`reaction-pill ${didZap ? "reacted" : ""}`} {...longPress()} onClick={e => fastZap(e)}> className={didZap ? "reacted" : ""}
{zapping ? <Spinner /> : wallet?.isReady() ? <Icon name="zapFast" /> : <Icon name="zap" />} {...longPress()}
{zapTotal > 0 && <div className="reaction-pill-number">{formatShort(zapTotal)}</div>} iconName={wallet?.isReady() ? "zapFast" : "zap"}
</div> value={zapTotal}
</> onClick={e => fastZap(e)}
/>
); );
} }
return null; return null;
@ -190,10 +200,12 @@ export default function NoteFooter(props: NoteFooterProps) {
function repostIcon() { function repostIcon() {
return ( return (
<div className={`reaction-pill ${hasReposted() ? "reacted" : ""}`} onClick={() => repost()}> <AsyncFooterIcon
<Icon name="repeat" size={18} /> className={hasReposted() ? "reacted" : ""}
{reposts.length > 0 && <div className="reaction-pill-number">{formatShort(reposts.length)}</div>} iconName="repeat"
</div> value={reposts.length}
onClick={() => repost()}
/>
); );
} }
@ -203,12 +215,12 @@ export default function NoteFooter(props: NoteFooterProps) {
} }
const reacted = hasReacted("+"); const reacted = hasReacted("+");
return ( return (
<> <AsyncFooterIcon
<div className={`reaction-pill ${reacted ? "reacted" : ""} `} onClick={() => react(prefs.reactionEmoji)}> className={reacted ? "reacted" : ""}
<Icon name={reacted ? "heart-solid" : "heart"} size={18} /> iconName={reacted ? "heart-solid" : "heart"}
<div className="reaction-pill-number">{formatShort(positive.length)}</div> value={positive.length}
</div> onClick={() => react(prefs.reactionEmoji)}
</> />
); );
} }
@ -228,9 +240,12 @@ export default function NoteFooter(props: NoteFooterProps) {
{tipButton()} {tipButton()}
{reactionIcons()} {reactionIcons()}
{repostIcon()} {repostIcon()}
<div className={`reaction-pill ${showNoteCreatorModal ? "reacted" : ""}`} onClick={handleReplyButtonClick}> <AsyncFooterIcon
<Icon name="reply" size={17} /> className={showNoteCreatorModal ? "reacted" : ""}
</div> iconName="reply"
onClick={async () => handleReplyButtonClick()}
/>
{powIcon()}
</div> </div>
{willRenderNoteCreator && <NoteCreator />} {willRenderNoteCreator && <NoteCreator />}
<SendSats <SendSats
@ -247,3 +262,36 @@ export default function NoteFooter(props: NoteFooterProps) {
</> </>
); );
} }
interface AsyncFooterIconProps extends HTMLProps<HTMLDivElement> {
iconName: string;
value?: number;
loading?: boolean;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => Promise<void>;
}
function AsyncFooterIcon(props: AsyncFooterIconProps) {
const [loading, setLoading] = useState(props.loading ?? false);
async function handleClick(e: React.MouseEvent<HTMLDivElement>) {
setLoading(true);
try {
if (props.onClick) {
await props.onClick(e);
}
} catch (ex) {
console.error(ex);
}
setLoading(false);
}
return (
<div
{...props}
className={`reaction-pill${props.className ? ` ${props.className}` : ""}`}
onClick={e => handleClick(e)}>
{loading ? <Spinner /> : <Icon name={props.iconName} size={18} />}
{props.value && <div className="reaction-pill-number">{formatShort(props.value)}</div>}
</div>
);
}

View File

@ -22,6 +22,7 @@ export * from "./zaps";
export * from "./signer"; export * from "./signer";
export * from "./text"; export * from "./text";
export * from "./pow"; export * from "./pow";
export * from "./pow-util";
export * from "./impl/nip4"; export * from "./impl/nip4";
export * from "./impl/nip44"; export * from "./impl/nip44";

View File

@ -30,7 +30,7 @@ export function minePow(e: NostrPowEvent, target: number) {
e.tags[nonceTagIdx][1] = (++ctr).toString(); e.tags[nonceTagIdx][1] = (++ctr).toString();
e.id = createId(e); e.id = createId(e);
} while (countLeadingZeroes(e.id) < target); } while (countLeadingZeros(e.id) < target);
return e; return e;
} }
@ -40,7 +40,7 @@ function createId(e: NostrPowEvent) {
return bytesToHex(sha256(JSON.stringify(payload))); return bytesToHex(sha256(JSON.stringify(payload)));
} }
function countLeadingZeroes(hex: string) { export function countLeadingZeros(hex: string) {
let count = 0; let count = 0;
for (let i = 0; i < hex.length; i++) { for (let i = 0; i < hex.length; i++) {