// 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, useRef } from "react"; import { isValidAddress, stripNameSuffix, useAddressPrefix, useNameSuffix } from "@utils/currency"; import { useWallets } from "@wallets"; import * as api from "@api"; import { KristAddressWithNames, lookupAddress } from "@api/lookup"; import { KristName } from "@api/types"; import { WalletHint } from "./WalletHint"; import { AddressHint } from "./AddressHint"; import { NameHint } from "./NameHint"; import { useSubscription } from "@global/ws/WebsocketSubscription"; 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 { // Used for clean-up const isMounted = useRef(true); const addressPrefix = useAddressPrefix(); const nameSuffix = useNameSuffix(); // Handle showing an address or name hint if the value is valid const [foundAddress, setFoundAddress] = useState<KristAddressWithNames | false | undefined>(); const [foundName, setFoundName] = useState<KristName | false | undefined>(); // To auto-refresh address balances, we need to subscribe to the address. // This is the address to subscribe to: const [validAddress, setValidAddress] = useState<string>(); const lastTransactionID = useSubscription({ address: validAddress }); // Used to show a wallet hint const { walletAddressMap, joinedAddressList } = useWallets(); const foundWallet = validAddress && value ? walletAddressMap[validAddress] : undefined; // The actual lookup function (debounced) const lookupHint = useMemo(() => debounce(async ( nameSuffix: string, value: string, hasAddress?: boolean, hasName?: boolean, nameHint?: boolean ) => { // Skip doing anything when unmounted to avoid illegal state updates if (!isMounted.current) return debug("unmounted skipped lookupHint"); debug("looking up hint for %s (address: %b) (name: %b)", value, hasAddress, hasName); if (hasAddress) { // Lookup an address setFoundName(undefined); try { const address = await lookupAddress(value, nameHint); if (!isMounted.current) return debug("unmounted skipped lookupHint hasAddress try"); setFoundAddress(address); } catch (ignored) { if (!isMounted.current) return debug("unmounted skipped lookupHint hasAddress catch"); setFoundAddress(false); } } else if (hasName) { // Lookup a name setFoundAddress(undefined); try { const rawName = stripNameSuffix(nameSuffix, value); const res = await api.get<{ name: KristName }>( "names/" + encodeURIComponent(rawName) ); if (!isMounted.current) return debug("unmounted skipped lookupHint hasName try"); setFoundName(res.name); } catch (ignored) { if (!isMounted.current) return debug("unmounted skipped lookupHint hasName catch"); setFoundName(false); } } }, HINT_LOOKUP_DEBOUNCE), []); // Look up the address/name if it is valid (debounced to 250ms) useEffect(() => { // Skip doing anything when unmounted to avoid illegal state updates if (!isMounted.current) debug("unmounted skipped lookup 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; } // Update the subscription if necessary if (hasValidAddress && validAddress !== value) { debug("updating valid address from %s to %s", validAddress, value); setValidAddress(value); } // Perform the lookup (debounced) lookupHint(nameSuffix, value, hasValidAddress, hasExactName, nameHint); }, [ lookupHint, nameSuffix, value, addressPrefix, hasExactName, nameHint, validAddress, lastTransactionID, joinedAddressList ]); // Clean up the debounced function when unmounting useEffect(() => { return () => { debug("unmounting address picker hint"); isMounted.current = false; lookupHint?.cancel(); }; }, [lookupHint]); // Whether or not to show a separator between the wallet hint and address or // name hint (i.e. if two hints are shown) const showSep = !!foundWallet && (!!foundAddress || !!foundName); const foundAnything = !!foundWallet || !!foundAddress || !!foundName; if (foundAnything) return <div className="address-picker-hints"> {/* Show a wallet hint if possible */} {foundWallet && <WalletHint wallet={foundWallet} />} {/* Show a separator if there are two hints */} {showSep && <span className="address-picker-separator">–</span>} {/* Show an address hint if possible */} {foundAddress !== undefined && ( <AddressHint address={foundAddress || undefined} nameHint={nameHint} /> )} {/* Show a name hint if possible */} {foundName !== undefined && <NameHint name={foundName || undefined} />} </div>; return null; }