diff --git a/.vscode/settings.json b/.vscode/settings.json index 061c562..fafe782 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,6 +30,7 @@ "firstseen", "jwalelset", "languagedetector", + "linkify", "localisation", "metaname", "middot", diff --git a/package.json b/package.json index ae59b10..1a5a01f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-dom": "^17.0.1", "react-hotkeys": "^2.0.0", "react-i18next": "^11.8.8", + "react-linkify": "^1.0.0-alpha", "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", "react-timeago": "^5.2.0", @@ -85,6 +86,7 @@ "@types/node": "^14.14.31", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", + "@types/react-linkify": "^1.0.0", "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "@types/react-timeago": "^4.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ff071a..7694be2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,7 @@ react-dom: 17.0.1_react@17.0.1 react-hotkeys: 2.0.0_react@17.0.1 react-i18next: 11.8.8_i18next@19.9.0+react@17.0.1 + react-linkify: 1.0.0-alpha react-redux: 7.2.2_380dc38591053d98779d1f25fc7202b9 react-router-dom: 5.2.0_react@17.0.1 react-timeago: 5.2.0_react@17.0.1 @@ -44,6 +45,7 @@ '@types/node': 14.14.31 '@types/react': 17.0.2 '@types/react-dom': 17.0.1 + '@types/react-linkify': 1.0.0 '@types/react-redux': 7.1.16 '@types/react-router-dom': 5.1.7 '@types/react-timeago': 4.1.2 @@ -2156,6 +2158,12 @@ dev: true resolution: integrity: sha512-yIVyopxQb8IDZ7SOHeTovurFq+fXiPICa+GV3gp0Xedsl+MwQlMLKmvrnEjFbQxjliH5YVAEWFh975eVNmKj7Q== + /@types/react-linkify/1.0.0: + dependencies: + '@types/react': 17.0.2 + dev: true + resolution: + integrity: sha512-2NKXPQGaHNfh/dCqkVC55k1tAhQyNoNZa31J50nIneMVwHqUI00FAP+Lyp8e0BarPf84kn4GRVAhtWX9XJBzSQ== /@types/react-redux/7.1.16: dependencies: '@types/hoist-non-react-statics': 3.3.1 @@ -8005,6 +8013,12 @@ dev: true resolution: integrity: sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + /linkify-it/2.2.0: + dependencies: + uc.micro: 1.0.6 + dev: false + resolution: + integrity: sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== /load-json-file/2.0.0: dependencies: graceful-fs: 4.2.6 @@ -10894,6 +10908,13 @@ /react-is/17.0.1: resolution: integrity: sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + /react-linkify/1.0.0-alpha: + dependencies: + linkify-it: 2.2.0 + tlds: 1.218.0 + dev: false + resolution: + integrity: sha512-7gcIUvJkAXXttt1fmBK9cwn+1jTa4hbKLGCZ9J1U6EOkyb2/+LKL1Z28d9rtDLMnpvImlNlLPdTPooorl5cpmg== /react-redux/7.2.2_380dc38591053d98779d1f25fc7202b9: dependencies: '@babel/runtime': 7.13.8 @@ -12601,6 +12622,11 @@ dev: false resolution: integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + /tlds/1.218.0: + dev: false + hasBin: true + resolution: + integrity: sha512-JpD3eSrYaIFlU/OvtI5WTEK+v5qXZSeUifz4hT2bJsJKx5ykjZvg6i5yXVBJNjoN3XbTCtryc7H5v8B16yHfMg== /tmpl/1.0.4: dev: true resolution: @@ -12838,6 +12864,10 @@ hasBin: true resolution: integrity: sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== + /uc.micro/1.0.6: + dev: false + resolution: + integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== /unicode-canonical-property-names-ecmascript/1.0.4: dev: true engines: @@ -13740,6 +13770,7 @@ '@types/node': ^14.14.31 '@types/react': ^17.0.2 '@types/react-dom': ^17.0.1 + '@types/react-linkify': ^1.0.0 '@types/react-redux': ^7.1.16 '@types/react-router-dom': ^5.1.7 '@types/react-timeago': ^4.1.2 @@ -13774,6 +13805,7 @@ react-dom: ^17.0.1 react-hotkeys: ^2.0.0 react-i18next: ^11.8.8 + react-linkify: ^1.0.0-alpha react-redux: ^7.2.2 react-refresh: ^0.9.0 react-router-dom: ^5.2.0 diff --git a/public/locales/en.json b/public/locales/en.json index d6be8b5..af93e57 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -250,7 +250,12 @@ "blockDifficultyHashRateTooltip": "Estimated combined network mining hash rate, based on the current work.", "blockDifficultyChartWork": "Block Difficulty", "blockDifficultyChartLinear": "Linear", - "blockDifficultyChartLog": "Logarithmic" + "blockDifficultyChartLog": "Logarithmic", + + "motdCardTitle": "Server MOTD", + "motdDebugMode": "This sync node is an unofficial development server. Balances and transactions can be manipulated. Proceed with caution.", + + "whatsNewCardTitle": "What's New" }, "credits": { diff --git a/src/components/DateTime.less b/src/components/DateTime.less new file mode 100644 index 0000000..4081a0b --- /dev/null +++ b/src/components/DateTime.less @@ -0,0 +1,15 @@ +@import (reference) "../App.less"; + +.date-time { + &-secondary, &-secondary a, &-secondary time { + color: @text-color-secondary; + } + + &-small, &-small a, &-small time { + font-size: 90%; + + @media (max-width: @screen-xl) { + font-size: 85%; + } + } +} diff --git a/src/components/DateTime.tsx b/src/components/DateTime.tsx index f1433e4..218d52c 100644 --- a/src/components/DateTime.tsx +++ b/src/components/DateTime.tsx @@ -6,19 +6,33 @@ import { Tooltip } from "antd"; import dayjs from "dayjs"; +import TimeAgo from "react-timeago"; + +import "./DateTime.less"; interface OwnProps { date?: Date | string | null; + timeAgo?: boolean; + small?: boolean; + secondary?: boolean; } type Props = React.HTMLProps & OwnProps; -export function DateTime({ date, ...props }: Props): JSX.Element | null { +export function DateTime({ date, timeAgo, small, secondary, ...props }: Props): JSX.Element | null { if (!date) return null; const realDate = typeof date === "string" ? new Date(date) : date; + const classes = classNames("date-time", props.className, { + "date-time-timeago": timeAgo, + "date-time-small": small, + "date-time-secondary": secondary + }); + return - - {dayjs(realDate).format("YYYY-MM-DD HH:mm:ss")} + + {timeAgo + ? + : dayjs(realDate).format("YYYY-MM-DD HH:mm:ss")} ; } diff --git a/src/components/transactions/TransactionItem.tsx b/src/components/transactions/TransactionItem.tsx index f5d8f6a..397d058 100644 --- a/src/components/transactions/TransactionItem.tsx +++ b/src/components/transactions/TransactionItem.tsx @@ -5,13 +5,12 @@ import classNames from "classnames"; import { Row, Col, Tooltip, Grid } from "antd"; -import TimeAgo from "react-timeago"; - import { useTranslation, Trans } from "react-i18next"; import { Link } from "react-router-dom"; import { KristTransaction } from "../../krist/api/types"; import { Wallet } from "../../krist/wallets/Wallet"; +import { DateTime } from "../DateTime"; import { KristValue } from "../KristValue"; import { KristNameLink } from "../KristNameLink"; import { ContextualAddress } from "../ContextualAddress"; @@ -103,9 +102,9 @@ {/* Transaction time */} - - - + + + diff --git a/src/components/ws/SyncMOTD.tsx b/src/components/ws/SyncMOTD.tsx index ded78bd..5fe6b80 100644 --- a/src/components/ws/SyncMOTD.tsx +++ b/src/components/ws/SyncMOTD.tsx @@ -10,7 +10,7 @@ import { store } from "../../App"; import * as api from "../../krist/api"; -import { KristMOTD } from "../../krist/api/types"; +import { KristMOTD, KristMOTDBase } from "../../krist/api/types"; import { recalculateWallets, useWallets } from "../../krist/wallets/Wallet"; @@ -24,6 +24,14 @@ debug("motd: %s", data.motd); store.dispatch(nodeActions.setCurrency(data.currency)); store.dispatch(nodeActions.setConstants(data.constants)); + + const motdBase: KristMOTDBase = { + motd: data.motd, + motdSet: new Date(data.motd_set), + debugMode: data.debug_mode, + miningEnabled: data.mining_enabled + }; + store.dispatch(nodeActions.setMOTD(motdBase)); } /** Sync the MOTD with the Krist node on startup. */ diff --git a/src/krist/api/types.ts b/src/krist/api/types.ts index cf9188b..f67eeff 100644 --- a/src/krist/api/types.ts +++ b/src/krist/api/types.ts @@ -89,13 +89,27 @@ currency_name: "Krist", currency_symbol: "KST" }; +export interface KristMOTDBase { + motd: string; + motdSet: Date; + + miningEnabled: boolean; + debugMode: boolean; +} +export const DEFAULT_MOTD_BASE: KristMOTDBase = { + motd: "", + motdSet: new Date(), + miningEnabled: true, + debugMode: false +}; + export interface KristMOTD { motd: string; - set: string; + motd_set: string; public_url: string; mining_enabled: boolean; - debug_enabled: boolean; + debug_mode: boolean; constants: KristConstants; currency: KristCurrency; diff --git a/src/pages/addresses/AddressButtonRow.tsx b/src/pages/addresses/AddressButtonRow.tsx index e7b7c93..5fbf5cc 100644 --- a/src/pages/addresses/AddressButtonRow.tsx +++ b/src/pages/addresses/AddressButtonRow.tsx @@ -23,12 +23,12 @@ {/* Send/transfer Krist button */} {myWallet ? ( - ) : ( - )} @@ -42,7 +42,7 @@ ) : ( - )} diff --git a/src/pages/addresses/AddressPage.less b/src/pages/addresses/AddressPage.less index 38cd8e3..fe54b42 100644 --- a/src/pages/addresses/AddressPage.less +++ b/src/pages/addresses/AddressPage.less @@ -47,15 +47,6 @@ .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/NameItem.tsx b/src/pages/addresses/NameItem.tsx index 4b04f9f..6bc6df3 100644 --- a/src/pages/addresses/NameItem.tsx +++ b/src/pages/addresses/NameItem.tsx @@ -2,15 +2,14 @@ // 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 { Row } 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"; +import { DateTime } from "../../components/DateTime"; export function NameItem({ name }: { name: KristName }): JSX.Element { const { t } = useTranslation(); @@ -29,8 +28,8 @@ {/* Purchase time */} - - - + + + ; } diff --git a/src/pages/dashboard/DashboardPage.less b/src/pages/dashboard/DashboardPage.less index 0d8796f..f60d697 100644 --- a/src/pages/dashboard/DashboardPage.less +++ b/src/pages/dashboard/DashboardPage.less @@ -110,4 +110,14 @@ } } } + + .dashboard-card-motd { + .ant-alert { + margin-bottom: @margin-md; + } + + p { + margin-bottom: 0; + } + } } diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index f0a2e3d..3ea2f14 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -12,6 +12,8 @@ import { TransactionsCard } from "./TransactionsCard"; import { BlockValueCard } from "./BlockValueCard"; import { BlockDifficultyCard } from "./BlockDifficultyCard"; +import { MOTDCard } from "./MOTDCard"; +import { WhatsNewCard } from "./WhatsNewCard"; import "./DashboardPage.less"; @@ -28,5 +30,10 @@ + + + + + ; } diff --git a/src/pages/dashboard/MOTDCard.tsx b/src/pages/dashboard/MOTDCard.tsx new file mode 100644 index 0000000..855d2a5 --- /dev/null +++ b/src/pages/dashboard/MOTDCard.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Card, Alert } from "antd"; + +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; + +import { useTranslation } from "react-i18next"; + +import Linkify from "react-linkify"; +import { DateTime } from "../../components/DateTime"; + +export function MOTDCard(): JSX.Element { + const { t } = useTranslation(); + const { motd, motdSet, debugMode } = useSelector((s: RootState) => s.node.motd); + + return + {debugMode && } + +

+ ( + {text} + )}> + {motd} + +

+ + +
; +} diff --git a/src/pages/dashboard/WhatsNewCard.tsx b/src/pages/dashboard/WhatsNewCard.tsx new file mode 100644 index 0000000..33cd4e9 --- /dev/null +++ b/src/pages/dashboard/WhatsNewCard.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Card } from "antd"; + +import { useTranslation } from "react-i18next"; + +export function WhatsNewCard(): JSX.Element { + const { t } = useTranslation(); + + return + TODO + ; +} diff --git a/src/pages/wallets/WalletsTable.tsx b/src/pages/wallets/WalletsTable.tsx index c5d00d5..e0b562d 100644 --- a/src/pages/wallets/WalletsTable.tsx +++ b/src/pages/wallets/WalletsTable.tsx @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next"; +import { ContextualAddress } from "../../components/ContextualAddress"; import { KristValue } from "../../components/KristValue"; import { DateTime } from "../../components/DateTime"; import { WalletEditButton } from "./WalletEditButton"; @@ -104,6 +105,8 @@ { title: t("myWallets.columnAddress"), dataIndex: "address", key: "address", + + render: address => , sorter: (a, b) => a.address.localeCompare(b.address) }, diff --git a/src/store/actions/NodeActions.ts b/src/store/actions/NodeActions.ts index 8afe8e4..dd11089 100644 --- a/src/store/actions/NodeActions.ts +++ b/src/store/actions/NodeActions.ts @@ -2,7 +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 { createAction } from "typesafe-actions"; -import { KristWorkDetailed, KristCurrency, KristConstants } from "../../krist/api/types"; +import { KristWorkDetailed, KristCurrency, KristConstants, KristMOTDBase } from "../../krist/api/types"; import * as constants from "../constants"; @@ -11,3 +11,4 @@ export const setDetailedWork = createAction(constants.DETAILED_WORK)(); export const setCurrency = createAction(constants.CURRENCY)(); export const setConstants = createAction(constants.CONSTANTS)(); +export const setMOTD = createAction(constants.MOTD)(); diff --git a/src/store/constants.ts b/src/store/constants.ts index 3b5b130..f05ff94 100644 --- a/src/store/constants.ts +++ b/src/store/constants.ts @@ -31,3 +31,4 @@ export const DETAILED_WORK = "DETAILED_WORK"; export const CURRENCY = "CURRENCY"; export const CONSTANTS = "CONSTANTS"; +export const MOTD = "MOTD"; diff --git a/src/store/reducers/NodeReducer.ts b/src/store/reducers/NodeReducer.ts index ec7d233..e7461d4 100644 --- a/src/store/reducers/NodeReducer.ts +++ b/src/store/reducers/NodeReducer.ts @@ -2,8 +2,8 @@ // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import { createReducer, ActionType } from "typesafe-actions"; -import { KristWorkDetailed, KristCurrency, DEFAULT_CURRENCY, KristConstants, DEFAULT_CONSTANTS } from "../../krist/api/types"; -import { setSyncNode, setLastBlockID, setDetailedWork, setCurrency, setConstants } from "../actions/NodeActions"; +import { KristWorkDetailed, KristCurrency, DEFAULT_CURRENCY, KristConstants, DEFAULT_CONSTANTS, KristMOTDBase, DEFAULT_MOTD_BASE } from "../../krist/api/types"; +import { setSyncNode, setLastBlockID, setDetailedWork, setCurrency, setConstants, setMOTD } from "../actions/NodeActions"; import packageJson from "../../../package.json"; @@ -13,6 +13,7 @@ readonly syncNode: string; readonly currency: KristCurrency; readonly constants: KristConstants; + readonly motd: KristMOTDBase; } export function getInitialNodeState(): State { @@ -20,7 +21,8 @@ lastBlockID: 0, syncNode: localStorage.getItem("syncNode") || packageJson.defaultSyncNode, currency: DEFAULT_CURRENCY, - constants: DEFAULT_CONSTANTS + constants: DEFAULT_CONSTANTS, + motd: DEFAULT_MOTD_BASE }; } @@ -44,4 +46,8 @@ .handleAction(setConstants, (state: State, action: ActionType) => ({ ...state, constants: action.payload + })) + .handleAction(setMOTD, (state: State, action: ActionType) => ({ + ...state, + motd: action.payload }));