Newer
Older
CrypticOreWallet / src / pages / names / mgmt / NamePicker.tsx
@Drew Lemmy Drew Lemmy on 15 Mar 2021 3 KB feat: allow picking multiple names
// 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, Dispatch, SetStateAction } from "react";
import { Select, notification } from "antd";

import { useTranslation, TFunction } from "react-i18next";

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

import { Wallet, WalletAddressMap, useWallets } from "@wallets";
import { KristName } from "@api/types";
import { lookupNames } from "@api/lookup";

import { useNameSuffix } from "@utils/currency";

import { throttle, groupBy } from "lodash-es";

import Debug from "debug";
const debug = Debug("kristweb:name-picker");

const FETCH_THROTTLE = 500;

async function _fetchNames(
  t: TFunction,
  nameSuffix: string,
  wallets: WalletAddressMap,
  setOptions: Dispatch<SetStateAction<JSX.Element[] | null>>
): Promise<void> {
  debug("performing name fetch");

  try {
    // Get the full list of names for the given wallets.
    const addresses = Object.keys(wallets);
    const { names, total } = await lookupNames(addresses, {
      orderBy: "name", order: "ASC",
      limit: 1000 // TODO: support more than 1000
    });

    // Since more than 1000 isn't supported yet, show a warning
    if (total > 1000)
      notification.warning({ message: t("namePicker.warningTotalLimit") });

    // Group the names into OptGroups per wallet.
    const options = Object.entries(groupBy(names, n => n.owner))
      .map(([address, group]) =>
        getNameOptions(nameSuffix, wallets[address], group))

    debug("got names:", names, options);
    setOptions(options);
  } catch (err) {
    setOptions(null);
    notification.error({
      message: t("error"),
      description: t("namePicker.errorLookup")
    });
  }
}

function getNameOptions(
  nameSuffix: string,
  wallet: Wallet,
  names: KristName[]
): JSX.Element {
  const groupLabel = wallet.label || wallet.address;

  // Group by owning wallet
  return <Select.OptGroup key={wallet.address} label={groupLabel}>
    {/* Each individual name */}
    {names.map(name => (
      <Select.Option
        key={name.name}
        value={name.name}
        data-name={name.name + "." + nameSuffix}
      >
        {name.name}.{nameSuffix}
      </Select.Option>
    ))}
  </Select.OptGroup>
}

interface Props {
  multiple?: boolean;
}

export function NamePicker({
  multiple
}: Props): JSX.Element {
  const { t } = useTranslation();

  // Used to fetch the list of available names
  const { walletAddressMap, joinedAddressList } = useWallets();
  const fetchNames = useMemo(() =>
    throttle(_fetchNames, FETCH_THROTTLE, { leading: true }), []);
  const nameSuffix = useNameSuffix();

  // Used for auto-refreshing the names if they happen to update
  const refreshID = useSelector((s: RootState) => s.node.lastOwnNameTransactionID);

  const [nameOptions, setNameOptions] = useState<JSX.Element[] | null>(null);

  // Fetch the name list on mount/when the address list changes, or when one of
  // our wallets receives a name transaction.
  useEffect(() => {
    debug(
      "addressList updated (%s, %s, %d)",
      joinedAddressList, nameSuffix, refreshID
    );

    fetchNames(t, nameSuffix, walletAddressMap, setNameOptions);
  }, [joinedAddressList, nameSuffix, refreshID]);

  // TODO: wrap this in a Form.Item
  return <Select
    showSearch
    placeholder={multiple !== false
      ? t("namePicker.placeholderMultiple")
      : t("namePicker.placeholder")}

    allowClear
    mode={multiple !== false ? "multiple" : undefined}

    loading={!nameOptions}

    style={{ width: 300 }}

    // Filter by name with suffix case insensitively
    filterOption={(input, option) => {
      // Display all groups
      if (option?.options) return false;

      const name = option?.["data-name"] || option?.value;
      if (!name) return false;

      return name.toUpperCase().indexOf(input.toUpperCase()) >= 0;
    }}
  >
    {nameOptions}
  </Select>
}