diff --git a/.eslintrc.json b/.eslintrc.json index d349a11..27eed9e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,9 +28,18 @@ "eol-last": ["error", "always"], "object-shorthand": ["error", "always"], "no-unused-vars": 0, - "tsdoc/syntax": "warn", + "no-lonely-if": "warn", + "no-trailing-spaces": "warn", + "no-whitespace-before-property": "warn", + "space-before-blocks": "warn", + "space-in-parens": ["warn", "never"], + "space-infix-ops": "warn", + "eqeqeq": "warn", + "react/display-name": 0, "react/prop-types": 0, + + "tsdoc/syntax": "warn", "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-module-boundary-types": ["warn", { "allowArgumentsExplicitlyTypedAsAny": true, @@ -48,7 +57,12 @@ "ignoreRestSiblings": true, "argsIgnorePattern": "^_" }], - "@typescript-eslint/no-non-null-assertion": 0 + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/space-before-function-paren": ["warn", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + }] }, "extends": [ "eslint:recommended", diff --git a/public/locales/en.json b/public/locales/en.json index 48925a6..88b3f79 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -23,14 +23,14 @@ "resultTransactionID": "Transaction ID", "resultTransactions": "Transactions", "resultTransactionsAddress": "Search for transactions involving <1 />", - "resultTransactionsAddressResult": "<1>{{count}} transaction involving <3 />", - "resultTransactionsAddressResult_plural": "<1>{{count}} transactions involving <3 />", + "resultTransactionsAddressResult": "<0>{{count, number}} transaction involving <2 />", + "resultTransactionsAddressResult_plural": "<0>{{count, number}} transactions involving <2 />", "resultTransactionsName": "Search for transactions involving <1 />", - "resultTransactionsNameResult": "<1>{{count}} transaction sent to <3 />", - "resultTransactionsNameResult_plural": "<1>{{count}} transactions sent to <3 />", + "resultTransactionsNameResult": "<0>{{count, number}} transaction sent to <2 />", + "resultTransactionsNameResult_plural": "<0>{{count, number}} transactions sent to <2 />", "resultTransactionsMetadata": "Searching for metadata containing <1 />", - "resultTransactionsMetadataResult": "<1>{{count}} transaction with metadata containing <3 />", - "resultTransactionsMetadataResult_plural": "<1>{{count}} transactions with metadata containing <3 />" + "resultTransactionsMetadataResult": "<0>{{count, number}} transaction with metadata containing <2 />", + "resultTransactionsMetadataResult_plural": "<0>{{count, number}} transactions with metadata containing <2 />" }, "send": "Send", diff --git a/src/components/auth/AuthMasterPasswordPopover.tsx b/src/components/auth/AuthMasterPasswordPopover.tsx index 118d124..5f02f6a 100644 --- a/src/components/auth/AuthMasterPasswordPopover.tsx +++ b/src/components/auth/AuthMasterPasswordPopover.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, useRef, FunctionComponent } from "react"; +import React, { useState, useRef, FC } from "react"; import { Popover, Button, Input, Form } from "antd"; import { TooltipPlacement } from "antd/lib/tooltip"; @@ -24,7 +24,7 @@ placement?: TooltipPlacement; } -export const AuthMasterPasswordPopover: FunctionComponent = ({ encrypt, onSubmit, placement, children }) => { +export const AuthMasterPasswordPopover: FC = ({ encrypt, onSubmit, placement, children }) => { const { salt, tester } = useSelector((s: RootState) => s.walletManager, shallowEqual); const { t } = useTranslation(); diff --git a/src/components/auth/AuthorisedAction.tsx b/src/components/auth/AuthorisedAction.tsx index 360966e..bcbcb16 100644 --- a/src/components/auth/AuthorisedAction.tsx +++ b/src/components/auth/AuthorisedAction.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, { FunctionComponent, useState } from "react"; +import React, { FC, useState } from "react"; import { TooltipPlacement } from "antd/lib/tooltip"; import { useSelector, shallowEqual } from "react-redux"; @@ -18,7 +18,7 @@ popoverPlacement?: TooltipPlacement; } -export const AuthorisedAction: FunctionComponent = ({ encrypt, onAuthed, popoverPlacement, children }) => { +export const AuthorisedAction: FC = ({ encrypt, onAuthed, popoverPlacement, children }) => { const { isAuthed, hasMasterPassword } = useSelector((s: RootState) => s.walletManager, shallowEqual); diff --git a/src/layout/PageLayout.tsx b/src/layout/PageLayout.tsx index 9c94978..bd36c7d 100644 --- a/src/layout/PageLayout.tsx +++ b/src/layout/PageLayout.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, { FunctionComponent, useEffect } from "react"; +import React, { FC, useEffect } from "react"; import { PageHeader } from "antd"; import { useTranslation } from "react-i18next"; @@ -23,7 +23,7 @@ className?: string; } -export const PageLayout: FunctionComponent = ({ +export const PageLayout: FC = ({ siteTitle, siteTitleKey, title, titleKey, subTitle, subTitleKey, diff --git a/src/layout/nav/Search.tsx b/src/layout/nav/Search.tsx index c3343b4..fb2b1e6 100644 --- a/src/layout/nav/Search.tsx +++ b/src/layout/nav/Search.tsx @@ -149,40 +149,92 @@ const staticResult = (value: string, label: ReactNode) => [{ value, label }]; function renderResults(): { value: string; label: ReactNode }[] { - debug("current state: %b %b %b %b", rateLimitHit, !value.trim(), loading, results); + const cleanQuery = value.trim(); + debug("current state: %b %b %b %b", rateLimitHit, !cleanQuery, loading, results); // Show a warning instead of the results if the rate limit was hit if (rateLimitHit) return staticResult("rateLimitHit", ); // Don't return anything if there's no query at all - if (!value.trim()) return []; - // Loading spinner, only if we don't already have some results - if (loading && !results) return staticResult("loading", ); - // No results placeholder - if (!loading && !results) return staticResult("noResults", ); + if (!cleanQuery) return []; + if (!results) { + // Loading spinner, only if we don't already have some results + if (loading) return staticResult("loading", ); + else return staticResult("noResults", ); + } + + const resultsMatches = results.matches; + + // The list of results to return for the AutoComplete component const options = []; - if (results) { - const { exactAddress, exactName, exactBlock, exactTransaction } = results.matches; + // The 'exact match' results; these are pretty immediate and return + // definitive data + const { exactAddress, exactName, exactBlock, exactTransaction } = resultsMatches; - if (exactAddress) options.push({ - value: "address-" + exactAddress.address, - label: + if (exactAddress) options.push({ + value: "address-" + exactAddress.address, + label: + }); + if (exactName) options.push({ + value: "name-" + exactName.name, + label: + }); + if (exactBlock) options.push({ + value: "block-" + exactBlock.height, + label: + }); + if (exactTransaction) options.push({ + value: "transaction-" + exactTransaction.id, + label: + }); + + // The 'extended' results; these are counts of transactions and may take a + // bit longer to load. They're only shown if the query is longer than 3 + // characters. + if (cleanQuery.length > 3) { + // Whether or not to show the loading spinner on the extended items. + // This is a pretty poor way to track if the extended results are still + // loading some new value. + const extendedLoading = loading && (!extendedResults || extendedResults.query.originalQuery !== cleanQuery); + const extendedMatches = extendedResults?.matches?.transactions; + + // Do our own checks to preemptively know what kind of transaction results + // will be shown. Note that metadata will always be searched. + const addressInvolved = extendedMatches?.addressInvolved; + const showAddress = (addressInvolved !== false && addressInvolved !== undefined) + && exactAddress; // We definitely know the address exists + + const nameInvolved = extendedMatches?.nameInvolved; + const showName = (nameInvolved !== false && nameInvolved !== undefined) + && exactName; // We definitely know the name exists + + if (showAddress) options.push({ + value: "transactions-address-" + value, + label: }); - if (exactName) options.push({ - value: "name-" + exactName.name, - label: + if (showName) options.push({ + value: "transactions-name-" + value, + label: }); - if (exactBlock) options.push({ - value: "block-" + exactBlock.height, - label: - }); - - if (exactTransaction) options.push({ - value: "transaction-" + exactTransaction.id, - label: + // Metadata is always searched + options.push({ + value: "transactions-metadata-" + value, + label: }); } @@ -193,6 +245,7 @@ } />; } + +interface ExtendedMatchProps { + loading?: boolean; + count?: number; + query?: ReactNode; + + loadingKey: string; + resultKey: string; +} +type ExtendedMatchBaseProps = Omit & { query: string }; +export function ExtendedMatchBase({ loading, count, query, loadingKey, resultKey }: ExtendedMatchProps): JSX.Element { + const { t } = useTranslation(); + + function Query(): JSX.Element { + return <>{query}; + } + + return
+ {/* Result type (e.g. 'Address', 'Transaction') */} + + {t("nav.search.resultTransactions")} + + + + {loading || typeof count !== "number" + ? <> + + + Placeholder + + + : + {{ count }} placeholder + } + + +
; +} + +export function ExtendedAddressMatch(props: ExtendedMatchBaseProps): JSX.Element { + return {props.query}} + + loadingKey="nav.search.resultTransactionsAddress" + resultKey="nav.search.resultTransactionsAddressResult" + />; +} + +export function ExtendedNameMatch(props: ExtendedMatchBaseProps): JSX.Element { + return } + + loadingKey="nav.search.resultTransactionsName" + resultKey="nav.search.resultTransactionsNameResult" + />; +} + +export function ExtendedMetadataMatch(props: ExtendedMatchBaseProps): JSX.Element { + return '{props.query}'} + + loadingKey="nav.search.resultTransactionsMetadata" + resultKey="nav.search.resultTransactionsMetadataResult" + />; +} diff --git a/src/pages/dashboard/TransactionsCard.tsx b/src/pages/dashboard/TransactionsCard.tsx index 534f3b5..bb1e6bf 100644 --- a/src/pages/dashboard/TransactionsCard.tsx +++ b/src/pages/dashboard/TransactionsCard.tsx @@ -67,7 +67,7 @@ ; } - const isEmpty = !loading && (error || !res || res.count == 0); + const isEmpty = !loading && (error || !res || res.count === 0); return diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index f8e54ec..3252723 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.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, { FunctionComponent } from "react"; +import React, { FC } from "react"; import { Menu } from "antd"; import { BugOutlined, GlobalOutlined } from "@ant-design/icons"; @@ -14,7 +14,7 @@ interface SettingsPageLayoutProps extends PageLayoutProps { pageName?: string; } -export const SettingsPageLayout: FunctionComponent = ({ pageName, children, ...rest }) => { +export const SettingsPageLayout: FC = ({ pageName, children, ...rest }) => { return