Newer
Older
CrypticOreWallet / src / components / addresses / picker / AddressPicker.tsx
@Drew Lemmy Drew Lemmy on 10 Mar 2021 4 KB feat: add names to address picker
// 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 React, { useState, useMemo } from "react";
import classNames from "classnames";
import { AutoComplete } from "antd";

import { useTranslation } from "react-i18next";

import { useSelector } from "react-redux";
import { RootState } from "@store";

import { useWallets } from "@wallets";
import { isValidAddress, getNameParts } from "@utils/currency";

import { getCategoryHeader } from "./Header";
import { getAddressItem } from "./Item";
import { getOptions } from "./options";

import "./AddressPicker.less";

interface Props {
  walletsOnly?: boolean;
  className?: string;
}

export function AddressPicker({ walletsOnly, className }: Props): JSX.Element {
  const { t } = useTranslation();

  const [value, setValue] = useState<string | undefined>("");
  const cleanValue = value?.toLowerCase().trim();

  // Note that the address picker's options are memoised against the wallets
  // (and soon the address book too), but to save on time and expense, the
  // 'exact address' match is prepended to these options dynamically.
  const { wallets, addressList } = useWallets();
  const options = useMemo(() => getOptions(t, wallets), [t, wallets]);

  // Check if the input text is an exact address. If it is, create an extra item
  // 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
    && !walletsOnly
    && isValidAddress(addressPrefix, cleanValue)
    && !addressList.includes(cleanValue);
  const exactAddressItem = hasExactAddress
    ? {
      ...getCategoryHeader(t("addressPicker.categoryExactAddress")),
      options: [getAddressItem({ address: cleanValue })]
    }
    : undefined;

  // 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 nameParts = !walletsOnly ? getNameParts(nameSuffix, cleanValue) : undefined;
  const hasExactName = cleanValue && !walletsOnly && !!nameParts?.name;
  const exactNameItem = hasExactName
    ? {
      ...getCategoryHeader(t("addressPicker.categoryExactName")),
      options: [getAddressItem({ name: nameParts })]
    }
    : undefined;

  // Shallow copy the options if we need to prepend anything, otherwise use the
  // original memoised array. Prepend the exact address or exact name if they
  // are available.
  const fullOptions = hasExactAddress || hasExactName
    ? [
      ...(exactAddressItem ? [exactAddressItem] : []),
      ...(exactNameItem ? [exactNameItem] : []),
      ...options
    ]
    : options;

  const classes = classNames("address-picker", className, {
    "address-picker-wallets-only": walletsOnly,
    "address-picker-has-exact-address": hasExactAddress,
    "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 <AutoComplete
    className={classes}
    dropdownClassName="address-picker-dropdown"

    // Change the placeholder to 'Choose a wallet' if this is for wallets only
    placeholder={walletsOnly
      ? t("addressPicker.placeholderWalletsOnly")
      : t("addressPicker.placeholder")}

    // Show a clear button on the input for convenience
    allowClear

    // 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 || !inputValue) return false;
      // TODO: Do we want to filter categories here too?

      const inp = inputValue.toUpperCase();

      const address = option!.value;
      const walletLabel = option!["data-wallet-label"];

      const matchedAddress = address.toUpperCase().indexOf(inp) !== -1;
      const matchedLabel = walletLabel && walletLabel.toUpperCase().indexOf(inp) !== -1;

      return matchedAddress || matchedLabel;
    }}

    options={fullOptions}

    onChange={setValue}
    value={value}

    // TODO: remove this
    style={{ minWidth: 300 }}
  />;
}