diff --git a/.vscode/settings.json b/.vscode/settings.json index 00bbe57..d66c1d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,6 +40,7 @@ "midiots", "motd", "multiline", + "optimisation", "pnpm", "precaching", "privatekeys", diff --git a/public/locales/en.json b/public/locales/en.json index 3d4d2d5..7e433e2 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -279,7 +279,13 @@ "title": "Settings", "menuLanguage": "Language", + + "subMenuAutoRefresh": "Auto-refresh", + "autoRefreshTables": "Auto-refresh tables", + "autoRefreshTablesDescription": "Whether or not large table listings (e.g. transactions, names) should automatically refresh when a change is detected on the network.", + "subMenuDebug": "Debug settings", + "advancedWalletFormats": "Advanced wallet formats", "menuTranslations": "Translations", diff --git a/public/locales/ja.json b/public/locales/ja.json deleted file mode 100644 index e714ab1..0000000 --- a/public/locales/ja.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nav": { - "connection": { - "online": "オンライン", - "offline": "オフライン", - "connecting": "接続中" - }, - - "search": "Kristネットワークを検索", - - "send": "送る", - "request": "受け取る" - }, - - "sidebar": { - "totalBalance": "合計残高", - "guestIndicator": "ゲストです", - "dashboard": "ダッシュボード", - "myWallets": "私の財布", - "addressBook": "住所録", - "transactions": "トランザクション", - "names": "名前", - "mining": "マイニング", - "network": "ネットワーク", - "blocks": "ブロック", - "statistics": "統計", - "madeBy": "作成者:<1>{{authorName}}", - "hostedBy": "ホスト:<1>{{host}}", - "credits": "謝辞" - }, - - "dialog": { - "close": "終了" - }, - - "pagination": { - "justPage": "Page {{page}}", - "pageWithTotal": "Page {{page}} of {{total}}" - }, - - "loading": "読み込み中...", - - "masterPassword": { - "dialogTitle": "マスターパスワード", - "passwordPlaceholder": "マスターパスワード", - "browseAsGuest": "ゲストとして閲覧", - "createPassword": "パスワードの作成", - "logIn": "ログイン", - "forgotPassword": "パスワードをお忘れの場合", - "intro": "新しいマスターパスワードを入力して財布を暗号化する、かゲストとしてKristWebを閲覧することができます<1>。", - "dontForgetPassword": "このパスワードは絶対に忘れないようにしましょう。忘れてしまった場合は、新しいパスワードを作成して、再度すべての財布を追加する必要があります。", - "loginIntro": "マスターパスワードを入力して財布を開けする、かゲストとしてKristWebを閲覧することができます。", - "learnMore": "詳細はこちら", - "errorPasswordRequired": "パスワードが必要です。", - "errorPasswordUnset": "マスターパスワードが設定されていません。", - "errorPasswordIncorrect": "パスワードが間違っています。", - "errorUnknown": "不明なエラーです。", - "helpWalletStorageTitle": "助けて:財布の保管", - "helpWalletStorage": "KristWebにウォレットを追加すると、ウォレットの秘密鍵はブラウザのローカルストレージに保存され、マスターパスワードで暗号化されます。\n保存されたすべてのウォレットは同じマスターパスワードで暗号化され、KristWebを開くたびに入力する必要があります。あなたの実際のウォレットは一切変更されません。\nゲストとしてKristWebを閲覧する場合、マスターパスワードを入力する必要はありませんが、ウォレットを追加したり使用したりすることはできません。あなたはまだKristネットワークを探索することができます。" - } -} diff --git a/src/__data__/languages.json b/src/__data__/languages.json index 209c45d..42023e8 100644 --- a/src/__data__/languages.json +++ b/src/__data__/languages.json @@ -23,15 +23,9 @@ { "name": "Anavrins", "url": "https://github.com/xAnavrins" - } + } ] }, - "ja": { - "name": "Japanese", - "nativeName": "日本語", - "country": "jp", - "contributors": [] - }, "vi": { "name": "Vietnamese", "nativeName": "Tiếng Việt", @@ -40,7 +34,7 @@ { "name": "Boom", "url": "https://github.com/signalhunter" - } + } ] } } diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx index 34d0b4a..7915477 100644 --- a/src/components/ws/WebsocketService.tsx +++ b/src/components/ws/WebsocketService.tsx @@ -10,7 +10,7 @@ import * as api from "../../krist/api"; import { KristAddress, KristBlock, KristTransaction, WSConnectionState, WSIncomingMessage, WSSubscriptionLevel } from "../../krist/api/types"; -import { useWallets, findWalletByAddress, syncWallet, syncWalletUpdate } from "../../krist/wallets/Wallet"; +import { useWallets, findWalletByAddress, syncWallet, syncWalletUpdate, Wallet } from "../../krist/wallets/Wallet"; import WebSocketAsPromised from "websocket-as-promised"; import { throttle } from "lodash-es"; @@ -149,6 +149,8 @@ const fromWallet = findWalletByAddress(this.wallets, transaction.from || undefined); const toWallet = findWalletByAddress(this.wallets, transaction.to); + this.updateTransactionIDs(transaction, fromWallet, toWallet); + switch (transaction.type) { // Update the name counts using the address lookup case "name_purchase": @@ -180,6 +182,35 @@ } } + private updateTransactionIDs(transaction: KristTransaction, fromWallet?: Wallet | null, toWallet?: Wallet | null) { + // Updating these last IDs will trigger auto-refreshes on pages that + // need them + const id = transaction.id; + + debug("lastTransactionID now %d", id); + store.dispatch(nodeActions.setLastTransactionID(id)); + + if (fromWallet || toWallet) { + debug("lastOwnTransactionID now %d", id); + store.dispatch(nodeActions.setLastOwnTransactionID(id)); + } + + if (transaction.type.startsWith("name_")) { + debug("lastNameTransactionID now %d", id); + store.dispatch(nodeActions.setLastNameTransactionID(id)); + + if (fromWallet || toWallet) { + debug("lastOwnNameTransactionID now %d", id); + store.dispatch(nodeActions.setLastOwnNameTransactionID(id)); + } + } + + if (transaction.type !== "mined") { + debug("lastNonMinedTransactionID now %d", id); + store.dispatch(nodeActions.setLastNonMinedTransactionID(id)); + } + } + /** Queues a command to re-fetch an address's balance. The response will be * handled in {@link handleMessage}. This is automatically throttled to * execute on the leading edge of 500ms (REFRESH_THROTTLE_MS). */ diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts index 7adecc0..e285fff 100644 --- a/src/krist/wallets/Wallet.ts +++ b/src/krist/wallets/Wallet.ts @@ -382,11 +382,28 @@ }); } +export interface WalletsHookValues { + wallets: WalletMap; + walletAddressMap: Record; + + addressList: string[]; + joinedAddressList: string; +} + /** Hook that fetches the wallets from the Redux store. */ -export function useWallets(): { wallets: WalletMap; walletAddressMap: Record } { +export function useWallets(): WalletsHookValues { const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); const walletAddressMap = Object.values(wallets) .reduce((o, wallet) => ({ ...o, [wallet.address]: wallet }), {}); - return { wallets, walletAddressMap }; + // A cheap address list used for deep comparison. It's totally okay to assume + // this list will only change when the addresses will change, as since ES2015, + // object ordering is _basically_ consistent: + // https://stackoverflow.com/a/5525820/1499974 + // https://stackoverflow.com/a/38218582/1499974 + // https://stackoverflow.com/a/23202095/1499974 + const addressList = Object.keys(walletAddressMap); + const joinedAddressList = addressList.join(","); + + return { wallets, walletAddressMap, addressList, joinedAddressList }; } diff --git a/src/pages/names/NamesPage.tsx b/src/pages/names/NamesPage.tsx index bf3e28c..bff9a9d 100644 --- a/src/pages/names/NamesPage.tsx +++ b/src/pages/names/NamesPage.tsx @@ -1,11 +1,14 @@ // 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, useMemo, Dispatch, SetStateAction } from "react"; +import React, { useState, useMemo } from "react"; import { useTranslation, TFunction } from "react-i18next"; import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; + import { PageLayout } from "../../layout/PageLayout"; import { NamesResult } from "./NamesResult"; import { NamesTable } from "./NamesTable"; @@ -56,6 +59,34 @@ // invalid address), the table will bubble it up to here const [error, setError] = useState(); + // Used to handle memoisation and auto-refreshing + const { joinedAddressList } = useWallets(); + const lastNameTransactionID = useSelector((s: RootState) => s.node.lastNameTransactionID); + const lastOwnNameTransactionID = useSelector((s: RootState) => s.node.lastOwnNameTransactionID); + const shouldAutoRefresh = useSelector((s: RootState) => s.settings.autoRefreshTables); + + // Comma-separated list of addresses, used as an optimisation for + // memoisation (no deep equality in useMemo) + const usedAddresses = listingType === ListingType.WALLETS + ? joinedAddressList : address; + + // If auto-refresh is disabled, use a static refresh ID + const usedRefreshID = shouldAutoRefresh + ? (listingType === ListingType.WALLETS + ? lastOwnNameTransactionID + : lastNameTransactionID) + : 0; + + // Memoise the table so that it only updates the props (thus triggering a + // re-fetch of the transactions) when something relevant changes + const memoTable = useMemo(() => ( + + ), [usedAddresses, usedRefreshID, setError]); + const siteTitle = getSiteTitle(t, listingType, address); const subTitle = listingType === ListingType.NETWORK_ADDRESS ? address : undefined; @@ -73,33 +104,6 @@ > {error ? - : (listingType === ListingType.WALLETS - // Version of the table component that memoises the wallets - ? - : )} + : memoTable} ; } - -/** - * This is equivalent to TransactionsPage.TransactionsTableWithWallets. See that - * component for some comments and review on why this is necessary, and how it - * could be improved in the future. - */ -function NamesTableWithWallets({ setError }: { setError: Dispatch> }): JSX.Element { - const { walletAddressMap } = useWallets(); - - // See TransactionsPage.tsx for comments - // TODO: improve this - const addresses = Object.keys(walletAddressMap); - addresses.sort(); - const addressList = addresses.join(","); - - const table = useMemo(() => ( - - ), [addressList, setError]); - - return table; -} diff --git a/src/pages/names/NamesTable.tsx b/src/pages/names/NamesTable.tsx index 4e78efa..1e9e9a7 100644 --- a/src/pages/names/NamesTable.tsx +++ b/src/pages/names/NamesTable.tsx @@ -18,11 +18,14 @@ const debug = Debug("kristweb:names-table"); interface Props { + // Number used to trigger a refresh of the names listing + refreshingID?: number; + addresses?: string[]; setError?: Dispatch>; } -export function NamesTable({ addresses, setError }: Props): JSX.Element { +export function NamesTable({ refreshingID, addresses, setError }: Props): JSX.Element { const { t } = useTranslation(); const [loading, setLoading] = useState(true); @@ -43,7 +46,7 @@ .then(setRes) .catch(setError) .finally(() => setLoading(false)); - }, [addresses, setError, options]); + }, [refreshingID, addresses, setError, options]); debug("results? %b res.names.length: %d res.count: %d res.total: %d", !!res, res?.names?.length, res?.count, res?.total); diff --git a/src/pages/settings/SettingBoolean.tsx b/src/pages/settings/SettingBoolean.tsx index 1179383..f53ab46 100644 --- a/src/pages/settings/SettingBoolean.tsx +++ b/src/pages/settings/SettingBoolean.tsx @@ -14,9 +14,15 @@ setting: SettingName; title?: string; titleKey?: string; + description?: string; + descriptionKey?: string; } -export function SettingBoolean({ setting, title, titleKey }: Props): JSX.Element { +export function SettingBoolean({ + setting, + title, titleKey, + description, descriptionKey +}: Props): JSX.Element { const settingValue = useSelector((s: RootState) => s.settings[setting]); const { t } = useTranslation(); @@ -31,5 +37,11 @@ > {titleKey ? t(titleKey) : title} + + {description || descriptionKey && ( +
+ {descriptionKey ? t(descriptionKey) : description} +
+ )} ; } diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index 3252723..ede3c42 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -3,7 +3,7 @@ // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import React, { FC } from "react"; import { Menu } from "antd"; -import { BugOutlined, GlobalOutlined } from "@ant-design/icons"; +import { BugOutlined, GlobalOutlined, ReloadOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; @@ -30,13 +30,29 @@ return + {/* Language selector */} }>{t("settings.menuLanguage")} - } title={t("settings.subMenuDebug")}> + {/* Auto-refresh settings */} + } title={t("settings.subMenuAutoRefresh")}> + {/* Auto-refresh tables */} + + + + + + {/* Debug settings */} + } title={t("settings.subMenuDebug")}> + {/* Advanced wallet formats */} + {/* Translations management */} {t("settings.menuTranslations")} diff --git a/src/pages/transactions/TransactionsPage.tsx b/src/pages/transactions/TransactionsPage.tsx index 71bf1a2..4b32ceb 100644 --- a/src/pages/transactions/TransactionsPage.tsx +++ b/src/pages/transactions/TransactionsPage.tsx @@ -1,12 +1,16 @@ // 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, useMemo, Dispatch, SetStateAction } from "react"; +import React, { useState, useMemo } from "react"; import { Switch } from "antd"; import { useTranslation, TFunction } from "react-i18next"; import { useParams } from "react-router-dom"; +import { useSelector, shallowEqual } from "react-redux"; +import { RootState } from "../../store"; +import { State as NodeState } from "../../store/reducers/NodeReducer"; + import { PageLayout } from "../../layout/PageLayout"; import { TransactionsResult } from "./TransactionsResult"; import { TransactionsTable } from "./TransactionsTable"; @@ -49,6 +53,8 @@ listingType: ListingType; } +/** Returns the correct site title key (with parameters if necessary) for the + * given listing type. */ function getSiteTitle(t: TFunction, listingType: ListingType, address?: string): string { switch (listingType) { case ListingType.WALLETS: @@ -64,6 +70,23 @@ } } +/** Returns the correct auto-refresh ID for the given listing type. */ +function getRefreshID(listingType: ListingType, includeMined: boolean, node: NodeState): number { + switch (listingType) { + case ListingType.WALLETS: + return node.lastOwnTransactionID; + case ListingType.NAME_HISTORY: + return node.lastNameTransactionID; + case ListingType.NAME_SENT: + case ListingType.NETWORK_ALL: + case ListingType.NETWORK_ADDRESS: // TODO: subscribe to a single name + // Prevent annoying refreshes when blocks are mined + return includeMined + ? node.lastTransactionID + : node.lastNonMinedTransactionID; + } +} + export function TransactionsPage({ listingType }: Props): JSX.Element { const { t } = useTranslation(); const { address, name } = useParams(); @@ -73,6 +96,35 @@ // invalid address), the table will bubble it up to here const [error, setError] = useState(); + // Used to handle memoisation and auto-refreshing + const { joinedAddressList } = useWallets(); + const nodeState = useSelector((s: RootState) => s.node, shallowEqual); + const shouldAutoRefresh = useSelector((s: RootState) => s.settings.autoRefreshTables); + + // Comma-separated list of addresses, used as an optimisation for + // memoisation (no deep equality in useMemo) + const usedAddresses = listingType === ListingType.WALLETS + ? joinedAddressList : address; + + // If auto-refresh is disabled, use a static refresh ID + const usedRefreshID = shouldAutoRefresh + ? getRefreshID(listingType, includeMined, nodeState) : 0; + + // Memoise the table so that it only updates the props (thus triggering a + // re-fetch of the transactions) when something relevant changes + const memoTable = useMemo(() => ( + + ), [usedAddresses, name, usedRefreshID, includeMined, setError]); + const siteTitle = getSiteTitle(t, listingType, address); const subTitle = name ? @@ -107,68 +159,6 @@ > {error ? - : (listingType === ListingType.WALLETS - ? ( - // Version of the table component that memoises the wallets - - ) - : ( - - ))} + : memoTable} ; } - -interface TableWithWalletsProps { - includeMined: boolean; - setError: Dispatch>; -} - -/** - * We only want to fetch the wallets (because changes will cause re-renders) - * if this is a wallet transactions listing. In order to achieve a conditional - * hook, the table is wrapped in this component only if the listing type is - * WALLETS. - */ -function TransactionsTableWithWallets({ includeMined, setError }: TableWithWalletsProps): JSX.Element { - const { walletAddressMap } = useWallets(); - const addresses = Object.keys(walletAddressMap); - - // The instance created by Object.keys is going to be different every time, - // but its values probably won't change. So, we're going to need a rather - // crude deep equality check. - // TODO: Perhaps do this check on the whole wallets object, so that we can - // auto-refresh the table when balances change, but still avoid - // refreshing otherwise. - // REVIEW: Another idea is to store a 'lastTransactionID' in Redux, - // concerning only our own wallets (and, in the future, a subscribed - // wallet when viewing an external address?). This way, we can still - // auto-refresh whenever we have to (a wallet is added/removed, a - // transaction is made to one of our wallets), and never when we - // don't. - addresses.sort(); - const addressList = addresses.join(","); - - const table = useMemo(() => ( - - ), [addressList, includeMined, setError]); - - return table; -} diff --git a/src/pages/transactions/TransactionsTable.tsx b/src/pages/transactions/TransactionsTable.tsx index 2e3c560..2b2fbe1 100644 --- a/src/pages/transactions/TransactionsTable.tsx +++ b/src/pages/transactions/TransactionsTable.tsx @@ -34,6 +34,9 @@ interface Props { listingType: ListingType; + // Number used to trigger a refresh of the transactions listing + refreshingID?: number; + addresses?: string[]; name?: string; @@ -41,7 +44,7 @@ setError?: Dispatch>; } -export function TransactionsTable({ listingType, addresses, name, includeMined, setError }: Props): JSX.Element { +export function TransactionsTable({ listingType, refreshingID, addresses, name, includeMined, setError }: Props): JSX.Element { const { t } = useTranslation(); const [loading, setLoading] = useState(true); @@ -66,7 +69,7 @@ .then(setRes) .catch(setError) .finally(() => setLoading(false)); - }, [listingType, addresses, name, setError, options, includeMined]); + }, [listingType, refreshingID, addresses, name, setError, options, includeMined]); debug("results? %b res.transactions.length: %d res.count: %d res.total: %d", !!res, res?.transactions?.length, res?.count, res?.total); diff --git a/src/store/actions/NodeActions.ts b/src/store/actions/NodeActions.ts index dd11089..8e38230 100644 --- a/src/store/actions/NodeActions.ts +++ b/src/store/actions/NodeActions.ts @@ -6,8 +6,14 @@ import * as constants from "../constants"; -export const setSyncNode = createAction(constants.SYNC_NODE)(); export const setLastBlockID = createAction(constants.LAST_BLOCK_ID)(); +export const setLastTransactionID = createAction(constants.LAST_TRANSACTION_ID)(); +export const setLastNonMinedTransactionID = createAction(constants.LAST_NON_MINED_TRANSACTION_ID)(); +export const setLastOwnTransactionID = createAction(constants.LAST_OWN_TRANSACTION_ID)(); +export const setLastNameTransactionID = createAction(constants.LAST_NAME_TRANSACTION_ID)(); +export const setLastOwnNameTransactionID = createAction(constants.LAST_OWN_NAME_TRANSACTION_ID)(); + +export const setSyncNode = createAction(constants.SYNC_NODE)(); export const setDetailedWork = createAction(constants.DETAILED_WORK)(); export const setCurrency = createAction(constants.CURRENCY)(); export const setConstants = createAction(constants.CONSTANTS)(); diff --git a/src/store/constants.ts b/src/store/constants.ts index f05ff94..7456fd9 100644 --- a/src/store/constants.ts +++ b/src/store/constants.ts @@ -25,9 +25,18 @@ // --- export const CONNECTION_STATE = "CONNECTION_STATE"; -// Node state -export const SYNC_NODE = "SYNC_NODE"; +// Node state (auto-refreshing) +// --- export const LAST_BLOCK_ID = "LAST_BLOCK_ID"; +export const LAST_NON_MINED_TRANSACTION_ID = "LAST_NON_MINED_TRANSACTION_ID"; +export const LAST_TRANSACTION_ID = "LAST_TRANSACTION_ID"; +export const LAST_OWN_TRANSACTION_ID = "LAST_OWN_TRANSACTION_ID"; +export const LAST_NAME_TRANSACTION_ID = "LAST_NAME_TRANSACTION_ID"; +export const LAST_OWN_NAME_TRANSACTION_ID = "LAST_OWN_NAME_TRANSACTION_ID"; + +// Node state (MOTD) +// --- +export const SYNC_NODE = "SYNC_NODE"; export const DETAILED_WORK = "DETAILED_WORK"; export const CURRENCY = "CURRENCY"; export const CONSTANTS = "CONSTANTS"; diff --git a/src/store/reducers/NodeReducer.ts b/src/store/reducers/NodeReducer.ts index e7461d4..bb13d34 100644 --- a/src/store/reducers/NodeReducer.ts +++ b/src/store/reducers/NodeReducer.ts @@ -1,16 +1,31 @@ // 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 { createReducer, ActionType } from "typesafe-actions"; -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 { createReducer } from "typesafe-actions"; +import { + KristWorkDetailed, KristCurrency, DEFAULT_CURRENCY, KristConstants, + DEFAULT_CONSTANTS, KristMOTDBase, DEFAULT_MOTD_BASE +} from "../../krist/api/types"; +import { + setLastBlockID, setLastTransactionID, setLastNonMinedTransactionID, + setLastOwnTransactionID, setLastNameTransactionID, setLastOwnNameTransactionID, + setSyncNode, setDetailedWork, setCurrency, setConstants, setMOTD +} from "../actions/NodeActions"; import packageJson from "../../../package.json"; export interface State { + // Used to handle auto-refreshing of various pages readonly lastBlockID: number; - readonly detailedWork?: KristWorkDetailed; + readonly lastTransactionID: number; + readonly lastNonMinedTransactionID: number; + readonly lastOwnTransactionID: number; + readonly lastNameTransactionID: number; + readonly lastOwnNameTransactionID: number; + + // Info from the MOTD readonly syncNode: string; + readonly detailedWork?: KristWorkDetailed; readonly currency: KristCurrency; readonly constants: KristConstants; readonly motd: KristMOTDBase; @@ -18,7 +33,15 @@ export function getInitialNodeState(): State { return { + // Used to handle auto-refreshing of various pages lastBlockID: 0, + lastTransactionID: 0, + lastNonMinedTransactionID: 0, + lastOwnTransactionID: 0, + lastNameTransactionID: 0, + lastOwnNameTransactionID: 0, + + // Info from the MOTD syncNode: localStorage.getItem("syncNode") || packageJson.defaultSyncNode, currency: DEFAULT_CURRENCY, constants: DEFAULT_CONSTANTS, @@ -27,27 +50,17 @@ } export const NodeReducer = createReducer({} as State) - .handleAction(setSyncNode, (state: State, action: ActionType) => ({ - ...state, - syncNode: action.payload - })) - .handleAction(setLastBlockID, (state: State, action: ActionType) => ({ - ...state, - lastBlockID: action.payload - })) - .handleAction(setDetailedWork, (state: State, action: ActionType) => ({ - ...state, - detailedWork: action.payload - })) - .handleAction(setCurrency, (state: State, action: ActionType) => ({ - ...state, - currency: action.payload - })) - .handleAction(setConstants, (state: State, action: ActionType) => ({ - ...state, - constants: action.payload - })) - .handleAction(setMOTD, (state: State, action: ActionType) => ({ - ...state, - motd: action.payload - })); + // Used to handle auto-refreshing of various pages + .handleAction(setLastBlockID, (state, action) => ({ ...state, lastBlockID: action.payload })) + .handleAction(setLastTransactionID, (state, action) => ({ ...state, lastTransactionID: action.payload })) + .handleAction(setLastNonMinedTransactionID, (state, action) => ({ ...state, lastNonMinedTransactionID: action.payload })) + .handleAction(setLastOwnTransactionID, (state, action) => ({ ...state, lastOwnTransactionID: action.payload })) + .handleAction(setLastNameTransactionID, (state, action) => ({ ...state, lastNameTransactionID: action.payload })) + .handleAction(setLastOwnNameTransactionID, (state, action) => ({ ...state, lastOwnNameTransactionID: action.payload })) + + // Info from the MOTD + .handleAction(setSyncNode, (state, action) => ({ ...state, syncNode: action.payload })) + .handleAction(setDetailedWork, (state, action) => ({ ...state, detailedWork: action.payload })) + .handleAction(setCurrency, (state, action) => ({ ...state, currency: action.payload })) + .handleAction(setConstants, (state, action) => ({ ...state, constants: action.payload })) + .handleAction(setMOTD, (state, action) => ({ ...state, motd: action.payload })); diff --git a/src/style/components.less b/src/style/components.less index 60a2508..f52904f 100644 --- a/src/style/components.less +++ b/src/style/components.less @@ -13,6 +13,20 @@ margin-bottom: 0; user-select: none; + + height: auto; + + &:last-child { border-bottom: none; } + } + + .ant-menu-submenu { + border-bottom: 1px solid @border-color-split; + &:last-child { border-bottom: none; } + } + + .menu-item-description { + color: @text-color-secondary; + font-size: @font-size-sm; } } diff --git a/src/utils/settings.ts b/src/utils/settings.ts index a1083d7..133287e 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -6,12 +6,20 @@ import { store } from "../App"; import * as actions from "../store/actions/SettingsActions"; +import Debug from "debug"; +const debug = Debug("kristweb:settings"); + export interface SettingsState { + /** Whether or not tables (e.g. transactions, names) should auto-refresh + * when a change is detected on the network. */ + readonly autoRefreshTables: boolean; + /** Whether or not advanced wallet formats are enabled. */ readonly walletFormats: boolean; } export const DEFAULT_SETTINGS: SettingsState = { + autoRefreshTables: true, walletFormats: false }; @@ -24,12 +32,18 @@ export function loadSettings(): SettingsState { // Import the default settings first const settings = { ...DEFAULT_SETTINGS }; + debug("loading settings"); // Using the default settings as a template, import the settings from local // storage for (const [settingName, value] of Object.entries(settings) as [AnySettingName, any][]) { const stored = localStorage.getItem(getSettingKey(settingName)); - if (stored === null) continue; + debug("setting %s - stored: %o - default: %o", settingName, stored, value); + + if (stored === null) { + debug("setting %s does not have a stored value", settingName); + continue; + } switch (typeof value) { case "boolean": @@ -44,6 +58,7 @@ } export function setBooleanSetting(settingName: SettingName, value: boolean): void { + debug("changing setting %s value to %o", settingName, value); localStorage.setItem(getSettingKey(settingName), value ? "true" : "false"); store.dispatch(actions.setBooleanSetting(settingName, value)); }