diff --git a/public/locales/en.json b/public/locales/en.json
index c1a7d2d..0cbe566 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -746,7 +746,12 @@
"categoryOtherWallets": "Other wallets",
"categoryAddressBook": "Address book",
"categoryExactAddress": "Exact address",
- "categoryExactName": "Exact name"
+ "categoryExactName": "Exact name",
+
+ "addressHint": "Balance: <1 />",
+ "addressHintWithNames": "Names: <1>{{names, number}}1>",
+ "nameHint": "Owner: <1 />",
+ "nameHintNotFound": "Name not found."
},
"sendTransaction": {
diff --git a/src/components/addresses/ContextualAddress.tsx b/src/components/addresses/ContextualAddress.tsx
index 3e5570a..a520d17 100644
--- a/src/components/addresses/ContextualAddress.tsx
+++ b/src/components/addresses/ContextualAddress.tsx
@@ -4,15 +4,13 @@
import classNames from "classnames";
import { Tooltip, Typography } from "antd";
-import { useSelector } from "react-redux";
-import { RootState } from "@store";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { KristAddress } from "@api/types";
import { Wallet, useWallets } from "@wallets";
import { parseCommonMeta, CommonMeta } from "@utils/commonmeta";
-import { stripNameSuffix } from "@utils/currency";
+import { useNameSuffix, stripNameSuffix } from "@utils/currency";
import { useBooleanSetting } from "@utils/settings";
import { KristNameLink } from "../names/KristNameLink";
@@ -84,7 +82,7 @@
}: Props): JSX.Element {
const { t } = useTranslation();
const { walletAddressMap } = useWallets();
- const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
+ const nameSuffix = useNameSuffix();
const addressCopyButtons = useBooleanSetting("addressCopyButtons");
if (!origAddress) return (
diff --git a/src/components/addresses/picker/AddressHint.tsx b/src/components/addresses/picker/AddressHint.tsx
new file mode 100644
index 0000000..d2d086a
--- /dev/null
+++ b/src/components/addresses/picker/AddressHint.tsx
@@ -0,0 +1,33 @@
+// Copyright (c) 2020-2021 Drew Lemmy
+// This file is part of KristWeb 2 under GPL-3.0.
+// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
+import { useTranslation, Trans } from "react-i18next";
+
+import { KristAddressWithNames } from "@api/lookup";
+import { KristValue } from "@comp/krist/KristValue";
+
+interface Props {
+ address?: KristAddressWithNames;
+ nameHint?: boolean;
+}
+
+export function AddressHint({ address, nameHint }: Props): JSX.Element {
+ const { t } = useTranslation();
+
+ return
+ {nameHint
+ ? (
+ // Show the name count if this picker is relevant to a name transfer
+
+ Balance: {{ names: address?.names || 0 }}
+
+ )
+ : (
+ // Otherwise, show the balance
+
+ Balance:
+
+ )
+ }
+ ;
+}
diff --git a/src/components/addresses/picker/AddressPicker.less b/src/components/addresses/picker/AddressPicker.less
index b3a598b..13ad66d 100644
--- a/src/components/addresses/picker/AddressPicker.less
+++ b/src/components/addresses/picker/AddressPicker.less
@@ -3,6 +3,14 @@
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
@import (reference) "../../../App.less";
+.address-picker {
+ margin-bottom: @form-item-margin-bottom;
+
+ .ant-form-item {
+ margin-bottom: 0;
+ }
+}
+
.address-picker-dropdown {
.address-picker-address-item {
display: flex;
diff --git a/src/components/addresses/picker/AddressPicker.tsx b/src/components/addresses/picker/AddressPicker.tsx
index aff7ad8..5bd5b39 100644
--- a/src/components/addresses/picker/AddressPicker.tsx
+++ b/src/components/addresses/picker/AddressPicker.tsx
@@ -8,11 +8,9 @@
import { useTranslation } from "react-i18next";
-import { useSelector } from "react-redux";
-import { RootState } from "@store";
-
import { useWallets } from "@wallets";
import {
+ useAddressPrefix, useNameSuffix,
isValidAddress, getNameParts,
getNameRegex, getAddressRegexV2
} from "@utils/currency";
@@ -20,6 +18,7 @@
import { getCategoryHeader } from "./Header";
import { getAddressItem } from "./Item";
import { getOptions } from "./options";
+import { usePickerHints } from "./PickerHints";
import "./AddressPicker.less";
@@ -31,6 +30,7 @@
walletsOnly?: boolean;
noNames?: boolean;
+ nameHint?: boolean;
className?: string;
}
@@ -43,6 +43,7 @@
walletsOnly,
noNames,
+ nameHint,
className,
...props
@@ -61,8 +62,8 @@
// to prepend to the list. Note that the 'exact address' item is NOT shown if
// the picker wants wallets only, or if the exact address already appears as a
// wallet (or later, an address book entry).
- const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix);
- const hasExactAddress = cleanValue
+ const addressPrefix = useAddressPrefix();
+ const hasExactAddress = !!cleanValue
&& !walletsOnly
&& isValidAddress(addressPrefix, cleanValue)
&& !addressList.includes(cleanValue);
@@ -75,10 +76,10 @@
// Check if the input text is an exact name. It may begin with a metaname, but
// must end with the name suffix.
- const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
+ const nameSuffix = useNameSuffix();
const nameParts = !walletsOnly && !noNames
? getNameParts(nameSuffix, cleanValue) : undefined;
- const hasExactName = cleanValue
+ const hasExactName = !!cleanValue
&& !walletsOnly
&& !noNames
&& !!nameParts?.name;
@@ -100,6 +101,9 @@
]
: options;
+ // Fetch an address or name hint if possible
+ const pickerHints = usePickerHints(nameHint, cleanValue, hasExactName);
+
const classes = classNames("address-picker", className, {
"address-picker-wallets-only": walletsOnly,
"address-picker-no-names": noNames,
@@ -107,112 +111,109 @@
"address-picker-has-exact-name": hasExactName,
});
- // TODO: Wrap this in a Form.Item in advance? Every place I can think of off
- // the top of my head will be using this in a form, so it might be good
- // to provide some of the validation logic here.
- // - Send Transaction Page (to/from) - Form
- // - Receive Transaction Page (to) - Form
- // - Name Purchase Page (owner) - Form
- // - Name Transfer Page (to) - Form
- // - Mining Page (to) - Possibly a form, can get away with making it one
- return
+ {
- const addressRegexp = getAddressRegexV2(addressPrefix);
+ // Address/name regexp
+ {
+ type: "method",
+ async validator(_, value): Promise {
+ const addressRegexp = getAddressRegexV2(addressPrefix);
- if (walletsOnly || noNames) {
- // Only validate with addresses
- if (!addressRegexp.test(value)) {
- if (walletsOnly) throw t("addressPicker.errorInvalidWalletsOnly");
- else throw t("addressPicker.errorInvalidAddressOnly");
+ if (walletsOnly || noNames) {
+ // Only validate with addresses
+ if (!addressRegexp.test(value)) {
+ if (walletsOnly)
+ throw t("addressPicker.errorInvalidWalletsOnly");
+ else throw t("addressPicker.errorInvalidAddressOnly");
+ }
+ } else {
+ // Validate addresses and names
+ const nameRegexp = getNameRegex(nameSuffix);
+ if (!addressRegexp.test(value) && !nameRegexp.test(value))
+ throw t("addressPicker.errorInvalidRecipient");
}
- } else {
- // Validate addresses and names
- const nameRegexp = getNameRegex(nameSuffix);
- if (!addressRegexp.test(value) && !nameRegexp.test(value))
- throw t("addressPicker.errorInvalidRecipient");
}
- }
- },
+ },
- // If this is walletsOnly, add an additional rule to enforce that the
- // given address is a wallet we actually own
- ...(walletsOnly ? [{
- type: "enum",
- enum: addressList,
- message: t("addressPicker.errorInvalidWalletsOnly")
- } as Rule] : []),
+ // If this is walletsOnly, add an additional rule to enforce that the
+ // given address is a wallet we actually own
+ ...(walletsOnly ? [{
+ type: "enum",
+ enum: addressList,
+ message: t("addressPicker.errorInvalidWalletsOnly")
+ } as Rule] : []),
- // If we have another address picker's value, assert that they are not
- // equal (e.g. to/from in a transaction can't be equal)
- ...(otherPickerValue ? [{
- async validator(_, value): Promise {
- if (value === otherPickerValue)
- throw t("addressPicker.errorEqual");
- }
- } as Rule] : [])
- ]}
+ // If we have another address picker's value, assert that they are not
+ // equal (e.g. to/from in a transaction can't be equal)
+ ...(otherPickerValue ? [{
+ async validator(_, value): Promise {
+ if (value === otherPickerValue)
+ throw t("addressPicker.errorEqual");
+ }
+ } as Rule] : [])
+ ]}
- {...props}
- >
-
+ {
- // Returning false if the option contains children will allow the select
- // to run filterOption for each child of that option group.
- if (option?.options) return false;
- // TODO: Do we want to filter categories here too?
+ // Filter the options based on the input text
+ filterOption={(inputValue, option) => {
+ // Returning false if the option contains children will allow the
+ // select to run filterOption for each child of that option group.
+ if (option?.options) return false;
+ // TODO: Do we want to filter categories here too?
- const address = option!.value?.toUpperCase();
- const walletLabel = option!["data-wallet-label"]?.toUpperCase();
+ const address = option!.value?.toUpperCase();
+ const walletLabel = option!["data-wallet-label"]?.toUpperCase();
- // If we have another address picker's value, hide that option from the
- // list (it will always be a wallet)
- // FIXME: filterOption doesn't get called at all when inputValue is
- // blank, which means this option will still appear until the
- // user actually starts typing.
- if (otherPickerValue?.toUpperCase() === address)
- return false;
+ // If we have another address picker's value, hide that option from
+ // the list (it will always be a wallet)
+ // FIXME: filterOption doesn't get called at all when inputValue is
+ // blank, which means this option will still appear until the
+ // user actually starts typing.
+ if (otherPickerValue?.toUpperCase() === address)
+ return false;
- // Now that we've filtered out the other picker's value, we can allow
- // every other option if there's no input
- if (!inputValue) return true;
+ // Now that we've filtered out the other picker's value, we can allow
+ // every other option if there's no input
+ if (!inputValue) return true;
- const inp = inputValue.toUpperCase();
+ const inp = inputValue.toUpperCase();
- const matchedAddress = address.indexOf(inp) !== -1;
- const matchedLabel = walletLabel?.indexOf(inp) !== -1;
+ const matchedAddress = address.indexOf(inp) !== -1;
+ const matchedLabel = walletLabel?.indexOf(inp) !== -1;
- return matchedAddress || matchedLabel;
- }}
+ return matchedAddress || matchedLabel;
+ }}
- options={fullOptions}
- />
- ;
+ options={fullOptions}
+ />
+
+
+ {/* Show the address/name hints if they are present */}
+ {pickerHints}
+ ;
}
diff --git a/src/components/addresses/picker/NameHint.tsx b/src/components/addresses/picker/NameHint.tsx
new file mode 100644
index 0000000..6aa521b
--- /dev/null
+++ b/src/components/addresses/picker/NameHint.tsx
@@ -0,0 +1,29 @@
+// Copyright (c) 2020-2021 Drew Lemmy
+// This file is part of KristWeb 2 under GPL-3.0.
+// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
+import { Typography } from "antd";
+
+import { useTranslation, Trans } from "react-i18next";
+
+import { KristName } from "@api/types";
+import { ContextualAddress } from "@comp/addresses/ContextualAddress";
+
+const { Text } = Typography;
+
+interface Props {
+ name?: KristName;
+}
+
+export function NameHint({ name }: Props): JSX.Element {
+ const { t } = useTranslation();
+
+ return
+ {name
+ ? (
+
+ Owner:
+
+ )
+ : {t("addressPicker.nameHintNotFound")}}
+ ;
+}
diff --git a/src/components/addresses/picker/PickerHints.tsx b/src/components/addresses/picker/PickerHints.tsx
new file mode 100644
index 0000000..439f411
--- /dev/null
+++ b/src/components/addresses/picker/PickerHints.tsx
@@ -0,0 +1,98 @@
+// Copyright (c) 2020-2021 Drew Lemmy
+// This file is part of KristWeb 2 under GPL-3.0.
+// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
+import { useState, useEffect, useMemo } from "react";
+
+import {
+ isValidAddress, stripNameSuffix,
+ useAddressPrefix, useNameSuffix
+} from "@utils/currency";
+
+import * as api from "@api";
+import { KristAddressWithNames, lookupAddress } from "@api/lookup";
+import { KristName } from "@api/types";
+
+import { AddressHint } from "./AddressHint";
+import { NameHint } from "./NameHint";
+
+import { debounce } from "lodash-es";
+
+import Debug from "debug";
+const debug = Debug("kristweb:address-picker-hints");
+
+const HINT_LOOKUP_DEBOUNCE = 250;
+
+export function usePickerHints(
+ nameHint?: boolean,
+ value?: string,
+ hasExactName?: boolean
+): JSX.Element | null {
+ const addressPrefix = useAddressPrefix();
+ const nameSuffix = useNameSuffix();
+
+ // Handle showing an address or name hint if the value is valid
+ const [foundAddress, setFoundAddress] = useState();
+ const [foundName, setFoundName] = useState();
+
+ const lookupHint = useMemo(() => debounce((
+ nameSuffix: string,
+ value: string,
+ hasAddress?: boolean,
+ hasName?: boolean,
+ nameHint?: boolean
+ ) => {
+ debug("looking up hint for %s (address: %b) (name: %b)",
+ value, hasAddress, hasName);
+
+ if (hasAddress) {
+ // Lookup an address
+ setFoundName(undefined);
+ lookupAddress(value, nameHint)
+ .then(setFoundAddress)
+ .catch(() => setFoundAddress(false));
+ } else if (hasName) {
+ // Lookup a name
+ setFoundAddress(undefined);
+
+ const rawName = stripNameSuffix(nameSuffix, value);
+
+ api.get<{ name: KristName }>("names/" + encodeURIComponent(rawName))
+ .then(res => setFoundName(res.name))
+ .catch(() => setFoundName(false));
+ }
+ }, HINT_LOOKUP_DEBOUNCE), []);
+
+ // Look up the address/name if it is valid (debounced to 250ms)
+ useEffect(() => {
+ if (!value) {
+ setFoundAddress(undefined);
+ setFoundName(undefined);
+ return;
+ }
+
+ // hasExactAddress fails for walletsOnly, so use this variant instead
+ const hasValidAddress = !!value
+ && isValidAddress(addressPrefix, value);
+
+ if (!hasValidAddress && !hasExactName) {
+ setFoundAddress(undefined);
+ setFoundName(undefined);
+ return;
+ }
+
+ // Perform the lookup (debounced)
+ lookupHint(nameSuffix, value, hasValidAddress, hasExactName, nameHint);
+ }, [lookupHint, nameSuffix, value, addressPrefix, hasExactName, nameHint]);
+
+ // Return an address hint if possible
+ if (foundAddress !== undefined) return (
+
+ );
+
+ // Return a name hint if possible
+ if (foundName !== undefined) return (
+
+ );
+
+ return null;
+}
diff --git a/src/components/names/KristNameLink.tsx b/src/components/names/KristNameLink.tsx
index df05013..a5d6130 100644
--- a/src/components/names/KristNameLink.tsx
+++ b/src/components/names/KristNameLink.tsx
@@ -4,11 +4,9 @@
import classNames from "classnames";
import { Typography } from "antd";
-import { useSelector } from "react-redux";
-import { RootState } from "@store";
-
import { Link } from "react-router-dom";
+import { useNameSuffix } from "@utils/currency";
import { useBooleanSetting } from "@utils/settings";
const { Text } = Typography;
@@ -22,7 +20,7 @@
type Props = React.HTMLProps & OwnProps;
export function KristNameLink({ name, text, noLink, neverCopyable, ...props }: Props): JSX.Element | null {
- const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
+ const nameSuffix = useNameSuffix();
const nameCopyButtons = useBooleanSetting("nameCopyButtons");
const copyNameSuffixes = useBooleanSetting("copyNameSuffixes");
diff --git a/src/components/names/NameARecordLink.tsx b/src/components/names/NameARecordLink.tsx
index d7e605d..4f194cb 100644
--- a/src/components/names/NameARecordLink.tsx
+++ b/src/components/names/NameARecordLink.tsx
@@ -3,9 +3,7 @@
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import classNames from "classnames";
-import { useSelector } from "react-redux";
-import { RootState } from "@store";
-import { stripNameSuffix } from "@utils/currency";
+import { useNameSuffix, stripNameSuffix } from "@utils/currency";
import { KristNameLink } from "./KristNameLink";
@@ -23,7 +21,7 @@
}
export function NameARecordLink({ a, className }: Props): JSX.Element | null {
- const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
+ const nameSuffix = useNameSuffix();
if (!a) return null;
diff --git a/src/components/transactions/TransactionConciseMetadata.tsx b/src/components/transactions/TransactionConciseMetadata.tsx
index 6e4b0db..497865c 100644
--- a/src/components/transactions/TransactionConciseMetadata.tsx
+++ b/src/components/transactions/TransactionConciseMetadata.tsx
@@ -3,11 +3,8 @@
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import classNames from "classnames";
-import { useSelector } from "react-redux";
-import { RootState } from "@store";
-
import { KristTransaction } from "@api/types";
-import { stripNameFromMetadata } from "@utils/currency";
+import { useNameSuffix, stripNameFromMetadata } from "@utils/currency";
import "./TransactionConciseMetadata.less";
@@ -23,7 +20,7 @@
* to a specified amount of characters.
*/
export function TransactionConciseMetadata({ transaction, metadata, limit = 30, className }: Props): JSX.Element | null {
- const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
+ const nameSuffix = useNameSuffix();
// Don't render anything if there's no metadata (after the hooks)
const meta = metadata || transaction?.metadata;
diff --git a/src/global/ws/SyncMOTD.tsx b/src/global/ws/SyncMOTD.tsx
index 8331231..0564008 100644
--- a/src/global/ws/SyncMOTD.tsx
+++ b/src/global/ws/SyncMOTD.tsx
@@ -13,6 +13,7 @@
import { KristMOTD, KristMOTDBase } from "@api/types";
import { recalculateWallets, useWallets } from "@wallets";
+import { useAddressPrefix } from "@utils/currency";
import Debug from "debug";
const debug = Debug("kristweb:sync-motd");
@@ -45,7 +46,7 @@
const connectionState = useSelector((s: RootState) => s.websocket.connectionState);
// All these are used to determine if we need to recalculate the addresses
- const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix);
+ const addressPrefix = useAddressPrefix();
const masterPassword = useSelector((s: RootState) => s.masterPassword.masterPassword);
const { wallets } = useWallets();
diff --git a/src/pages/names/NamePage.tsx b/src/pages/names/NamePage.tsx
index 3e251d8..9647bfa 100644
--- a/src/pages/names/NamePage.tsx
+++ b/src/pages/names/NamePage.tsx
@@ -8,9 +8,6 @@
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
-import { useSelector } from "react-redux";
-import { RootState } from "@store";
-
import { PageLayout } from "@layout/PageLayout";
import { APIErrorResult } from "@comp/results/APIErrorResult";
@@ -22,7 +19,9 @@
import * as api from "@api";
import { KristName } from "@api/types";
import { LookupTransactionType as LookupTXType } from "@api/lookup";
+
import { useWallets } from "@wallets";
+import { useNameSuffix } from "@utils/currency";
import { useBooleanSetting } from "@utils/settings";
import { NameButtonRow } from "./NameButtonRow";
@@ -149,7 +148,7 @@
export function NamePage(): JSX.Element {
// Used to refresh the name data on syncNode change
const syncNode = api.useSyncNode();
- const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
+ const nameSuffix = useNameSuffix();
const { name } = useParams();
const [kristName, setKristName] = useState();
diff --git a/src/pages/transactions/TransactionMetadataCard.tsx b/src/pages/transactions/TransactionMetadataCard.tsx
index 3e70ecd..bbb7f5a 100644
--- a/src/pages/transactions/TransactionMetadataCard.tsx
+++ b/src/pages/transactions/TransactionMetadataCard.tsx
@@ -6,10 +6,8 @@
import { useTranslation } from "react-i18next";
-import { useSelector } from "react-redux";
-import { RootState } from "@store";
-
import { parseCommonMeta } from "@utils/commonmeta";
+import { useNameSuffix } from "@utils/currency";
import { HelpIcon } from "@comp/HelpIcon";
import { useBooleanSetting } from "@utils/settings";
@@ -93,7 +91,7 @@
export function TransactionMetadataCard({ metadata }: { metadata: string }): JSX.Element {
const { t } = useTranslation();
- const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
+ const nameSuffix = useNameSuffix();
// Default to the 'Raw' tab instead of 'CommonMeta'
const defaultRaw = useBooleanSetting("transactionDefaultRaw");
diff --git a/src/pages/transactions/send/SendTransactionForm.tsx b/src/pages/transactions/send/SendTransactionForm.tsx
index 82a921e..f42928e 100644
--- a/src/pages/transactions/send/SendTransactionForm.tsx
+++ b/src/pages/transactions/send/SendTransactionForm.tsx
@@ -39,9 +39,9 @@
const [from, setFrom] = useState(initialFrom);
const [to, setTo] = useState("");
- function onValuesChange(changed: Partial) {
- if (changed.from !== undefined) setFrom(changed.from);
- if (changed.to !== undefined) setTo(changed.to);
+ function onValuesChange(_: unknown, values: Partial) {
+ setFrom(values.from || "");
+ setTo(values.to || "");
}
return