diff --git a/.vscode/settings.json b/.vscode/settings.json index 6fe1eed..f2b1e2a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "Sider", "Syncable", "Transpiler", + "Websockets", "antd", "anticon", "appendhashes", @@ -23,6 +24,7 @@ "jwalelset", "languagedetector", "localisation", + "middot", "motd", "multiline", "pnpm", diff --git a/package.json b/package.json index aed7805..6fef33e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "url": "https://github.com/tmpim/KristWeb2.git" }, "author": "Lemmmy", - "defaultSyncNode": "https://krist.ceriat.net", + "defaultSyncNode": "http://localhost:8080", "supportURL": "https://donate.lemmmy.pw", "supportersURL": "https://donate.lemmmy.pw/supporters.json", "translateURL": "https://github.com/tmpim/KristWeb2/blob/master/README.md#contributing-translations", diff --git a/public/locales/en.json b/public/locales/en.json index a8cee1e..d4ab497 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -201,6 +201,13 @@ "transactionsCardTitle": "Transactions", "blockValueCardTitle": "Block Value", + "blockValueBaseValue": "Base value (<1>)", + "blockValueBaseValueNames": "{{count}} name", + "blockValueBaseValueNames_plural": "{{count}} names", + "blockValueNextDecrease": "Decreases by <1> in <3>{{count}} block", + "blockValueNextDecrease_plural": "Decreases by <1> in <3>{{count}} blocks", + "blockValueReset": "Resets in <1>{{count}} block", + "blockValueReset_plural": "Resets in <1>{{count}} blocks", "blockDifficultyCardTitle": "Block Difficulty" }, diff --git a/src/App.tsx b/src/App.tsx index 20022f2..1b4c350 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { SyncWallets } from "./components/wallets/SyncWallets"; import { ForcedAuth } from "./components/auth/ForcedAuth"; import { WebsocketService } from "./components/ws/WebsocketService"; +import { SyncWork } from "./components/ws/SyncWork"; export const store = createStore( rootReducer, @@ -38,6 +39,7 @@ {/* Services, etc. */} + diff --git a/src/components/KristValue.less b/src/components/KristValue.less index fd74d99..2fa06bd 100644 --- a/src/components/KristValue.less +++ b/src/components/KristValue.less @@ -24,4 +24,12 @@ content: " "; } } + + &.krist-value-green { + color: @kw-green; + + .anticon svg, .krist-currency-long { + color: fade(@kw-green, 75%); + } + } } diff --git a/src/components/KristValue.tsx b/src/components/KristValue.tsx index 5a7afd1..b9527b6 100644 --- a/src/components/KristValue.tsx +++ b/src/components/KristValue.tsx @@ -8,15 +8,16 @@ value?: number; long?: boolean; hideNullish?: boolean; + green?: boolean; }; type Props = React.HTMLProps & OwnProps; -export const KristValue = ({ value, long, hideNullish, ...props }: Props): JSX.Element | null => +export const KristValue = ({ value, long, hideNullish, green, ...props }: Props): JSX.Element | null => hideNullish && (value === undefined || value === null) ? null : ( - + {(value || 0).toLocaleString()} {long && KST} diff --git a/src/components/ws/SyncWork.tsx b/src/components/ws/SyncWork.tsx new file mode 100644 index 0000000..1cddf52 --- /dev/null +++ b/src/components/ws/SyncWork.tsx @@ -0,0 +1,42 @@ +import { useEffect } from "react"; + +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "../../store"; +import * as nodeActions from "../../store/actions/NodeActions"; + +import { AppDispatch } from "../../App"; +import { APIResponse, KristWorkDetailed } from "../../krist/api/types"; + +import packageJson from "../../../package.json"; + +import Debug from "debug"; +const debug = Debug("kristweb:sync-work"); + +export async function updateDetailedWork(dispatch: AppDispatch): Promise { + const syncNode = packageJson.defaultSyncNode; // TODO: support alt nodes + + debug("updating detailed work"); + + const res = await fetch(syncNode + "/work/detailed"); + if (!res.ok || res.status !== 200) // TODO: handle API errors + throw new Error("error fetching detailed work"); + + const data: APIResponse = await res.json(); + if (!data?.ok) throw new Error("error fetching detailed work"); + + debug("work: %d", data.work); + dispatch(nodeActions.setDetailedWork(data)); +} + +/** Sync the work with the Krist node on startup. */ +export function SyncWork(): JSX.Element | null { + const lastBlockID = useSelector((s: RootState) => s.node.lastBlockID); + const dispatch = useDispatch(); + + useEffect(() => { + // TODO: show errors to the user? + updateDetailedWork(dispatch).catch(console.error); + }, [lastBlockID]); + + return null; +} diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx index ce3affd..3063669 100644 --- a/src/components/ws/WebsocketService.tsx +++ b/src/components/ws/WebsocketService.tsx @@ -1,14 +1,15 @@ -import React, { useMemo, useEffect } from "react"; +import { useMemo, useEffect } from "react"; import { useSelector, shallowEqual, useDispatch } from "react-redux"; import { AppDispatch } from "../../App"; import { RootState } from "../../store"; import { WalletMap } from "../../store/reducers/WalletsReducer"; import * as wsActions from "../../store/actions/WebsocketActions"; +import * as nodeActions from "../../store/actions/NodeActions"; import packageJson from "../../../package.json"; -import { APIResponse, KristAddress, KristTransaction, WSConnectionState, WSIncomingMessage, WSSubscriptionLevel } from "../../krist/api/types"; +import { APIResponse, KristAddress, KristBlock, KristTransaction, WSConnectionState, WSIncomingMessage, WSSubscriptionLevel } from "../../krist/api/types"; import { findWalletByAddress, syncWalletUpdate } from "../../krist/wallets/Wallet"; import WebSocketAsPromised from "websocket-as-promised"; @@ -159,6 +160,16 @@ break; } + case "block": { + // Update the last block ID, which will trigger a re-fetch for + // work-related and block value-related components. + const block = data.block as KristBlock; + debug("block id now %d", block.height); + + this.dispatch(nodeActions.setLastBlockID(block.height)); + + break; + } } } } diff --git a/src/krist/api/types.ts b/src/krist/api/types.ts index 19dc3e0..db24302 100644 --- a/src/krist/api/types.ts +++ b/src/krist/api/types.ts @@ -20,6 +20,30 @@ type: KristTransactionType; } +export interface KristBlock { + height: number; + address: string; + hash: string; + short_hash: string; + value: number; + difficulty: number; + time: string; +} + +export interface KristWorkDetailed { + work: number; + unpaid: number; + + base_value: number; + block_value: number; + + decrease: { + value: number; + blocks: number; + reset: number; + }; +} + export type APIResponse> = T & { ok: boolean; error?: string; diff --git a/src/pages/dashboard/BlockValueCard.tsx b/src/pages/dashboard/BlockValueCard.tsx index 1556906..dc9061c 100644 --- a/src/pages/dashboard/BlockValueCard.tsx +++ b/src/pages/dashboard/BlockValueCard.tsx @@ -1,12 +1,70 @@ import React from "react"; -import { Card } from "antd"; +import { Card, Skeleton, Typography, Progress } from "antd"; -import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; + +import { useTranslation, Trans } from "react-i18next"; +import { Link } from "react-router-dom"; + +import { KristValue } from "../../components/KristValue"; + +const { Text } = Typography; export function BlockValueCard(): JSX.Element { const { t } = useTranslation(); - return + const work = useSelector((s: RootState) => s.node.detailedWork); + const hasNames = (work?.unpaid || 0) > 0; + return + + {work && <> + {/* Main block value */} + + + {hasNames && <> + {/* Base value + names */} +
+ + Base value () + +  +  + + {t("dashboard.blockValueBaseValueNames", { count: work.unpaid })} + +
+ + {/* Progress bar */} + + + {/* Decrease and reset */} +
+ {/* Decrease */} + {work.decrease.blocks !== work.decrease.reset && <> + + Decreases by + + in + {{ count: work.decrease.blocks }} + + + · + } + + {/* Reset */} + + Resets in + {{ count: work.decrease.reset }} + +
+ } + } +
; } diff --git a/src/pages/dashboard/DashboardPage.less b/src/pages/dashboard/DashboardPage.less index d88e5dd..20d8f09 100644 --- a/src/pages/dashboard/DashboardPage.less +++ b/src/pages/dashboard/DashboardPage.less @@ -44,24 +44,6 @@ margin-bottom: @margin-sm; } - .dashboard-wallets-balance { - .krist-value { - color: @kw-green; - - .anticon svg, .krist-currency-long { - color: fade(@kw-green, 75%); - } - - &.empty { - color: @text-color; - - .anticon svg, .krist-currency-long { - color: fade(@text-color, 75%); - } - } - } - } - .dashboard-wallet-preview { padding: @padding-sm @padding-md; margin-bottom: @margin-xs; @@ -102,6 +84,25 @@ } } + .dashboard-card-block-value { + .dashboard-block-value-main.krist-value { + font-size: @heading-4-size; + } + + .dashboard-block-value-next-decrease { + margin: @margin-sm 0; + } + + .dashboard-block-value-progress-text { + color: @text-color-secondary; + font-size: @font-size-sm; + + .dashboard-block-value-progress-middot { + margin: 0 @margin-xs; + } + } + } + .dashboard-more a { display: block; width: 100%; diff --git a/src/pages/dashboard/WalletOverviewCard.tsx b/src/pages/dashboard/WalletOverviewCard.tsx index 2a9b173..8ae2bb0 100644 --- a/src/pages/dashboard/WalletOverviewCard.tsx +++ b/src/pages/dashboard/WalletOverviewCard.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Card, Row, Col, Typography } from "antd"; +import { Card, Row, Col } from "antd"; import { useSelector, shallowEqual } from "react-redux"; import { RootState } from "../../store"; @@ -48,7 +48,7 @@ 0 ? "" : "empty"} />} + value={ 0} />} /> diff --git a/src/store/actions/NodeActions.ts b/src/store/actions/NodeActions.ts new file mode 100644 index 0000000..865f7ff --- /dev/null +++ b/src/store/actions/NodeActions.ts @@ -0,0 +1,7 @@ +import { createAction } from "typesafe-actions"; +import { KristWorkDetailed } from "../../krist/api/types"; + +import * as constants from "../constants"; + +export const setLastBlockID = createAction(constants.LAST_BLOCK_ID)(); +export const setDetailedWork = createAction(constants.DETAILED_WORK)(); diff --git a/src/store/actions/index.ts b/src/store/actions/index.ts index 16d89e2..18d0f62 100644 --- a/src/store/actions/index.ts +++ b/src/store/actions/index.ts @@ -2,11 +2,13 @@ import * as walletsActions from "./WalletsActions"; import * as settingsActions from "./SettingsActions"; import * as websocketActions from "./WebsocketActions"; +import * as nodeActions from "./NodeActions"; const RootAction = { walletManager: walletManagerActions, wallets: walletsActions, settings: settingsActions, - websocket: websocketActions + websocket: websocketActions, + node: nodeActions }; export default RootAction; diff --git a/src/store/constants.ts b/src/store/constants.ts index 59b31e5..6f54c4a 100644 --- a/src/store/constants.ts +++ b/src/store/constants.ts @@ -19,3 +19,7 @@ // Websockets // --- export const CONNECTION_STATE = "CONNECTION_STATE"; + +// Node state +export const LAST_BLOCK_ID = "LAST_BLOCK_ID"; +export const DETAILED_WORK = "DETAILED_WORK"; diff --git a/src/store/reducers/NodeReducer.ts b/src/store/reducers/NodeReducer.ts new file mode 100644 index 0000000..a2d9249 --- /dev/null +++ b/src/store/reducers/NodeReducer.ts @@ -0,0 +1,23 @@ +import { createReducer, ActionType } from "typesafe-actions"; +import { KristWorkDetailed } from "../../krist/api/types"; +import { setLastBlockID, setDetailedWork } from "../actions/NodeActions"; + +export interface State { + readonly lastBlockID: number; + readonly detailedWork?: KristWorkDetailed; +} + +export const initialState: State = { + lastBlockID: 0 +}; + +export const NodeReducer = createReducer(initialState) + .handleAction(setLastBlockID, (state: State, action: ActionType) => ({ + ...state, + lastBlockID: action.payload + })) + .handleAction(setDetailedWork, (state: State, action: ActionType) => ({ + ...state, + detailedWork: action.payload + })); + diff --git a/src/store/reducers/RootReducer.ts b/src/store/reducers/RootReducer.ts index 3d7ecfd..4e687f4 100644 --- a/src/store/reducers/RootReducer.ts +++ b/src/store/reducers/RootReducer.ts @@ -4,10 +4,12 @@ import { WalletsReducer } from "./WalletsReducer"; import { SettingsReducer } from "./SettingsReducer"; import { WebsocketReducer } from "./WebsocketReducer"; +import { NodeReducer } from "./NodeReducer"; export default combineReducers({ walletManager: WalletManagerReducer, wallets: WalletsReducer, settings: SettingsReducer, - websocket: WebsocketReducer + websocket: WebsocketReducer, + node: NodeReducer }); diff --git a/src/style/components.less b/src/style/components.less index d8ace36..0e87835 100644 --- a/src/style/components.less +++ b/src/style/components.less @@ -118,3 +118,12 @@ .text-small { font-size: @font-size-sm; } + +.text-green { + color: @kw-green; +} + +.text-orange { + color: @orange-7; +} +