chore: cleanup AsyncIcon element
This commit is contained in:
@ -1,31 +1,15 @@
|
||||
import "./AsyncButton.css";
|
||||
import React, { useState, ForwardedRef } from "react";
|
||||
import React, { ForwardedRef } from "react";
|
||||
import Spinner from "../Icons/Spinner";
|
||||
import useLoading from "Hooks/useLoading";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
disabled?: boolean;
|
||||
onClick(e: React.MouseEvent): Promise<void> | void;
|
||||
children?: React.ReactNode;
|
||||
export interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
onClick?: (e: React.MouseEvent) => Promise<void> | void;
|
||||
}
|
||||
|
||||
const AsyncButton = React.forwardRef<HTMLButtonElement, AsyncButtonProps>((props, ref) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
async function handle(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (loading || props.disabled) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
if (typeof props.onClick === "function") {
|
||||
const f = props.onClick(e);
|
||||
if (f instanceof Promise) {
|
||||
await f;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
const { handle, loading } = useLoading(props.onClick, props.disabled);
|
||||
|
||||
return (
|
||||
<button
|
||||
@ -33,7 +17,7 @@ const AsyncButton = React.forwardRef<HTMLButtonElement, AsyncButtonProps>((props
|
||||
type="button"
|
||||
disabled={loading || props.disabled}
|
||||
{...props}
|
||||
className={`spinner-button${props.className ? ` ${props.className}` : ""}`}
|
||||
className={classNames("spinner-button", props.className)}
|
||||
onClick={handle}>
|
||||
<span style={{ visibility: loading ? "hidden" : "visible" }}>{props.children}</span>
|
||||
{loading && (
|
||||
|
@ -1,35 +1,23 @@
|
||||
import Icon from "Icons/Icon";
|
||||
import useLoading from "Hooks/useLoading";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { HTMLProps, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
export interface AsyncIconProps extends HTMLProps<HTMLDivElement> {
|
||||
export type AsyncIconProps = React.HTMLProps<HTMLDivElement> & {
|
||||
iconName: string;
|
||||
iconSize?: number;
|
||||
loading?: boolean;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => Promise<void>;
|
||||
}
|
||||
onClick?: (e: React.MouseEvent) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export function AsyncIcon(props: AsyncIconProps) {
|
||||
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);
|
||||
}
|
||||
const { loading, handle } = useLoading(props.onClick, props.disabled);
|
||||
|
||||
const mergedProps = { ...props } as Record<string, unknown>;
|
||||
delete mergedProps["iconName"];
|
||||
delete mergedProps["iconSize"];
|
||||
delete mergedProps["loading"];
|
||||
return (
|
||||
<div {...mergedProps} onClick={e => handleClick(e)}>
|
||||
<div {...mergedProps} onClick={handle} className={classNames("button-icon-sm", props.className)}>
|
||||
{loading ? <Spinner /> : <Icon name={props.iconName} size={props.iconSize} />}
|
||||
{props.children}
|
||||
</div>
|
||||
|
@ -211,7 +211,7 @@ export function NoteCreator() {
|
||||
});
|
||||
}
|
||||
|
||||
async function onSubmit(ev: React.MouseEvent<HTMLButtonElement>) {
|
||||
async function onSubmit(ev: React.MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
await sendNote();
|
||||
}
|
||||
@ -450,14 +450,20 @@ export function NoteCreator() {
|
||||
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="pie-chart"
|
||||
iconSize={24}
|
||||
onClick={() => note.update(v => (v.pollOptions = ["A", "B"]))}
|
||||
className="note-creator-icon"
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
<AsyncIcon
|
||||
iconName="settings-04"
|
||||
iconSize={24}
|
||||
onClick={() => note.update(v => (v.advanced = !v.advanced))}
|
||||
className="note-creator-icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex g8">
|
||||
<button className="secondary" onClick={cancel}>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { HTMLProps, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLongPress } from "use-long-press";
|
||||
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, NostrLink } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { formatShort } from "Number";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
@ -11,7 +12,7 @@ import { delay, findTag, normalizeReaction } from "SnortUtils";
|
||||
import { NoteCreator } from "Element/Event/NoteCreator";
|
||||
import SendSats from "Element/SendSats";
|
||||
import { ZapsSummary } from "Element/Event/Zap";
|
||||
import { AsyncIcon } from "Element/AsyncIcon";
|
||||
import { AsyncIcon, AsyncIconProps } from "Element/AsyncIcon";
|
||||
|
||||
import { useWallet } from "Wallet";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
@ -308,18 +309,11 @@ 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) {
|
||||
function AsyncFooterIcon(props: AsyncIconProps & { value: number }) {
|
||||
const mergedProps = {
|
||||
...props,
|
||||
iconSize: 18,
|
||||
className: `transition duration-200 ease-in-out reaction-pill${props.className ? ` ${props.className}` : ""}`,
|
||||
className: classNames("transition duration-200 ease-in-out reaction-pill", props.className),
|
||||
};
|
||||
return (
|
||||
<AsyncIcon {...mergedProps}>
|
||||
|
20
packages/app/src/Hooks/useLoading.tsx
Normal file
20
packages/app/src/Hooks/useLoading.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function useLoading<T>(fn: ((e: React.MouseEvent) => Promise<T> | T) | undefined, disabled?: boolean) {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
async function handle(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (loading || disabled) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
if (typeof fn === "function") {
|
||||
await fn(e);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return { handle, loading };
|
||||
}
|
@ -24,6 +24,7 @@ import { getDisplayName } from "Element/User/DisplayName";
|
||||
import { Day } from "Const";
|
||||
import Tabs, { Tab } from "Element/Tabs";
|
||||
import classNames from "classnames";
|
||||
import { AsyncIcon } from "Element/AsyncIcon";
|
||||
|
||||
function notificationContext(ev: TaggedNostrEvent) {
|
||||
switch (ev.kind) {
|
||||
@ -194,15 +195,15 @@ function NotificationSummary({ evs }: { evs: Array<TaggedNostrEvent> }) {
|
||||
);
|
||||
}, [evs, period]);
|
||||
|
||||
const filterIcon = (f: NotificationSummaryFilter, icon: string, iconActiveClass?: string) => {
|
||||
const filterIcon = (f: NotificationSummaryFilter, icon: string, iconActiveClass: string) => {
|
||||
const active = hasFlag(filter, f);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames("icon-sm transparent", { active: active })}
|
||||
onClick={() => setFilter(v => v ^ f)}>
|
||||
<Icon name={icon} className={active ? iconActiveClass : undefined} />
|
||||
</button>
|
||||
<AsyncIcon
|
||||
className={classNames("button-icon-sm transparent", { active, [iconActiveClass]: active })}
|
||||
onClick={() => setFilter(v => v ^ f)}
|
||||
name={""}
|
||||
iconName={icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -337,20 +337,15 @@ button.icon:hover {
|
||||
color: var(--highlight);
|
||||
}
|
||||
|
||||
button.icon-sm {
|
||||
.button-icon-sm {
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--gray-light) !important;
|
||||
}
|
||||
|
||||
button.icon-sm:not(.active):hover {
|
||||
background-color: var(--gray-dark);
|
||||
}
|
||||
|
||||
button.icon-sm.active {
|
||||
.button-icon-sm.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
|
@ -120,9 +120,6 @@
|
||||
"3KNMbJ": {
|
||||
"defaultMessage": "Articles"
|
||||
},
|
||||
"3Rx6Qo": {
|
||||
"defaultMessage": "Advanced"
|
||||
},
|
||||
"3cc4Ct": {
|
||||
"defaultMessage": "Light"
|
||||
},
|
||||
|
@ -39,7 +39,6 @@
|
||||
"2k0Cv+": "Dislikes ({n})",
|
||||
"2ukA4d": "{n} hours",
|
||||
"3KNMbJ": "Articles",
|
||||
"3Rx6Qo": "Advanced",
|
||||
"3cc4Ct": "Light",
|
||||
"3gOsZq": "Translators",
|
||||
"3qnJlS": "You are voting with {amount} sats",
|
||||
|
Reference in New Issue
Block a user