diff --git a/public/locales/en.json b/public/locales/en.json index 2e52d70..1128999 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -329,6 +329,11 @@ "cardNamesTitle": "Names", "transactionsError": "There was an error fetching the transactions. See the console for details.", + "namesError": "There was an error fetching the names. See the console for details.", + + "namePurchased": "Purchased <1 />", + "nameReceived": "Received <1 />", + "namesSeeMore": "See all {{count, number}}...", "resultInvalidTitle": "Invalid address", "resultInvalid": "That does not look like a valid Krist address.", diff --git a/src/components/transactions/TransactionItem.tsx b/src/components/transactions/TransactionItem.tsx index 25f1518..f5d8f6a 100644 --- a/src/components/transactions/TransactionItem.tsx +++ b/src/components/transactions/TransactionItem.tsx @@ -2,6 +2,7 @@ // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import React from "react"; +import classNames from "classnames"; import { Row, Col, Tooltip, Grid } from "antd"; import TimeAgo from "react-timeago"; @@ -88,7 +89,11 @@ const hideNameAddress = !bps.xl; - return + const classes = classNames("card-list-item", "transaction-summary-item", { + "new": isNew + }); + + return {/* Transaction type and link to transaction */} diff --git a/src/krist/api/lookup.ts b/src/krist/api/lookup.ts index c006236..8e59f13 100644 --- a/src/krist/api/lookup.ts +++ b/src/krist/api/lookup.ts @@ -1,9 +1,12 @@ // 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 { KristAddress, KristTransaction } from "./types"; +import { KristAddress, KristTransaction, KristName } from "./types"; import * as api from "."; +// ============================================================================= +// Addresses +// ============================================================================= interface LookupAddressesResponse { found: number; notFound: number; @@ -46,6 +49,9 @@ return kristAddress; } +// ============================================================================= +// Transactions +// ============================================================================= interface LookupTransactionsOptions { includeMined?: boolean; limit?: number; @@ -76,3 +82,35 @@ + "?" + qs ); } + +// ============================================================================= +// Names +// ============================================================================= +interface LookupNamesOptions { + limit?: number; + offset?: number; + orderBy?: "name" | "owner" | "original_owner" | "registered" | "updated" | "a" | "unpaid"; + order?: "ASC" | "DESC"; +} + +export interface LookupNamesResponse { + count: number; + total: number; + names: KristName[]; +} + +export async function lookupNames(addresses: string[], opts: LookupNamesOptions): Promise { + if (!addresses || addresses.length === 0) return { count: 0, total: 0, names: [] }; + + const qs = new URLSearchParams(); + if (opts.limit) qs.append("limit", opts.limit.toString()); + if (opts.offset) qs.append("offset", opts.offset.toString()); + if (opts.orderBy) qs.append("orderBy", opts.orderBy); + if (opts.order) qs.append("order", opts.order); + + return await api.get( + "lookup/names/" + + encodeURIComponent(addresses.join(",")) + + "?" + qs + ); +} diff --git a/src/krist/api/types.ts b/src/krist/api/types.ts index 2fccabf..cf9188b 100644 --- a/src/krist/api/types.ts +++ b/src/krist/api/types.ts @@ -38,9 +38,11 @@ export interface KristName { name: string; owner: string; + original_owner?: string; registered: string; updated?: string | null; a?: string | null; + unpaid: number; } export interface KristWorkDetailed { diff --git a/src/layout/nav/Search.tsx b/src/layout/nav/Search.tsx index b320223..141af72 100644 --- a/src/layout/nav/Search.tsx +++ b/src/layout/nav/Search.tsx @@ -347,7 +347,6 @@ keyMap={{ SEARCH: ["command+k", "ctrl+k"] }} handlers={{ SEARCH: e => { - console.log(e); e?.preventDefault(); autocompleteRef.current?.focus(); } diff --git a/src/pages/addresses/AddressButtonRow.tsx b/src/pages/addresses/AddressButtonRow.tsx new file mode 100644 index 0000000..3737635 --- /dev/null +++ b/src/pages/addresses/AddressButtonRow.tsx @@ -0,0 +1,49 @@ +// 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 from "react"; +import { Button } from "antd"; +import { SendOutlined, SwapOutlined, UserAddOutlined, EditOutlined } from "@ant-design/icons"; + +import { useTranslation } from "react-i18next"; + +import { KristAddressWithNames } from "../../krist/api/lookup"; +import { useWallets } from "../../krist/wallets/Wallet"; +import { WalletEditButton } from "../wallets/WalletEditButton"; + +export function AddressButtonRow({ address }: { address: KristAddressWithNames }): JSX.Element { + const { t } = useTranslation(); + const { wallets } = useWallets(); + + const myWallet = Object.values(wallets) + .find(w => w.address === address.address); + + return <> + {/* Send/transfer Krist button */} + {myWallet + ? ( + + ) + : ( + + )} + + {/* Add friend/edit wallet button */} + {/* TODO: Change this to edit if they're already a friend */} + {myWallet + ? ( + + + + ) + : ( + + )} + ; +} diff --git a/src/pages/addresses/AddressNamesCard.tsx b/src/pages/addresses/AddressNamesCard.tsx new file mode 100644 index 0000000..cac9a62 --- /dev/null +++ b/src/pages/addresses/AddressNamesCard.tsx @@ -0,0 +1,72 @@ +// 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, useEffect } from "react"; +import classNames from "classnames"; +import { Card, Skeleton, Empty, Row } from "antd"; + +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; + +import { NameItem } from "./NameItem"; +import { lookupNames, LookupNamesResponse } from "../../krist/api/lookup"; + +import { useSyncNode } from "../../krist/api"; + +import { SmallResult } from "../../components/SmallResult"; + +import Debug from "debug"; +const debug = Debug("kristweb:address-names-card"); + +async function fetchNames(address: string): Promise { + debug("fetching names"); + return lookupNames( + [address], + { limit: 5, orderBy: "registered", order: "DESC" } + ); +} + +export function AddressNamesCard({ address }: { address: string }): JSX.Element { + const { t } = useTranslation(); + const syncNode = useSyncNode(); + + const [res, setRes] = useState(); + const [error, setError] = useState(); + const [loading, setLoading] = useState(true); + + // Fetch names on page load or sync node reload + useEffect(() => { + if (!syncNode) return; + + fetchNames(address) + .then(setRes) + .catch(setError) + .finally(() => setLoading(false)); + }, [syncNode, address]); + + const isEmpty = !loading && (error || !res || res.count === 0); + const classes = classNames("kw-card", "address-card-names", { + "empty": isEmpty + }); + + return + + {error + ? + : (res && res.count > 0 + ? <> + {/* Name listing */} + {res.names.map(name => )} + + {/* See more link */} + + + {t("address.namesSeeMore", { count: res.total })} + + + + : + )} + + ; +} diff --git a/src/pages/addresses/AddressPage.less b/src/pages/addresses/AddressPage.less index 07a72e3..e1554ca 100644 --- a/src/pages/addresses/AddressPage.less +++ b/src/pages/addresses/AddressPage.less @@ -28,4 +28,19 @@ max-width: 768px; margin-bottom: @margin-lg; } + + .address-card-names { + .address-name-item { + display: block; + + .name-registered { + color: @text-color-secondary; + font-size: 90%; + + @media (max-width: @screen-xl) { + font-size: 85%; + } + } + } + } } diff --git a/src/pages/addresses/AddressPage.tsx b/src/pages/addresses/AddressPage.tsx index 7297ba8..802a23e 100644 --- a/src/pages/addresses/AddressPage.tsx +++ b/src/pages/addresses/AddressPage.tsx @@ -2,8 +2,7 @@ // 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, useEffect } from "react"; -import { Row, Col, Skeleton, Button } from "antd"; -import { SendOutlined, UserAddOutlined } from "@ant-design/icons"; +import { Row, Col, Skeleton } from "antd"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; @@ -18,7 +17,9 @@ import * as api from "../../krist/api"; import { lookupAddress, KristAddressWithNames } from "../../krist/api/lookup"; +import { AddressButtonRow } from "./AddressButtonRow"; import { AddressTransactionsCard } from "./AddressTransactionsCard"; +import { AddressNamesCard } from "./AddressNamesCard"; import "./AddressPage.less"; @@ -35,18 +36,8 @@ {/* Address */}

{address.address}

- {/* Send Krist button */} - {/* TODO: If this is one of our own wallets then say 'Transfer krist' */} - - - {/* Add friend button */} - {/* TODO: Change this to edit if they're already a friend, and if it is - one of our own wallets then say 'Edit wallet' */} - + {/* Buttons (e.g. Send Krist, Add friend) */} +
{/* Main address info */} @@ -79,10 +70,14 @@ {/* Transaction and name row */} {/* Recent transactions */} - + - + + {/* Names */} + + + ; } diff --git a/src/pages/addresses/AddressTransactionsCard.tsx b/src/pages/addresses/AddressTransactionsCard.tsx index 92f3217..2914713 100644 --- a/src/pages/addresses/AddressTransactionsCard.tsx +++ b/src/pages/addresses/AddressTransactionsCard.tsx @@ -1,7 +1,7 @@ // 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, useEffect, useMemo } from "react"; +import React, { useState, useEffect } from "react"; import classNames from "classnames"; import { Card, Skeleton, Empty } from "antd"; diff --git a/src/pages/addresses/NameItem.tsx b/src/pages/addresses/NameItem.tsx new file mode 100644 index 0000000..4b04f9f --- /dev/null +++ b/src/pages/addresses/NameItem.tsx @@ -0,0 +1,36 @@ +// 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 from "react"; +import { Row, Col, Tooltip, Grid } from "antd"; + +import { useTranslation, Trans } from "react-i18next"; +import { Link } from "react-router-dom"; + +import TimeAgo from "react-timeago"; + +import { KristName } from "../../krist/api/types"; +import { KristNameLink } from "../../components/KristNameLink"; + +export function NameItem({ name }: { name: KristName }): JSX.Element { + const { t } = useTranslation(); + + const nameEl = ; + const nameLink = "/network/names/" + encodeURIComponent(name.name); + const nameTime = new Date(name.registered); + + return +
+ {/* Display 'purchased' if this is the original owner, otherwise display + * 'received'. */} + {name.owner === name.original_owner + ? Purchased {nameEl} + : Received {nameEl}} +
+ + {/* Purchase time */} + + + +
; +} diff --git a/src/pages/wallets/WalletEditButton.tsx b/src/pages/wallets/WalletEditButton.tsx new file mode 100644 index 0000000..a6e9c96 --- /dev/null +++ b/src/pages/wallets/WalletEditButton.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 React, { useState, FC } from "react"; + +import { AuthorisedAction } from "../../components/auth/AuthorisedAction"; +import { AddWalletModal } from "./AddWalletModal"; + +import { Wallet } from "../../krist/wallets/Wallet"; + +interface Props { + wallet: Wallet; +} + +export const WalletEditButton: FC = ({ wallet, children }): JSX.Element => { + const [editWalletVisible, setEditWalletVisible] = useState(false); + + return <> + setEditWalletVisible(true)} + popoverPlacement="left" + > + {children} + + + + ; +}; diff --git a/src/pages/wallets/WalletsTable.tsx b/src/pages/wallets/WalletsTable.tsx index dd4bf7d..c5d00d5 100644 --- a/src/pages/wallets/WalletsTable.tsx +++ b/src/pages/wallets/WalletsTable.tsx @@ -9,7 +9,7 @@ import { KristValue } from "../../components/KristValue"; import { DateTime } from "../../components/DateTime"; -import { AuthorisedAction } from "../../components/auth/AuthorisedAction"; +import { WalletEditButton } from "./WalletEditButton"; import { AddWalletModal } from "./AddWalletModal"; import { Wallet, deleteWallet, useWallets } from "../../krist/wallets/Wallet"; @@ -29,19 +29,14 @@ className="wallet-actions" buttonsRender={([leftButton, rightButton]) => [ - setEditWalletVisible(true)} - popoverPlacement="left" - > + {React.cloneElement(leftButton as React.ReactElement, { className: "ant-btn-left", // force the border-radius disabled: wallet.dontSave })} - , + , rightButton ]}