Newer
Older
CrypticOreWallet / src / pages / names / NamePage.tsx
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of TenebraWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/TenebraWeb2/blob/master/LICENSE.txt
import { useState, useEffect } from "react";
import { Row, Col, Skeleton, Typography, Tooltip } from "antd";
import { EditOutlined } from "@ant-design/icons";

import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";

import { PageLayout } from "@layout/PageLayout";
import { APIErrorResult } from "@comp/results/APIErrorResult";

import { Statistic } from "@comp/Statistic";
import { ContextualAddress } from "@comp/addresses/ContextualAddress";
import { DateTime } from "@comp/DateTime";
import { NameARecordLink } from "@comp/names/NameARecordLink";

import * as api from "@api";
import { TenebraName } from "@api/types";
import { LookupTransactionType as LookupTXType } from "@api/lookup";

import { useWallets } from "@wallets";
import { useNameSuffix } from "@utils/tenebra";
import { useSubscription } from "@global/ws/WebsocketSubscription";
import { useBooleanSetting } from "@utils/settings";

import { NameButtonRow } from "./NameButtonRow";
import { NameTransactionsCard } from "./NameTransactionsCard";

import { useAuth } from "@comp/auth";
import { useNameEditModal } from "./mgmt/NameEditModalLink";
import { useSendTransactionModal } from "@comp/transactions/SendTransactionModalLink";

import "./NamePage.less";

const { Text } = Typography;

interface ParamTypes {
  name: string;
}

interface PageContentsProps {
  name: TenebraName;
  nameWithSuffix: string;
  lastTransactionID: number;
}

function PageContents({
  name,
  nameWithSuffix,
  lastTransactionID
}: PageContentsProps): JSX.Element {
  const { t } = useTranslation();
  const { wallets } = useWallets();
  const copyNameSuffixes = useBooleanSetting("copyNameSuffixes");

  const myWallet = Object.values(wallets)
    .find(w => w.address === name.owner);

  const nameCopyText = copyNameSuffixes ? nameWithSuffix : name.name;

  const promptAuth = useAuth();
  const [openNameEdit, nameEditModal] = useNameEditModal();
  const [openSendTx, sendTxModal] = useSendTransactionModal();

  return <>
    {/* Name and buttons */}
    <Row className="top-name-row">
      {/* Name */}
      <Text className="name" copyable={{ text: nameCopyText }}>
        {nameWithSuffix}
      </Text>

      {/* Buttons (e.g. Send Tenebra, transfer name, update A Record) */}
      <NameButtonRow
        name={name}
        nameWithSuffix={nameWithSuffix}
        myWallet={!!myWallet}

        openNameEdit={openNameEdit}
        openSendTx={openSendTx}
      />
    </Row>

    {/* Main name info */}
    <Row className="name-info-row">
      {/* Owner */}
      <Col span={24} md={12} lg={8}>
        <Statistic
          titleKey="name.owner"
          value={<ContextualAddress address={name.owner} />}
        />
      </Col>

      {/* Original owner, only if different */}
      {name.original_owner && name.owner !== name.original_owner && (
        <Col span={24} md={12} lg={8}>
          <Statistic
            titleKey="name.originalOwner"
            value={<ContextualAddress address={name.original_owner} />}
          />
        </Col>
      )}

      {/* Registered date */}
      <Col span={24} md={12} lg={8}>
        <Statistic
          titleKey="name.registered"
          value={<DateTime date={name.registered} />}
        />
      </Col>

      {/* Updated date, only if different */}
      {name.updated && name.registered !== name.updated && (
        <Col span={24} md={12} lg={8}>
          <Statistic
            titleKey="name.updated"
            value={<DateTime date={name.updated} />}
          />
        </Col>
      )}

      {/* Unpaid blocks, only if > 0 */}
      {(name.unpaid && name.unpaid > 0) ? (
        <Col span={24} md={12} lg={8}>
          <Statistic
            titleKey="name.unpaid"
            value={t("name.unpaidCount", { count: name.unpaid })}
            green
          />
        </Col>
      ) : <></>}
    </Row>

    {/* A record */}
    {name.a && (
      <Row gutter={16} className="name-a-record-row">
        <Col span={24}>
          <Statistic
            titleKey="name.aRecord"
            titleExtra={myWallet && <>
              <Tooltip title={t("name.aRecordEditTooltip")}>
                <Typography.Link
                  className="name-a-record-edit"
                  onClick={() => promptAuth(false, () =>
                    openNameEdit("update", name.name, name.a))}>
                  <EditOutlined />
                </Typography.Link>
              </Tooltip>
            </>}
            value={<NameARecordLink a={name.a} />}
          />
        </Col>
      </Row>
    )}

    {/* Transactions and name history row */}
    <Row gutter={16} className="name-card-row">
      {/* Recent transactions */}
      <Col span={24} xl={14} xxl={12}>
        <NameTransactionsCard
          name={name.name}
          type={LookupTXType.NAME_TRANSACTIONS}
          lastTransactionID={lastTransactionID}
        />
      </Col>

      {/* Name history */}
      <Col span={24} xl={10} xxl={12}>
        <NameTransactionsCard
          name={name.name}
          type={LookupTXType.NAME_HISTORY}
          lastTransactionID={lastTransactionID}
        />
      </Col>
    </Row>

    {nameEditModal}
    {sendTxModal}
  </>;
}

export function NamePage(): JSX.Element {
  // Used to refresh the name data on syncNode change
  const syncNode = api.useSyncNode();
  const nameSuffix = useNameSuffix();

  const { name } = useParams();
  const [tenebraName, setTenebraName] = useState<TenebraName | undefined>();
  const [error, setError] = useState<Error | undefined>();

  // Used to refresh the cards when a transaction is made to the name
  const lastTransactionID = useSubscription({ name });
  const shouldAutoRefresh = useBooleanSetting("autoRefreshNamePage");
  const usedRefreshID = shouldAutoRefresh ? lastTransactionID : 0;

  // Load the name on page load
  useEffect(() => {
    api.get<{ name: TenebraName }>("names/" + encodeURIComponent(name))
      .then(res => setTenebraName(res.name))
      .catch(err => { console.error(err); setError(err); });
  }, [syncNode, name, usedRefreshID]);

  const nameWithSuffix = tenebraName
    ? `${tenebraName.name}.${nameSuffix}`
    : undefined;

  // Change the page title depending on whether or not the name has loaded
  const title = tenebraName
    ? { siteTitle: nameWithSuffix, subTitle: nameWithSuffix }
    : { siteTitleKey: "name.title" };

  return <PageLayout
    className="name-page"
    titleKey="name.title"
    {...title}
  >
    {error
      ? (
        <APIErrorResult
          error={error}

          invalidParameterTitleKey="name.resultInvalidTitle"
          invalidParameterSubTitleKey="name.resultInvalid"

          notFoundMessage="name_not_found"
          notFoundTitleKey="name.resultNotFoundTitle"
          notFoundSubTitleKey="name.resultNotFound"
        />
      )
      : (tenebraName
        ? (
          <PageContents
            name={tenebraName}
            nameWithSuffix={nameWithSuffix!}
            lastTransactionID={usedRefreshID}
          />
        )
        : <Skeleton active />)}
  </PageLayout>;
}