diff --git a/public/locales/en.json b/public/locales/en.json index f6e942a..21fa581 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -425,7 +425,10 @@ "resultInvalidTitle": "Invalid address", "resultInvalid": "That does not look like a valid Krist address.", "resultNotFoundTitle": "Address not found", - "resultNotFound": "That address has not yet been initialised on the Krist network." + "resultNotFound": "That address has not yet been initialised on the Krist network.", + + "verifiedCardTitle": "Verified address", + "verifiedWebsiteButton": "Visit website" }, "transactionSummary": { diff --git a/src/__data__/verified-addresses.json b/src/__data__/verified-addresses.json new file mode 100644 index 0000000..aa0bad4 --- /dev/null +++ b/src/__data__/verified-addresses.json @@ -0,0 +1,7 @@ +{ + "kqxhx5yn9v": { + "label": "SwitchCraft", + "description": "This address is the master wallet for the SwitchCraft Minecraft server. It represents the balance of all SwitchCraft players.", + "website": "https://switchcraft.pw" + } +} diff --git a/src/components/addresses/ContextualAddress.less b/src/components/addresses/ContextualAddress.less index 256c71a..9fba2c4 100644 --- a/src/components/addresses/ContextualAddress.less +++ b/src/components/addresses/ContextualAddress.less @@ -29,4 +29,25 @@ text-decoration-color: @text-color-secondary; text-decoration-thickness: 1px; } + + .address-verified { + &:not(.address-verified-inactive) a { + color: fade(@kw-orange, 60%); + + .address-verified-label, .kw-verified-check-icon { + color: @kw-orange; + } + } + + .kw-verified-check-icon { + display: inline-block; + font-size: 80%; + margin-left: 0.25em; + + svg { + position: relative; + top: -1px; + } + } + } } diff --git a/src/components/addresses/ContextualAddress.tsx b/src/components/addresses/ContextualAddress.tsx index 6d9653c..a7a18e0 100644 --- a/src/components/addresses/ContextualAddress.tsx +++ b/src/components/addresses/ContextualAddress.tsx @@ -16,6 +16,8 @@ import { KristNameLink } from "../names/KristNameLink"; import { ConditionalLink } from "@comp/ConditionalLink"; +import { getVerified, VerifiedAddress } from "./VerifiedAddress"; + import "./ContextualAddress.less"; const { Text } = Typography; @@ -40,11 +42,39 @@ hideNameAddress: boolean; } -export function AddressMetaname({ nameSuffix, address, commonMeta, source, hideNameAddress }: AddressMetanameProps): JSX.Element { +export function AddressMetaname({ + nameSuffix, + address, + commonMeta, + source, + hideNameAddress +}: AddressMetanameProps): JSX.Element { const rawMetaname = (source ? commonMeta?.return : commonMeta?.recipient) || undefined; const name = (source ? commonMeta?.returnName : commonMeta?.name) || undefined; const nameWithoutSuffix = name ? stripNameSuffix(nameSuffix, name) : undefined; + const verified = getVerified(address); + + function AddressContent() { + return verified + ? ( + // Verified address + + ) + : ( + // Regular address + + + ({address}) + + + ); + } + return name ? <> {/* Display the name/metaname (e.g. foo@bar.kst) */} @@ -56,11 +86,7 @@ {/* Display the original address too */} {!hideNameAddress && <> -   - - ({address}) - - +   } : ( @@ -108,7 +134,9 @@ ? !!commonMeta?.returnRecipient : !!commonMeta?.recipient; - const showTooltip = (hideNameAddress && !!hasMetaname) || !!wallet?.label; + const verified = getVerified(address); + + const showTooltip = !verified && ((hideNameAddress && !!hasMetaname) || !!wallet?.label); const copyable = !neverCopyable && addressCopyButtons ? { text: address } : undefined; @@ -144,15 +172,20 @@ hideNameAddress={!!hideNameAddress} /> ) - : ( - - - + : (verified + // Display the verified address if possible + ? + : ( + // Display the regular address or label + + + + ) ) } diff --git a/src/components/addresses/VerifiedAddress.tsx b/src/components/addresses/VerifiedAddress.tsx new file mode 100644 index 0000000..d325923 --- /dev/null +++ b/src/components/addresses/VerifiedAddress.tsx @@ -0,0 +1,109 @@ +// 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 classNames from "classnames"; +import { Row, Col, Card, Tooltip, Button } from "antd"; +import { GlobalOutlined } from "@ant-design/icons"; + +import { useTranslation } from "react-i18next"; + +import verifiedAddressesJson from "../../__data__/verified-addresses.json"; + +import { ConditionalLink } from "@comp/ConditionalLink"; +import { VerifiedCheck } from "./VerifiedCheck"; + +import Markdown from "markdown-to-jsx"; +import { useMarkdownLink } from "@comp/krist/MarkdownLink"; + +// A verified address is a service that transacts on behalf of its users, or +// holds a balance for its users, and is run by someone we think is trustworthy. + +export interface VerifiedAddress { + label: string; + description?: string; + website?: string; + isActive?: boolean; +} + +export type VerifiedAddresses = Record; +export const verifiedAddresses: VerifiedAddresses = verifiedAddressesJson; + +export const getVerified = (address: string): VerifiedAddress | undefined => + verifiedAddresses[address]; + +interface Props { + address: string; + verified: VerifiedAddress; + parens?: boolean; + className?: string; +} + +export function VerifiedAddress({ + address, + verified, + parens, + className +}: Props): JSX.Element { + const classes = classNames("address-verified", className, { + "address-verified-inactive": verified.isActive === false + }); + + return + + + {parens && <>(} + + {verified.label} + + + {parens && <>)} + + + ; +} + +export function VerifiedDescription({ + verified +}: { verified: VerifiedAddress }): JSX.Element { + const { t } = useTranslation(); + + // Make relative links start with the sync node, and override all links to + // open in a new tab + const MarkdownLink = useMarkdownLink(); + + return + + + {/* Description (markdown) */} + {verified.description &&

+ + {verified.description} + +

} + + {/* Website button */} + {verified.website && } +
+ +
; +} diff --git a/src/components/addresses/VerifiedCheck.tsx b/src/components/addresses/VerifiedCheck.tsx new file mode 100644 index 0000000..3189e16 --- /dev/null +++ b/src/components/addresses/VerifiedCheck.tsx @@ -0,0 +1,17 @@ +// 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 classNames from "classnames"; +import Icon from "@ant-design/icons"; + +export const VerifiedCheckSvg = (): JSX.Element => ( + + + +); +export const VerifiedCheck = ({ className, ...props }: any): JSX.Element => + ; diff --git a/src/pages/addresses/AddressPage.less b/src/pages/addresses/AddressPage.less index adbdfbd..4129fab 100644 --- a/src/pages/addresses/AddressPage.less +++ b/src/pages/addresses/AddressPage.less @@ -46,6 +46,10 @@ } } + .address-verified-description-row { + margin-top: @margin-lg; + } + .address-info-row { max-width: 768px; margin-bottom: @margin-lg; diff --git a/src/pages/addresses/AddressPage.tsx b/src/pages/addresses/AddressPage.tsx index 5bb1cbd..ffcf126 100644 --- a/src/pages/addresses/AddressPage.tsx +++ b/src/pages/addresses/AddressPage.tsx @@ -24,6 +24,8 @@ import { AddressTransactionsCard } from "./AddressTransactionsCard"; import { AddressNamesCard } from "./AddressNamesCard"; +import { getVerified, VerifiedDescription } from "@comp/addresses/VerifiedAddress"; + import "./AddressPage.less"; const { Text } = Typography; @@ -43,6 +45,9 @@ const myWallet = Object.values(wallets) .find(w => w.address === address.address); + const showWalletTags = myWallet && (myWallet.label || myWallet.category); + + const verified = getVerified(address.address); return <> {/* Address and buttons */} @@ -56,21 +61,35 @@ - {/* Wallet tags (if applicable) */} - {myWallet && (myWallet.label || myWallet.category) && ( + {/* Wallet/verified tags (if applicable) */} + {(showWalletTags || verified) && ( - {myWallet.label && + {/* Verified label */} + {verified?.label && + + {verified.label} + + } + + {/* Label */} + {myWallet?.label && {t("address.walletLabel")} {myWallet.label} } - {myWallet.category && + {/* Category */} + {myWallet?.category && {t("address.walletCategory")} {myWallet.category} } )} + {/* Verified description/website */} + {(verified?.description || verified?.website) && ( + + )} + {/* Main address info */} {/* Current balance */} diff --git a/src/style/components.less b/src/style/components.less index 7e65fee..d494109 100644 --- a/src/style/components.less +++ b/src/style/components.less @@ -248,3 +248,12 @@ } } } + +// Correct some commonly used tag colours +.ant-tag { + &.ant-tag-orange { + color: lighten(saturate(@kw-orange, 40%), 7%); + background: fade(darken(@kw-orange, 25%), 25%); + border-color: @alert-warning-border-color; + } +}