├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── LICENSE.txt ├── README.md ├── craco.config.js ├── package.json ├── public ├── favicon.ico ├── img │ ├── firefox.svg │ ├── flags_responsive.png │ ├── google-chrome.svg │ └── tmpim.svg ├── index.html ├── locales │ ├── de.json │ ├── en.json │ ├── fr.json │ ├── it.json │ ├── nl.json │ ├── pl.json │ ├── pt.json │ ├── tr.json │ └── vi.json ├── logo.svg ├── logo192.png ├── logo512.png ├── manifest.json ├── maskable512.png └── robots.txt ├── src ├── App.less ├── App.tsx ├── __data__ │ ├── languages.json │ └── verified-addresses.json ├── __tests__ │ └── App.tsx ├── components │ ├── CheeseburgerIcon.tsx │ ├── ConditionalLink.tsx │ ├── CopyInputButton.tsx │ ├── DateTime.tsx │ ├── Flag.tsx │ ├── HelpIcon.tsx │ ├── OptionalField.tsx │ ├── SmallCopyable.tsx │ ├── Statistic.tsx │ ├── addresses │ │ ├── ContextualAddress.less │ │ ├── ContextualAddress.tsx │ │ ├── VerifiedAddress.tsx │ │ ├── VerifiedCheck.tsx │ │ └── picker │ │ │ ├── AddressHint.tsx │ │ │ ├── AddressPicker.less │ │ │ ├── AddressPicker.tsx │ │ │ ├── Header.tsx │ │ │ ├── Item.tsx │ │ │ ├── NameHint.tsx │ │ │ ├── PickerHints.tsx │ │ │ ├── VerifiedHint.tsx │ │ │ ├── WalletHint.tsx │ │ │ └── options.ts │ ├── auth │ │ ├── AuthContext.tsx │ │ ├── AuthForm.tsx │ │ ├── AuthMasterPasswordModal.tsx │ │ ├── AuthorisedAction.tsx │ │ ├── FakeUsernameInput.tsx │ │ ├── MasterPasswordInput.tsx │ │ ├── SetMasterPasswordModal.tsx │ │ └── index.ts │ ├── krist │ │ ├── KristSymbol.tsx │ │ ├── KristValue.less │ │ ├── KristValue.tsx │ │ └── MarkdownLink.tsx │ ├── names │ │ ├── KristNameLink.tsx │ │ ├── NameARecordLink.less │ │ └── NameARecordLink.tsx │ ├── results │ │ ├── APIErrorResult.tsx │ │ ├── NoWalletsResult.tsx │ │ └── SmallResult.tsx │ ├── styles │ │ ├── ConditionalLink.less │ │ ├── DateTime.less │ │ ├── Flag.css │ │ ├── HelpIcon.less │ │ ├── OptionalField.less │ │ ├── SmallCopyable.less │ │ └── Statistic.less │ ├── transactions │ │ ├── AmountInput.tsx │ │ ├── SendTransactionModalLink.tsx │ │ ├── TransactionConciseMetadata.less │ │ ├── TransactionConciseMetadata.tsx │ │ ├── TransactionItem.tsx │ │ ├── TransactionItemParts.tsx │ │ ├── TransactionSummary.less │ │ ├── TransactionSummary.tsx │ │ ├── TransactionType.less │ │ └── TransactionType.tsx │ ├── types.ts │ └── wallets │ │ ├── SelectWalletCategory.tsx │ │ ├── SelectWalletFormat.tsx │ │ └── SyncWallets.tsx ├── global │ ├── AppHotkeys.tsx │ ├── AppLoading.tsx │ ├── AppRouter.tsx │ ├── AppServices.tsx │ ├── ErrorBoundary.tsx │ ├── ForcedAuth.tsx │ ├── LocaleContext.tsx │ ├── PurchaseKrist.tsx │ ├── StorageBroadcast.tsx │ ├── compat │ │ ├── CompatCheckModal.less │ │ ├── CompatCheckModal.tsx │ │ ├── index.ts │ │ └── localStorage.ts │ ├── legacy │ │ ├── LegacyMigration.tsx │ │ ├── LegacyMigrationForm.tsx │ │ └── LegacyMigrationModal.tsx │ └── ws │ │ ├── SyncDetailedWork.tsx │ │ ├── SyncMOTD.tsx │ │ ├── WebsocketConnection.ts │ │ ├── WebsocketProvider.tsx │ │ ├── WebsocketService.tsx │ │ └── WebsocketSubscription.ts ├── index.css ├── index.tsx ├── krist │ ├── api │ │ ├── AuthFailed.tsx │ │ ├── index.ts │ │ ├── login.ts │ │ ├── lookup.ts │ │ ├── names.ts │ │ ├── search.ts │ │ ├── transactions.ts │ │ └── types.ts │ ├── contacts │ │ ├── Contact.ts │ │ ├── contactStorage.ts │ │ ├── functions │ │ │ ├── addContact.ts │ │ │ └── editContact.ts │ │ ├── index.ts │ │ └── utils.ts │ └── wallets │ │ ├── Wallet.ts │ │ ├── functions │ │ ├── addWallet.ts │ │ ├── decryptWallet.ts │ │ ├── editWallet.ts │ │ ├── recalculateWallets.ts │ │ ├── resetMasterPassword.ts │ │ └── syncWallets.ts │ │ ├── index.ts │ │ ├── masterPassword.ts │ │ ├── utils.ts │ │ ├── walletFormats.ts │ │ └── walletStorage.ts ├── layout │ ├── AppLayout.less │ ├── AppLayout.tsx │ ├── PageLayout.less │ ├── PageLayout.tsx │ ├── nav │ │ ├── AppHeader.less │ │ ├── AppHeader.tsx │ │ ├── Brand.tsx │ │ ├── ConnectionIndicator.less │ │ ├── ConnectionIndicator.tsx │ │ ├── CymbalIndicator.tsx │ │ ├── Search.tsx │ │ ├── SearchResults.less │ │ ├── SearchResults.tsx │ │ └── TopMenu.tsx │ └── sidebar │ │ ├── ServiceWorkerCheck.tsx │ │ ├── Sidebar.less │ │ ├── Sidebar.tsx │ │ ├── SidebarFooter.tsx │ │ └── SidebarTotalBalance.tsx ├── pages │ ├── NotFoundPage.tsx │ ├── addresses │ │ ├── AddressButtonRow.tsx │ │ ├── AddressNamesCard.tsx │ │ ├── AddressPage.less │ │ ├── AddressPage.tsx │ │ ├── AddressTransactionsCard.tsx │ │ └── NameItem.tsx │ ├── backup │ │ ├── BackupResultsSummary.less │ │ ├── BackupResultsSummary.tsx │ │ ├── BackupResultsTree.less │ │ ├── BackupResultsTree.tsx │ │ ├── ExportBackupModal.tsx │ │ ├── ImportBackupForm.tsx │ │ ├── ImportBackupModal.tsx │ │ ├── ImportDetectFormat.tsx │ │ ├── ImportFileButton.tsx │ │ ├── ImportProgress.tsx │ │ ├── backupExport.ts │ │ ├── backupFormats.ts │ │ ├── backupImport.ts │ │ ├── backupImportUtils.ts │ │ ├── backupImportV1.ts │ │ ├── backupImportV2.ts │ │ ├── backupParser.ts │ │ └── backupResults.ts │ ├── blocks │ │ ├── BlockHash.less │ │ ├── BlockHash.tsx │ │ ├── BlockMobileItem.tsx │ │ ├── BlockPage.less │ │ ├── BlockPage.tsx │ │ ├── BlocksPage.less │ │ ├── BlocksPage.tsx │ │ └── BlocksTable.tsx │ ├── contacts │ │ ├── AddContactModal.tsx │ │ ├── ContactActions.tsx │ │ ├── ContactEditButton.tsx │ │ ├── ContactMobileItem.tsx │ │ ├── ContactsMobileItemActions.tsx │ │ ├── ContactsPage.less │ │ ├── ContactsPage.tsx │ │ ├── ContactsPageActions.tsx │ │ └── ContactsTable.tsx │ ├── credits │ │ ├── CreditsPage.less │ │ ├── CreditsPage.tsx │ │ ├── Privacy.tsx │ │ ├── Supporters.tsx │ │ └── Translators.tsx │ ├── dashboard │ │ ├── BlockDifficultyCard.tsx │ │ ├── BlockValueCard.tsx │ │ ├── DashboardPage.less │ │ ├── DashboardPage.tsx │ │ ├── InDevBanner.tsx │ │ ├── MOTDCard.tsx │ │ ├── TipsCard.tsx │ │ ├── TransactionsCard.tsx │ │ ├── WalletItem.tsx │ │ ├── WalletOverviewCard.tsx │ │ └── WhatsNewCard.tsx │ ├── dev │ │ └── DevPage.tsx │ ├── names │ │ ├── NameButtonRow.tsx │ │ ├── NameMobileItem.tsx │ │ ├── NameMobileItemActions.tsx │ │ ├── NamePage.less │ │ ├── NamePage.tsx │ │ ├── NameTransactionsCard.tsx │ │ ├── NamesPage.less │ │ ├── NamesPage.tsx │ │ ├── NamesTable.tsx │ │ ├── mgmt │ │ │ ├── ConfirmModal.tsx │ │ │ ├── EditProgress.tsx │ │ │ ├── NameActions.tsx │ │ │ ├── NameDataInput.tsx │ │ │ ├── NameEditForm.tsx │ │ │ ├── NameEditModal.tsx │ │ │ ├── NameEditModalLink.tsx │ │ │ ├── NamePicker.tsx │ │ │ ├── NamePurchaseModal.tsx │ │ │ ├── NamePurchaseModalLink.tsx │ │ │ ├── NoNamesModal.tsx │ │ │ ├── SuccessNotifContent.tsx │ │ │ ├── checkName.ts │ │ │ ├── handleErrors.ts │ │ │ └── lookupNames.ts │ │ └── tableLock.ts │ ├── settings │ │ ├── SettingBoolean.tsx │ │ ├── SettingDescription.tsx │ │ ├── SettingInteger.tsx │ │ ├── SettingLink.tsx │ │ ├── SettingsGroup.tsx │ │ ├── SettingsPage.less │ │ ├── SettingsPage.tsx │ │ ├── manage │ │ │ ├── ResetMasterPassword.tsx │ │ │ └── SettingsManage.tsx │ │ └── translations │ │ │ ├── LanguageItem.tsx │ │ │ ├── LanguagesTable.tsx │ │ │ ├── MissingKeysTable.tsx │ │ │ ├── SettingsTranslations.tsx │ │ │ ├── analyseLangs.ts │ │ │ ├── exportCSV.ts │ │ │ └── importJSON.ts │ ├── transactions │ │ ├── TransactionMetadataCard.tsx │ │ ├── TransactionPage.less │ │ ├── TransactionPage.tsx │ │ ├── TransactionRawDataCard.tsx │ │ ├── TransactionsPage.less │ │ ├── TransactionsPage.tsx │ │ ├── TransactionsTable.tsx │ │ ├── request │ │ │ ├── RequestForm.tsx │ │ │ ├── RequestPage.less │ │ │ └── RequestPage.tsx │ │ └── send │ │ │ ├── QueryParamsHook.tsx │ │ │ ├── SendTransactionConfirmModal.tsx │ │ │ ├── SendTransactionForm.tsx │ │ │ ├── SendTransactionModal.tsx │ │ │ ├── SendTransactionPage.less │ │ │ ├── SendTransactionPage.tsx │ │ │ ├── Success.tsx │ │ │ └── handleErrors.ts │ ├── wallets │ │ ├── AddWalletModal.tsx │ │ ├── ManageBackupsDropdown.tsx │ │ ├── NoWalletsMobileResult.tsx │ │ ├── WalletActions.tsx │ │ ├── WalletEditButton.tsx │ │ ├── WalletMobileItem.tsx │ │ ├── WalletMobileItemActions.tsx │ │ ├── WalletsPage.less │ │ ├── WalletsPage.tsx │ │ ├── WalletsPageActions.tsx │ │ ├── WalletsTable.tsx │ │ └── info │ │ │ ├── BooleanText.tsx │ │ │ ├── DecryptReveal.tsx │ │ │ ├── WalletDescAdvancedInfo.tsx │ │ │ ├── WalletDescBasicInfo.tsx │ │ │ ├── WalletDescSyncedInfo.tsx │ │ │ ├── WalletInfoModal.less │ │ │ └── WalletInfoModal.tsx │ └── whatsnew │ │ ├── CommitsCard.tsx │ │ ├── WhatsNewCard.tsx │ │ ├── WhatsNewPage.less │ │ ├── WhatsNewPage.tsx │ │ └── types.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── service-worker.ts ├── setupTests.ts ├── store │ ├── actions │ │ ├── ContactsActions.ts │ │ ├── MasterPasswordActions.ts │ │ ├── MiscActions.ts │ │ ├── NodeActions.ts │ │ ├── SettingsActions.ts │ │ ├── WalletsActions.ts │ │ ├── WebsocketActions.ts │ │ └── index.ts │ ├── constants.ts │ ├── index.ts │ ├── init.ts │ ├── reducers │ │ ├── ContactsReducer.ts │ │ ├── MasterPasswordReducer.ts │ │ ├── MiscReducer.ts │ │ ├── NodeReducer.ts │ │ ├── RootReducer.ts │ │ ├── SettingsReducer.ts │ │ ├── WalletsReducer.ts │ │ └── WebsocketReducer.ts │ └── types.d.ts ├── style │ ├── card.less │ ├── components.less │ ├── table.less │ └── theme.less └── utils │ ├── consoleWarning.ts │ ├── crypto │ ├── CryptoJS.ts │ ├── crypto.ts │ ├── generatePassword.ts │ └── index.ts │ ├── errors.ts │ ├── hooks │ ├── index.ts │ ├── useBreakpoint.ts │ ├── useHistoryState.ts │ └── useMountEffect.ts │ ├── i18n │ ├── errors.ts │ ├── fns.ts │ ├── index.ts │ ├── init.ts │ └── languages.ts │ ├── index.ts │ ├── krist │ ├── addressAlgo.ts │ ├── commonmeta.ts │ ├── currency.ts │ └── index.ts │ ├── misc │ ├── credits.ts │ ├── devState.ts │ ├── math.ts │ ├── promiseThrottle.ts │ └── sort.ts │ ├── serviceWorkerRegistration.ts │ ├── settings.ts │ ├── setup.ts │ └── table │ ├── SortModal.tsx │ ├── mobileList.tsx │ └── table.tsx ├── tools ├── addLanguages.js └── commitLog.js ├── tsconfig.extend.json ├── tsconfig.json ├── typings └── react-timeago │ └── lib │ └── formatters │ └── index.d.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.{js,ts,jsx,tsx,css,less,json}] 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | tools 4 | public 5 | typings 6 | craco.config.js 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://donate.lemmmy.pw"] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | package-lock.json 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | /src/__data__/host*.json 27 | 28 | .sentryclirc 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome against localhost", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}" 10 | }, 11 | { 12 | "type": "firefox", 13 | "request": "launch", 14 | "name": "Launch Firefox against localhost", 15 | "url": "http://localhost:3000", 16 | "webRoot": "${workspaceFolder}", 17 | "profile": "kristweb", 18 | "keepProfileChanges": true, 19 | "reAttach": true 20 | }, 21 | { 22 | "type": "firefox", 23 | "request": "attach", 24 | "name": "Attach Firefox against localhost", 25 | "url": "http://localhost:3000", 26 | "webRoot": "${workspaceFolder}" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "AGPL", 4 | "Algo", 5 | "antd", 6 | "anticon", 7 | "appendhashes", 8 | "arraybuffer", 9 | "Authed", 10 | "Authorise", 11 | "authorised", 12 | "behaviour", 13 | "broadcastchannel", 14 | "btns", 15 | "categorised", 16 | "chartjs", 17 | "clientside", 18 | "Colours", 19 | "commithash", 20 | "commonmeta", 21 | "compat", 22 | "cryptocurrency", 23 | "Debounces", 24 | "desaturate", 25 | "dont", 26 | "firstseen", 27 | "gitlog", 28 | "Inequal", 29 | "initialising", 30 | "jwalelset", 31 | "KRISTWALLET", 32 | "KRISTWALLETEXTENSION", 33 | "languagedetector", 34 | "Lemmy", 35 | "linkify", 36 | "Lngs", 37 | "localisation", 38 | "Lyqydate", 39 | "masterkey", 40 | "memoises", 41 | "metaname", 42 | "mgmt", 43 | "middot", 44 | "midiots", 45 | "motd", 46 | "multiline", 47 | "Mutex", 48 | "nolink", 49 | "Notif", 50 | "optimisation", 51 | "personalise", 52 | "pkgbuild", 53 | "pnpm", 54 | "Popconfirm", 55 | "Precache", 56 | "precaching", 57 | "privatekeys", 58 | "readonly", 59 | "serialisable", 60 | "serialised", 61 | "shallowequal", 62 | "Sider", 63 | "singleline", 64 | "submenu", 65 | "summarising", 66 | "Syncable", 67 | "testid", 68 | "timeago", 69 | "totalin", 70 | "totalout", 71 | "Transpiler", 72 | "treenode", 73 | "tsdoc", 74 | "typeahead", 75 | "uncategorised", 76 | "unmount", 77 | "unmounting", 78 | "unregistering", 79 | "UNSYNC", 80 | "unsyncable", 81 | "Voronoi", 82 | "webpackbar", 83 | "Websockets", 84 | "whatsnew" 85 | ], 86 | "i18next.defaultTranslatedLocale": "en", 87 | "i18next.i18nPaths": "public/locales", 88 | "files.associations": { 89 | "public/locales/**.json": "json5" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "start", 7 | "isBackground": true 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build app 2 | FROM node:16-alpine AS build 3 | 4 | RUN apk update && apk add git gzip 5 | 6 | WORKDIR /build 7 | 8 | COPY ["yarn.lock", "./"] 9 | RUN yarn global add rimraf @craco/craco@^6.1.1 10 | 11 | COPY ["package.json", "./"] 12 | RUN yarn install 13 | 14 | COPY . . 15 | 16 | ENV NODE_ENV=production 17 | 18 | ARG SENTRY_DSN 19 | ARG SENTRY_ORG 20 | ARG SENTRY_PROJECT 21 | ARG SENTRY_TOKEN 22 | ARG SENTRY_URL 23 | ENV SENTRY_DSN=$SENTRY_DSN 24 | ENV SENTRY_ORG=$SENTRY_ORG 25 | ENV SENTRY_PROJECT=$SENTRY_PROJECT 26 | ENV SENTRY_TOKEN=$SENTRY_TOKEN 27 | ENV SENTRY_URL=$SENTRY_URL 28 | 29 | RUN yarn run build 30 | RUN yarn run optimise 31 | 32 | # Copy the build files to the output folder (ideally volumed in) to be consumed 33 | # by the webserver 34 | FROM alpine 35 | 36 | WORKDIR /build 37 | COPY --from=build /build/build ./build 38 | 39 | RUN mkdir out 40 | CMD cp -r build/* out/ 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmpim/KristWeb2/c8c5f07c227d74bf1500a00811ce0744dee07863/public/favicon.ico -------------------------------------------------------------------------------- /public/img/flags_responsive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmpim/KristWeb2/c8c5f07c227d74bf1500a00811ce0744dee07863/public/img/flags_responsive.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmpim/KristWeb2/c8c5f07c227d74bf1500a00811ce0744dee07863/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmpim/KristWeb2/c8c5f07c227d74bf1500a00811ce0744dee07863/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "KristWeb", 3 | "name": "KristWeb", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "128x128 64x64 32x32 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | }, 20 | { 21 | "src": "maskable512.png", 22 | "type": "image/png", 23 | "sizes": "512x512", 24 | "purpose": "maskable" 25 | } 26 | ], 27 | "start_url": ".", 28 | "display": "standalone", 29 | "theme_color": "#343a56", 30 | "background_color": "#343a56" 31 | } 32 | -------------------------------------------------------------------------------- /public/maskable512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmpim/KristWeb2/c8c5f07c227d74bf1500a00811ce0744dee07863/public/maskable512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import "antd/dist/antd.dark.less"; 5 | @import "./style/theme.less"; 6 | @import "./style/components.less"; 7 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Suspense } from "react"; 5 | import { BrowserRouter as Router } from "react-router-dom"; 6 | 7 | import { Provider } from "react-redux"; 8 | import { initStore } from "@store/init"; 9 | 10 | // Set up localisation 11 | import "@utils/i18n"; 12 | 13 | // FIXME: Apparently the import order of my CSS is important. Who knew! 14 | import "./App.less"; 15 | 16 | import { ErrorBoundary } from "@global/ErrorBoundary"; 17 | import { AppLoading } from "@global/AppLoading"; 18 | import { AppServices } from "@global/AppServices"; 19 | import { WebsocketProvider } from "@global/ws/WebsocketProvider"; 20 | import { LocaleContext } from "@global/LocaleContext"; 21 | import { AuthProvider } from "@comp/auth/AuthContext"; 22 | 23 | import { AppLayout } from "@layout/AppLayout"; 24 | 25 | import Debug from "debug"; 26 | const debug = Debug("kristweb:app"); 27 | 28 | export let store: ReturnType; 29 | 30 | function App(): JSX.Element { 31 | debug("whole app is being rendered!"); 32 | 33 | if (!store) { 34 | debug("initialising redux store"); 35 | store = initStore(); 36 | (window as any).kwReduxStore = store; 37 | } 38 | 39 | return 40 | }> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {/* Services, etc. */} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ; 57 | } 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /src/__data__/verified-addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "kqxhx5yn9v": { 3 | "label": "SwitchCraft", 4 | "description": "This address is the master wallet for the SwitchCraft Minecraft server. It represents the balance of all SwitchCraft players.", 5 | "website": "https://sc3.io" 6 | }, 7 | "kitsemmaya": { 8 | "label": "BustAKrist", 9 | "description": "This address holds the bankroll and player balances for BustAKrist, a Krist gambling site.", 10 | "website": "https://bustakrist.its-em.ma" 11 | }, 12 | "k0resoaker": { 13 | "label": "Soak Bot", 14 | "description": "Money sent to this address is distributed among all currently online SwitchCraft players." 15 | }, 16 | "ksellshopq": { "label": "Sellshop" }, 17 | "kr08ac3b4o": { "label": "CodersNet", "isActive": false }, 18 | "kyq7arbu73": { "label": "CodersNet", "isActive": false }, 19 | "kek4daddy2": { "label": "KDice", "isActive": false }, 20 | "klemmyturd": { "label": "KFaucet", "isActive": false }, 21 | "knfe7aps4c": { "label": "KFaucet", "isActive": false }, 22 | "kh9w36ea1b": { "label": "KLottery", "isActive": false }, 23 | "klucky7942": { "label": "KLottery", "isActive": false }, 24 | "kmineqokuz": { "label": "KristMiner.cf", "isActive": false }, 25 | "kul2kr8t4l": { "label": "LurCraft", "isActive": false }, 26 | "k5cfswitch": { "label": "SwitchMarket", "isActive": false }, 27 | "kf4n3el1a5": { "label": "CodersNet Reimbursement", "isActive": false }, 28 | "k5pzufkwup": { "label": "Pippigrump Core", "isActive": false }, 29 | "kchocolate": { "label": "Tenebra ICO" }, 30 | "kqojv3i6eh": { 31 | "label": "PG231's LP", 32 | "description": "Liquidity pools on SwitchCraft operated by PG231." 33 | }, 34 | "kxxxxxxxxk": { "label": "X" } 35 | } 36 | -------------------------------------------------------------------------------- /src/__tests__/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { render, screen } from "@testing-library/react"; 5 | import App from "@app"; 6 | 7 | test("renders the app", async () => { 8 | render(); 9 | 10 | const appLayout = await screen.findByTestId("site-app-layout"); 11 | expect(appLayout).toBeInTheDocument(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/CheeseburgerIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import Icon from "@ant-design/icons"; 5 | 6 | export const CheeseburgerIconSvg = (): JSX.Element => ( 7 | 8 | 12 | 13 | ); 14 | export const CheeseburgerIcon = (props: any): JSX.Element => 15 | ; 16 | -------------------------------------------------------------------------------- /src/components/ConditionalLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC } from "react"; 5 | 6 | import { Link, useRouteMatch } from "react-router-dom"; 7 | 8 | import "./styles/ConditionalLink.less"; 9 | 10 | interface Props { 11 | to?: string; 12 | condition?: boolean; 13 | 14 | replace?: boolean; 15 | 16 | matchTo?: boolean; 17 | matchPath?: string; 18 | matchExact?: boolean; 19 | matchStrict?: boolean; 20 | matchSensitive?: boolean; 21 | } 22 | 23 | export const ConditionalLink: FC = ({ 24 | to, 25 | condition, 26 | 27 | replace, 28 | 29 | matchTo, 30 | matchPath, 31 | matchExact, 32 | matchStrict, 33 | matchSensitive, 34 | 35 | children, ...props 36 | }): JSX.Element => { 37 | // Disable the link if we're already on that route 38 | const wantsCondition = condition !== undefined; 39 | const wantsMatch = matchTo || !!matchPath; 40 | 41 | const match = useRouteMatch(wantsMatch ? { 42 | path: matchTo && to ? to : matchPath, 43 | exact: matchExact, 44 | strict: matchStrict, 45 | sensitive: matchSensitive 46 | } : {}); 47 | 48 | const active = (!wantsCondition || !!condition) && (!wantsMatch || !match); 49 | 50 | return active && to 51 | ? ( 52 | 57 | {children} 58 | 59 | ) 60 | : ( 61 | 62 | {children} 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/CopyInputButton.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { Tooltip, Button, ButtonProps, Input } from "antd"; 6 | import { CopyOutlined } from "@ant-design/icons"; 7 | 8 | import { useTranslation } from "react-i18next"; 9 | 10 | type Props = ButtonProps & { 11 | targetInput: React.RefObject; 12 | refocusButton?: boolean; 13 | content?: React.ReactNode; 14 | } 15 | 16 | export function CopyInputButton({ 17 | targetInput, 18 | refocusButton, 19 | content, 20 | ...buttonProps 21 | }: Props): JSX.Element { 22 | const { t } = useTranslation(); 23 | const [showCopied, setShowCopied] = useState(false); 24 | 25 | function copy(e: React.MouseEvent) { 26 | if (!targetInput.current) return; 27 | 28 | // targetInput.current.select(); 29 | targetInput.current.focus({ cursor: "all" }); 30 | document.execCommand("copy"); 31 | 32 | if (refocusButton === undefined || refocusButton) { 33 | e.currentTarget.focus(); 34 | } 35 | 36 | setShowCopied(true); 37 | } 38 | 39 | return { 43 | if (!visible && showCopied) setShowCopied(false); 44 | }} 45 | > 46 | 49 | ; 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Flag.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { HTMLProps } from "react"; 5 | import classNames from "classnames"; 6 | 7 | import "./styles/Flag.css"; 8 | 9 | interface Props extends HTMLProps { 10 | name?: string; 11 | code?: string; 12 | } 13 | 14 | export function Flag({ name, code, className, ...rest }: Props): JSX.Element { 15 | const classes = classNames( 16 | "flag", 17 | code ? "flag-" + code.toLowerCase() : "", 18 | className 19 | ); 20 | 21 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/HelpIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | import { Tooltip } from "antd"; 6 | import { QuestionCircleOutlined } from "@ant-design/icons"; 7 | 8 | import { useTranslation } from "react-i18next"; 9 | 10 | import "./styles/HelpIcon.less"; 11 | 12 | interface Props { 13 | text?: string; 14 | textKey?: string; 15 | className?: string; 16 | } 17 | 18 | export function HelpIcon({ text, textKey, className }: Props): JSX.Element { 19 | const { t } = useTranslation(); 20 | 21 | const classes = classNames("kw-help-icon", className); 22 | 23 | return 24 | 25 | ; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/OptionalField.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | import { Typography } from "antd"; 6 | import { CopyConfig } from "./types"; 7 | 8 | import { useTranslation } from "react-i18next"; 9 | 10 | import "./styles/OptionalField.less"; 11 | 12 | const { Text } = Typography; 13 | 14 | interface Props { 15 | value?: React.ReactNode | null | undefined; 16 | copyable?: boolean | CopyConfig; 17 | unsetKey?: string; 18 | className?: string; 19 | } 20 | 21 | export function OptionalField({ 22 | value, 23 | copyable, 24 | unsetKey, 25 | className 26 | }: Props): JSX.Element { 27 | const { t } = useTranslation(); 28 | 29 | const unset = value === undefined || value === null; 30 | const classes = classNames("optional-field", className, { 31 | "optional-field-unset": unset 32 | }); 33 | 34 | return 35 | {unset 36 | ? t(unsetKey || "optionalFieldUnset") 37 | : {value}} 38 | ; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Statistic.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | import "./styles/Statistic.less"; 9 | 10 | interface Props { 11 | title?: string; 12 | titleKey?: string; 13 | titleExtra?: React.ReactNode; 14 | value?: React.ReactNode; 15 | 16 | className?: string; 17 | green?: boolean; 18 | } 19 | 20 | export function Statistic({ title, titleKey, titleExtra, value, className, green }: Props): JSX.Element { 21 | const { t } = useTranslation(); 22 | 23 | const classes = classNames("kw-statistic", className, { 24 | "kw-statistic-green": green 25 | }); 26 | 27 | return
28 | {titleKey ? t(titleKey) : title}{titleExtra} 29 | {value} 30 |
; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/addresses/ContextualAddress.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .contextual-address { 7 | &:not(.contextual-address-allow-wrap) { 8 | .address-metaname, .address-name, .address-raw-metaname, 9 | .address-wallet { 10 | white-space: nowrap; 11 | } 12 | } 13 | 14 | .address-address, .address-original { 15 | white-space: nowrap; 16 | } 17 | 18 | .address-original { 19 | opacity: 0.8; 20 | } 21 | 22 | &.contextual-address-non-existent { 23 | &, span, a { 24 | color: @text-color-secondary; 25 | 26 | cursor: not-allowed; 27 | 28 | text-decoration-line: underline; 29 | text-decoration-style: dotted; 30 | text-decoration-color: @text-color-secondary; 31 | text-decoration-thickness: 1px; 32 | } 33 | } 34 | 35 | .address-verified { 36 | white-space: nowrap; 37 | word-break: break-word; 38 | 39 | &:not(.address-verified-inactive) { 40 | &, a { 41 | color: fade(@kw-orange, 60%); 42 | 43 | .address-verified-label, .kw-verified-check-icon { 44 | color: @kw-orange; 45 | } 46 | } 47 | } 48 | 49 | .kw-verified-check-icon { 50 | display: inline-block; 51 | font-size: 80%; 52 | margin-left: 0.25em; 53 | 54 | svg { 55 | position: relative; 56 | top: -1px; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/addresses/VerifiedCheck.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | import Icon from "@ant-design/icons"; 6 | 7 | export const VerifiedCheckSvg = (): JSX.Element => ( 8 | 9 | 10 | 11 | ); 12 | export const VerifiedCheck = ({ className, ...props }: any): JSX.Element => 13 | ; 18 | -------------------------------------------------------------------------------- /src/components/addresses/picker/AddressHint.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTranslation, Trans } from "react-i18next"; 5 | 6 | import { KristAddressWithNames } from "@api/lookup"; 7 | import { KristValue } from "@comp/krist/KristValue"; 8 | 9 | interface Props { 10 | address?: KristAddressWithNames; 11 | nameHint?: boolean; 12 | } 13 | 14 | export function AddressHint({ address, nameHint }: Props): JSX.Element { 15 | const { t } = useTranslation(); 16 | 17 | return 18 | {nameHint 19 | ? ( 20 | // Show the name count if this picker is relevant to a name transfer 21 | 22 | Balance: {{ names: address?.names || 0 }} 23 | 24 | ) 25 | : ( 26 | // Otherwise, show the balance 27 | 28 | Balance: 29 | 30 | ) 31 | } 32 | ; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/addresses/picker/AddressPicker.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../../App.less"; 5 | 6 | .address-picker { 7 | margin-bottom: @form-item-margin-bottom; 8 | 9 | .ant-form-item { 10 | margin-bottom: 0; 11 | } 12 | } 13 | 14 | .address-picker-dropdown { 15 | .address-picker-address-item { 16 | display: flex; 17 | flex-direction: row; 18 | align-items: center; 19 | flex-wrap: wrap; 20 | 21 | .krist-value { 22 | flex: 0; 23 | margin-left: auto; 24 | padding-left: @padding-sm; 25 | } 26 | 27 | .address-picker-item-content { 28 | min-width: 0; 29 | flex: 1; 30 | } 31 | 32 | .address-picker-wallet-label, .address-picker-contact-label { 33 | white-space: normal; 34 | word-break: break-word; 35 | } 36 | 37 | .address-picker-wallet-label + .address-picker-wallet-address, 38 | .address-picker-contact-label + .address-picker-contact-address { 39 | color: @text-color-secondary; 40 | } 41 | } 42 | } 43 | 44 | .address-picker-hints { 45 | .address-picker-hint { 46 | // Disallow wrapping within a hint, but still allow the hints themselves to 47 | // be wrapped (see #23) 48 | white-space: nowrap; 49 | word-break: none; 50 | } 51 | 52 | .address-picker-separator { 53 | margin: 0 @padding-xs; 54 | color: @text-color-secondary; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/addresses/picker/Header.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | import { OptionChildren } from "./options"; 6 | 7 | export function getCategoryHeader(category: string): Omit { 8 | return { 9 | label: ( 10 |
11 | {category} 12 |
13 | ), 14 | 15 | // Will possibly be used for filtering. See OptionValue for a comment on 16 | // the naming of this prop. 17 | "data-picker-category": category 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/addresses/picker/NameHint.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Typography } from "antd"; 5 | 6 | import { useTranslation, Trans } from "react-i18next"; 7 | 8 | import { KristName } from "@api/types"; 9 | import { ContextualAddress } from "@comp/addresses/ContextualAddress"; 10 | 11 | const { Text } = Typography; 12 | 13 | interface Props { 14 | name?: KristName; 15 | } 16 | 17 | export function NameHint({ name }: Props): JSX.Element { 18 | const { t } = useTranslation(); 19 | 20 | return 21 | {name 22 | ? ( 23 | 24 | Owner: 25 | 26 | ) 27 | : {t("addressPicker.nameHintNotFound")}} 28 | ; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/addresses/picker/VerifiedHint.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { VerifiedAddress, VerifiedAddressLink } from "@comp/addresses/VerifiedAddress"; 5 | 6 | interface Props { 7 | address: string; 8 | verified: VerifiedAddress; 9 | } 10 | 11 | export function VerifiedHint({ address, verified }: Props): JSX.Element { 12 | return 13 | 14 | ; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/addresses/picker/WalletHint.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTranslation, Trans } from "react-i18next"; 5 | 6 | import { Wallet } from "@wallets"; 7 | import { ContextualAddress } from "@comp/addresses/ContextualAddress"; 8 | 9 | interface Props { 10 | wallet: Wallet; 11 | } 12 | 13 | export function WalletHint({ wallet }: Props): JSX.Element { 14 | const { t } = useTranslation(); 15 | 16 | return 17 | 18 | Owner: 19 | 20 | ; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/auth/AuthMasterPasswordModal.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useRef } from "react"; 5 | import { Modal, Input } from "antd"; 6 | 7 | import { useTFns } from "@utils/i18n"; 8 | 9 | import { useAuthForm } from "./AuthForm"; 10 | 11 | interface Props { 12 | visible: boolean; 13 | encrypt?: boolean; 14 | onCancel: () => void; 15 | onSubmit: () => void; 16 | } 17 | 18 | export function AuthMasterPasswordModal({ 19 | visible, 20 | encrypt, 21 | onCancel, 22 | onSubmit 23 | }: Props): JSX.Element { 24 | const { t, tStr } = useTFns("masterPassword."); 25 | const inputRef = useRef(null); 26 | 27 | const { form, submit, reset } = useAuthForm(encrypt, onSubmit, inputRef); 28 | 29 | return { reset(); onCancel(); }} 38 | onOk={submit} 39 | > 40 | {form} 41 | ; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/auth/AuthorisedAction.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import React, { FC, useContext } from "react"; 5 | import { message } from "antd"; 6 | 7 | import i18n from "@utils/i18n"; 8 | 9 | import { AuthContext, PromptAuthFn } from "./AuthContext"; 10 | 11 | interface Props { 12 | encrypt?: boolean; 13 | onAuthed?: () => void; 14 | children: React.ReactNode; 15 | } 16 | 17 | export const AuthorisedAction: FC = ({ encrypt, onAuthed, children }) => { 18 | const promptAuth = useContext(AuthContext); 19 | 20 | // This is used to pass the 'onClick' prop down to the child. The child MUST 21 | // support the onClick prop. 22 | // NOTE: If the child is a custom component, make sure it passes `...props` 23 | // down to its child. 24 | const child = React.Children.only(children) as React.ReactElement; 25 | 26 | // Wrap the single child element and override onClick 27 | return React.cloneElement(child, { onClick: (e: MouseEvent) => { 28 | e.preventDefault(); 29 | promptAuth?.(encrypt, onAuthed); 30 | }}); 31 | }; 32 | 33 | export const useAuth = (): PromptAuthFn => 34 | useContext(AuthContext) || (() => 35 | message.error(i18n.t("masterPassword.earlyAuthError"))); 36 | -------------------------------------------------------------------------------- /src/components/auth/FakeUsernameInput.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Input } from "antd"; 5 | 6 | /// Fake username field for master password inputs, to trick autofill. 7 | export function FakeUsernameInput(): JSX.Element { 8 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/auth/MasterPasswordInput.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Input } from "antd"; 5 | 6 | interface Props { 7 | inputRef?: React.Ref; 8 | placeholder: string; 9 | tabIndex?: number; 10 | autoFocus?: boolean; 11 | } 12 | 13 | export function getMasterPasswordInput({ inputRef, placeholder, tabIndex, autoFocus }: Props): JSX.Element { 14 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/auth/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | export * from "./AuthorisedAction"; 6 | -------------------------------------------------------------------------------- /src/components/krist/KristSymbol.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import Icon from "@ant-design/icons"; 5 | 6 | export const KristSymbolSvg = (): JSX.Element => ( 7 | 8 | 9 | 10 | ); 11 | export const KristSymbol = (props: any): JSX.Element => 12 | ; 13 | -------------------------------------------------------------------------------- /src/components/krist/KristValue.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .krist-value { 7 | font-size: 100%; 8 | 9 | white-space: nowrap; 10 | 11 | .anticon svg { 12 | /* Hack to make it consistent with Lato */ 13 | position: relative; 14 | bottom: 0.125em; 15 | font-size: 0.75em; 16 | color: @text-color-secondary; 17 | } 18 | 19 | .krist-value-amount { 20 | font-weight: bold; 21 | } 22 | 23 | .krist-currency-long { 24 | color: @text-color-secondary; 25 | 26 | &::before { 27 | content: " "; 28 | } 29 | } 30 | 31 | &.krist-value-green { 32 | color: @kw-green; 33 | 34 | .anticon svg, .krist-currency-long { 35 | color: fade(@kw-green, 75%); 36 | } 37 | } 38 | 39 | &.krist-value-zero { 40 | color: @text-color-secondary; 41 | 42 | .anticon svg, .krist-currency-long { 43 | color: fade(@text-color-secondary, 60%); 44 | } 45 | } 46 | } 47 | 48 | // The currency symbol appears too dark when inside a button 49 | .ant-btn.ant-btn-primary .krist-value .anticon svg { 50 | color: fade(@text-color, 70%); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/krist/KristValue.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import React from "react"; 5 | import classNames from "classnames"; 6 | 7 | import { useSelector } from "react-redux"; 8 | import { RootState } from "@store"; 9 | 10 | import { KristSymbol } from "./KristSymbol"; 11 | 12 | import "./KristValue.less"; 13 | 14 | interface OwnProps { 15 | icon?: React.ReactNode; 16 | value?: number; 17 | long?: boolean; 18 | hideNullish?: boolean; 19 | green?: boolean; 20 | highlightZero?: boolean; 21 | } 22 | type Props = React.HTMLProps & OwnProps; 23 | 24 | export const KristValue = ({ 25 | icon, 26 | value, 27 | long, 28 | hideNullish, 29 | green, 30 | highlightZero, 31 | ...props 32 | }: Props): JSX.Element | null => { 33 | const currencySymbol = useSelector((s: RootState) => s.node.currency.currency_symbol); 34 | 35 | if (hideNullish && (value === undefined || value === null)) return null; 36 | 37 | const classes = classNames("krist-value", props.className, { 38 | "krist-value-green": green, 39 | "krist-value-zero": highlightZero && value === 0 40 | }); 41 | 42 | return ( 43 | 44 | {icon || ((currencySymbol || "KST") === "KST" && )} 45 | {(value || 0).toLocaleString()} 46 | {long && {currencySymbol || "KST"}} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/krist/MarkdownLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC } from "react"; 5 | import { Link } from "react-router-dom"; 6 | 7 | import { useSyncNode } from "@api"; 8 | 9 | // Allow overriding a link to make it open in a new tab and start with baseURL. 10 | // This is usually used by the markdown renderers. 11 | export function useMarkdownLink(baseURL?: string): FC { 12 | // Default for baseURL if not specified 13 | const syncNode = useSyncNode(); 14 | const base = baseURL || syncNode; 15 | 16 | return ({ title, href, children }) => { 17 | // Force the link to start with baseURL/syncNode if it's relative 18 | const absLink = href.startsWith("/") 19 | ? base + href 20 | : href; 21 | 22 | return 27 | {children} 28 | ; 29 | }; 30 | } 31 | 32 | export function useRelativeMarkdownLink(): FC { 33 | return ({ title, href, children }) => { 34 | return 35 | {children} 36 | ; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/names/KristNameLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | import { Typography } from "antd"; 6 | 7 | import { ConditionalLink } from "@comp/ConditionalLink"; 8 | 9 | import { useNameSuffix } from "@utils/krist"; 10 | import { useBooleanSetting } from "@utils/settings"; 11 | 12 | const { Text } = Typography; 13 | 14 | interface OwnProps { 15 | name: string; 16 | text?: string; 17 | noLink?: boolean; 18 | neverCopyable?: boolean; 19 | } 20 | type Props = React.HTMLProps & OwnProps; 21 | 22 | export function KristNameLink({ name, text, noLink, neverCopyable, ...props }: Props): JSX.Element | null { 23 | const nameSuffix = useNameSuffix(); 24 | const nameCopyButtons = useBooleanSetting("nameCopyButtons"); 25 | const copyNameSuffixes = useBooleanSetting("copyNameSuffixes"); 26 | 27 | if (!name) return null; 28 | const nameWithSuffix = `${name}.${nameSuffix}`; 29 | const content = text || nameWithSuffix; 30 | 31 | const copyable = !neverCopyable && nameCopyButtons 32 | ? { text: copyNameSuffixes ? nameWithSuffix : name } 33 | : undefined; 34 | 35 | const classes = classNames("krist-name", props.className); 36 | 37 | return 38 | 43 | {content} 44 | 45 | ; 46 | } 47 | -------------------------------------------------------------------------------- /src/components/names/NameARecordLink.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .name-a-record-link { 7 | background: @kw-darker; 8 | border-radius: @border-radius-base; 9 | 10 | display: inline-block; 11 | margin-top: @padding-xs; 12 | padding: 0.25rem @padding-xs; 13 | 14 | font-size: @font-size-base * 0.9; 15 | font-family: monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/names/NameARecordLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | 6 | import { useNameSuffix, stripNameSuffix } from "@utils/krist"; 7 | 8 | import { KristNameLink } from "./KristNameLink"; 9 | 10 | import "./NameARecordLink.less"; 11 | 12 | interface Props { 13 | a?: string; 14 | className?: string; 15 | } 16 | 17 | export function NameARecordLink({ a, className }: Props): JSX.Element | null { 18 | const nameSuffix = useNameSuffix(); 19 | 20 | if (!a) return null; 21 | 22 | const classes = classNames("name-a-record-link", className); 23 | 24 | // I don't have a citation for this other than a vague memory, but there are 25 | // (as of writing this) 45 names in the database whose A records begin with 26 | // `$` and then point to another name. There is an additional 1 name that 27 | // actually points to a domain, but still begins with `$` and ends with the 28 | // name suffix. 40 of these names end in the `.kst` suffix. Since I cannot 29 | // find any specification or documentation on it right now, I support both 30 | // formats. The suffix is stripped if it is present. 31 | if (a.startsWith("$")) { 32 | // Probably a name redirect 33 | const withoutPrefix = a.replace(/^\$/, ""); 34 | const nameWithoutSuffix = stripNameSuffix(nameSuffix, withoutPrefix); 35 | 36 | return ; 42 | } 43 | 44 | return 45 | {a} 46 | ; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/styles/ConditionalLink.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .conditional-link-disabled { 7 | color: @primary-color; 8 | cursor: pointer; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/styles/DateTime.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .date-time { 7 | &-secondary, &-secondary a, &-secondary time { 8 | color: @text-color-secondary; 9 | } 10 | 11 | &-small, &-small a, &-small time { 12 | font-size: 90%; 13 | 14 | @media (max-width: @screen-xl) { 15 | font-size: 85%; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/styles/HelpIcon.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .kw-help-icon { 7 | display: inline-block; 8 | margin-left: @padding-xs; 9 | 10 | font-size: 90%; 11 | 12 | color: @text-color-secondary; 13 | cursor: pointer; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/styles/OptionalField.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .optional-field { 7 | &.optional-field-unset { 8 | color: @text-color-secondary; 9 | font-style: italic; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/styles/SmallCopyable.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | .small-copyable { 5 | border: 0; 6 | background: transparent; 7 | padding: 0; 8 | line-height: inherit; 9 | display: inline-block; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/styles/Statistic.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .kw-statistic { 7 | &-title { 8 | color: @kw-text-secondary; 9 | display: block; 10 | } 11 | 12 | &-value { 13 | font-size: @heading-3-size; 14 | 15 | .ant-typography-copy { 16 | line-height: 1 !important; 17 | margin-left: @padding-xs; 18 | 19 | .anticon { 20 | font-size: @font-size-base; 21 | vertical-align: 0; 22 | } 23 | } 24 | } 25 | 26 | &-green &-value { 27 | color: @kw-green; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/transactions/SendTransactionModalLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC, useState, useCallback } from "react"; 5 | 6 | import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; 7 | import { SendTransactionModal } from "@pages/transactions/send/SendTransactionModal"; 8 | 9 | import { Wallet } from "@wallets"; 10 | 11 | interface Props { 12 | from?: Wallet | string; 13 | to?: string; 14 | } 15 | 16 | export const SendTransactionModalLink: FC = ({ 17 | from, 18 | to, 19 | children 20 | }): JSX.Element => { 21 | const [modalVisible, setModalVisible] = useState(false); 22 | 23 | return <> 24 | setModalVisible(true)}> 25 | {children} 26 | 27 | 28 | 35 | ; 36 | }; 37 | 38 | export type OpenSendTxFn = (from?: Wallet | string, to?: string) => void; 39 | export type SendTxHookRes = [ 40 | OpenSendTxFn, 41 | JSX.Element | null, 42 | (visible: boolean) => void 43 | ]; 44 | 45 | interface FromTo { 46 | from?: Wallet | string; 47 | to?: string; 48 | } 49 | 50 | export function useSendTransactionModal(): SendTxHookRes { 51 | const [opened, setOpened] = useState(false); 52 | const [visible, setVisible] = useState(false); 53 | const [fromTo, setFromTo] = useState({}); 54 | 55 | const open = useCallback((from?: Wallet | string, to?: string) => { 56 | setFromTo({ from, to }); 57 | setVisible(true); 58 | setOpened(true); 59 | }, []); 60 | 61 | const modal = opened 62 | ? 66 | : null; 67 | 68 | return [open, modal, setVisible]; 69 | } 70 | -------------------------------------------------------------------------------- /src/components/transactions/TransactionConciseMetadata.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .transaction-concise-metadata { 7 | color: @kw-text-tertiary; 8 | font-family: monospace; 9 | font-size: 85%; 10 | 11 | &-truncated::after { 12 | content: "\2026"; 13 | color: @text-color-secondary; 14 | user-select: none; 15 | } 16 | } 17 | 18 | a.transaction-concise-metadata { 19 | &:hover, &:active, &:focus { 20 | color: @text-color; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/transactions/TransactionConciseMetadata.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | 6 | import { Link } from "react-router-dom"; 7 | 8 | import { KristTransaction } from "@api/types"; 9 | import { useNameSuffix, stripNameFromMetadata } from "@utils/krist"; 10 | 11 | import "./TransactionConciseMetadata.less"; 12 | 13 | interface Props { 14 | transaction?: KristTransaction; 15 | metadata?: string; 16 | limit?: number; 17 | className?: string; 18 | } 19 | 20 | /** 21 | * Trims the name and metaname from the start of metadata, and truncates it 22 | * to a specified amount of characters. 23 | */ 24 | export function TransactionConciseMetadata({ 25 | transaction: tx, 26 | metadata, 27 | limit = 30, 28 | className 29 | }: Props): JSX.Element | null { 30 | const nameSuffix = useNameSuffix(); 31 | 32 | // Don't render anything if there's no metadata (after the hooks) 33 | const meta = metadata || tx?.metadata; 34 | if (!meta) return null; 35 | 36 | // Strip the name from the start of the transaction metadata, if it is present 37 | const hasName = tx && (tx.sent_name || tx.sent_metaname); 38 | const withoutName = hasName 39 | ? stripNameFromMetadata(nameSuffix, meta) 40 | : meta; 41 | 42 | // Trim it down to the limit if necessary 43 | const wasTruncated = withoutName.length > limit; 44 | const truncated = wasTruncated ? withoutName.substr(0, limit) : withoutName; 45 | 46 | const classes = classNames("transaction-concise-metadata", className, { 47 | "transaction-concise-metadata-truncated": wasTruncated 48 | }); 49 | 50 | // Link to the transaction if it is available 51 | return tx 52 | ? ( 53 | 57 | {truncated} 58 | 59 | ) 60 | : {truncated}; 61 | } 62 | -------------------------------------------------------------------------------- /src/components/transactions/TransactionSummary.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Row } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | import { Link } from "react-router-dom"; 8 | 9 | import { useWallets } from "@wallets"; 10 | 11 | import { KristTransaction } from "@api/types"; 12 | import { TransactionItem } from "./TransactionItem"; 13 | 14 | import "./TransactionSummary.less"; 15 | 16 | interface Props { 17 | transactions?: KristTransaction[]; 18 | 19 | seeMoreCount?: number; 20 | seeMoreKey?: string; 21 | seeMoreLink?: string; 22 | } 23 | 24 | export function TransactionSummary({ transactions, seeMoreCount, seeMoreKey, seeMoreLink }: Props): JSX.Element { 25 | const { t } = useTranslation(); 26 | const { walletAddressMap } = useWallets(); 27 | 28 | return <> 29 | {transactions && transactions.map(t => ( 30 | 35 | ))} 36 | 37 | {seeMoreCount !== undefined && 38 | 39 | {t(seeMoreKey || "transactionSummary.seeMore", { count: seeMoreCount })} 40 | 41 | } 42 | ; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/transactions/TransactionType.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .transaction-type { 7 | &, a { 8 | user-select: none; 9 | 10 | font-weight: bold; 11 | color: @text-color-secondary; 12 | } 13 | 14 | &-transferred, &-name_transferred { 15 | &, a { color: @kw-primary; } 16 | } 17 | &-sent, &-name_sent, &-name_purchased { 18 | &, a { color: @kw-orange; } 19 | } 20 | &-received, &-mined, &-name_received { 21 | &, a { color: @kw-green; } 22 | } 23 | &-name_a_record { 24 | &, a { color: @kw-purple; } 25 | } 26 | &-bumped { 27 | &, a { color: @kw-text-tertiary; } 28 | } 29 | 30 | &-no-link { 31 | &, a { cursor: default; } 32 | } 33 | 34 | @media (max-width: @screen-xl) { 35 | font-size: 90%; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | /** CopyConfig from ant-design (antd/lib/typography/Base.d.ts) */ 6 | export interface CopyConfig { 7 | text?: string; 8 | onCopy?: () => void; 9 | icon?: React.ReactNode; 10 | tooltips?: boolean | React.ReactNode; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/wallets/SelectWalletFormat.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Select } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | import { WalletFormatName, ADVANCED_FORMATS } from "@wallets"; 9 | import { useBooleanSetting } from "@utils/settings"; 10 | 11 | interface Props { 12 | initialFormat: WalletFormatName; 13 | } 14 | 15 | export function SelectWalletFormat({ initialFormat }: Props): JSX.Element { 16 | const advancedWalletFormats = useBooleanSetting("walletFormats"); 17 | const { t } = useTranslation(); 18 | 19 | return ; 30 | } 31 | -------------------------------------------------------------------------------- /src/global/AppHotkeys.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | import { useHistory } from "react-router-dom"; 6 | import { GlobalHotKeys } from "react-hotkeys"; 7 | 8 | export function AppHotkeys(): JSX.Element { 9 | const history = useHistory(); 10 | 11 | return history.push("/dev") 15 | }} 16 | />; 17 | } 18 | -------------------------------------------------------------------------------- /src/global/AppLoading.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | export function AppLoading(): JSX.Element { 6 | return
7 | {/* Spinner */} 8 |
9 | 10 | {/* Loading hint */} 11 | {/* NOTE: This is not translated, as usually this component is shown when 12 | the translations are being loaded! */} 13 | setsetstsetsetestLoading KristWeb... 14 |
; 15 | } 16 | -------------------------------------------------------------------------------- /src/global/AppServices.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { StorageBroadcast } from "./StorageBroadcast"; 5 | import { LegacyMigration } from "./legacy/LegacyMigration"; 6 | import { SyncWallets } from "@comp/wallets/SyncWallets"; 7 | import { ForcedAuth } from "./ForcedAuth"; 8 | import { WebsocketService } from "./ws/WebsocketService"; 9 | import { SyncMOTD } from "./ws/SyncMOTD"; 10 | import { AppHotkeys } from "./AppHotkeys"; 11 | import { PurchaseKristHandler } from "./PurchaseKrist"; 12 | import { AdvanceTip } from "@pages/dashboard/TipsCard"; 13 | 14 | export function AppServices(): JSX.Element { 15 | return <> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ; 26 | } 27 | -------------------------------------------------------------------------------- /src/global/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC } from "react"; 5 | import { Alert } from "antd"; 6 | 7 | import { useTFns } from "@utils/i18n"; 8 | 9 | import * as Sentry from "@sentry/react"; 10 | import { errorReporting } from "@utils"; 11 | 12 | interface Props { 13 | name: string; 14 | } 15 | 16 | export const ErrorBoundary: FC = ({ name, children }) => { 17 | return } 19 | onError={console.error} 20 | 21 | // Add the boundary name to the scope 22 | beforeCapture={scope => { 23 | scope.setTag("error-boundary", name); 24 | }} 25 | > 26 | {children} 27 | ; 28 | }; 29 | 30 | function ErrorFallback(): JSX.Element { 31 | const { tStr } = useTFns("errorBoundary."); 32 | 33 | return 39 |

{tStr("description")}

40 | 41 | {/* If Sentry error reporting is enabled, add a message saying the error 42 | * was automatically reported. */} 43 | {errorReporting && ( 44 |

{tStr("sentryNote")}

45 | )} 46 | } 47 | />; 48 | } 49 | -------------------------------------------------------------------------------- /src/global/ForcedAuth.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { message } from "antd"; 5 | import { useTranslation, TFunction } from "react-i18next"; 6 | 7 | import { authMasterPassword, useMasterPassword } from "@wallets"; 8 | 9 | import { useMountEffect } from "@utils/hooks"; 10 | import { criticalError } from "@utils"; 11 | 12 | async function forceAuth(t: TFunction, salt: string, tester: string): Promise { 13 | try { 14 | const password = localStorage.getItem("forcedAuth"); 15 | if (!password) return; 16 | 17 | await authMasterPassword(salt, tester, password); 18 | message.warning(t("masterPassword.forcedAuthWarning")); 19 | } catch (e) { 20 | criticalError(e); 21 | } 22 | } 23 | 24 | /** For development purposes, check the presence of a local storage key 25 | * containing the master password, and automatically authenticate with it. */ 26 | export function ForcedAuth(): JSX.Element | null { 27 | const { isAuthed, hasMasterPassword, salt, tester } 28 | = useMasterPassword(); 29 | 30 | const { t } = useTranslation(); 31 | 32 | useMountEffect(() => { 33 | if (isAuthed || !hasMasterPassword || !salt || !tester) return; 34 | forceAuth(t, salt, tester); 35 | }); 36 | 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /src/global/compat/CompatCheckModal.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .compat-check-modal { 7 | .ant-modal-confirm-btns { 8 | display: none; 9 | } 10 | 11 | .browser-choices { 12 | display: flex; 13 | flex-direction: row; 14 | 15 | // Remove the offset from the confirm modal body padding 16 | margin-left: -38px; 17 | padding-top: @margin-sm; 18 | 19 | a { 20 | flex: 1; 21 | 22 | display: flex; 23 | flex-direction: column; 24 | 25 | padding: @margin-sm; 26 | 27 | color: @text-color; 28 | text-align: center; 29 | background: transparent; 30 | border-radius: @border-radius-base; 31 | transition: all @animation-duration-base ease; 32 | 33 | &:hover { 34 | background: @kw-lighter; 35 | } 36 | 37 | img { 38 | width: 96px; 39 | margin: 0 auto @margin-md auto; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/global/compat/localStorage.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | // Implementation sourced from MDN: 6 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage 7 | export function localStorageAvailable( 8 | type: "localStorage" | "sessionStorage" = "localStorage" 9 | ): boolean { 10 | let storage; 11 | try { 12 | storage = window[type]; 13 | const x = "__storage_test__"; 14 | storage.setItem(x, x); 15 | storage.removeItem(x); 16 | return true; 17 | } catch(e) { 18 | return e instanceof DOMException && ( 19 | // everything except Firefox 20 | e.code === 22 || 21 | // Firefox 22 | e.code === 1014 || 23 | // test name field too, because code might not be present 24 | // everything except Firefox 25 | e.name === "QuotaExceededError" || 26 | // Firefox 27 | e.name === "NS_ERROR_DOM_QUOTA_REACHED") && 28 | // acknowledge QuotaExceededError only if there's something already stored 29 | (!!storage && storage.length !== 0); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/global/ws/SyncDetailedWork.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useEffect } from "react"; 5 | 6 | import { useSelector } from "react-redux"; 7 | import { RootState } from "@store"; 8 | import * as nodeActions from "@actions/NodeActions"; 9 | 10 | import { store } from "@app"; 11 | 12 | import * as api from "@api"; 13 | import { KristWorkDetailed } from "@api/types"; 14 | 15 | import { criticalError } from "@utils"; 16 | 17 | import Debug from "debug"; 18 | const debug = Debug("kristweb:sync-work"); 19 | 20 | export async function updateDetailedWork(): Promise { 21 | debug("updating detailed work"); 22 | const data = await api.get("work/detailed"); 23 | 24 | debug("work: %d", data.work); 25 | store.dispatch(nodeActions.setDetailedWork(data)); 26 | } 27 | 28 | /** Sync the detailed work with the Krist node on startup. */ 29 | export function SyncDetailedWork(): JSX.Element | null { 30 | const { lastBlockID } = useSelector((s: RootState) => s.node); 31 | 32 | useEffect(() => { 33 | updateDetailedWork().catch(criticalError); 34 | }, [lastBlockID]); 35 | 36 | return null; 37 | } 38 | -------------------------------------------------------------------------------- /src/global/ws/WebsocketProvider.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC, createContext, useState, Dispatch, SetStateAction } from "react"; 5 | 6 | import { WebsocketConnection } from "./WebsocketConnection"; 7 | 8 | import Debug from "debug"; 9 | const debug = Debug("kristweb:websocket-provider"); 10 | 11 | export interface WSContextType { 12 | connection?: WebsocketConnection; 13 | setConnection?: Dispatch>; 14 | } 15 | export const WebsocketContext = createContext({}); 16 | 17 | export const WebsocketProvider: FC = ({ children }): JSX.Element => { 18 | const [connection, setConnection] = useState(); 19 | 20 | debug("ws provider re-rendering"); 21 | 22 | return 23 | {children} 24 | ; 25 | }; 26 | -------------------------------------------------------------------------------- /src/global/ws/WebsocketService.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useEffect, useContext } from "react"; 5 | 6 | import { WebsocketContext } from "./WebsocketProvider"; 7 | import { WebsocketConnection } from "./WebsocketConnection"; 8 | 9 | import * as api from "@api"; 10 | import { useWallets } from "@wallets"; 11 | 12 | import Debug from "debug"; 13 | const debug = Debug("kristweb:websocket-service"); 14 | 15 | export function WebsocketService(): JSX.Element | null { 16 | const { wallets } = useWallets(); 17 | const syncNode = api.useSyncNode(); 18 | 19 | const { connection, setConnection } = useContext(WebsocketContext); 20 | 21 | // On first render, or if the sync node changes, create the websocket 22 | // connection 23 | useEffect(() => { 24 | // Don't reconnect if we already have a connection and the sync node hasn't 25 | // changed (prevents infinite loops) 26 | if (connection && connection.syncNode === syncNode) return; 27 | 28 | // Close any existing connections 29 | if (connection) connection.forceClose(); 30 | 31 | if (!setConnection) { 32 | debug("ws provider setConnection is missing!"); 33 | return; 34 | } 35 | 36 | // Connect to the Krist websocket server 37 | setConnection(new WebsocketConnection(syncNode)); 38 | 39 | // On unmount, force close the existing connection 40 | return () => { 41 | if (connection) connection.forceClose(); 42 | }; 43 | }, [syncNode, connection, setConnection]); 44 | 45 | // If the wallets change, let the websocket service know so that it can keep 46 | // track of events related to any new wallets 47 | useEffect(() => { 48 | if (connection) connection.setWallets(wallets); 49 | }, [wallets, connection]); 50 | 51 | return null; 52 | } 53 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2020-2021 Drew Lemmy 2 | * This file is part of KristWeb 2 under AGPL-3.0. 3 | * Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt */ 4 | body { 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 7 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 8 | sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 15 | monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/krist/api/login.ts: -------------------------------------------------------------------------------- 1 | import { TranslatedError } from "@utils/i18n"; 2 | 3 | import { store } from "@app"; 4 | 5 | import { decryptWallet, syncWallet, Wallet } from "@wallets"; 6 | import * as api from "@api"; 7 | 8 | export async function loginWallet(wallet: Wallet): Promise { 9 | const masterPassword = store.getState().masterPassword.masterPassword; 10 | if (!masterPassword) throw new TranslatedError("myWallets.login.masterPasswordRequired"); 11 | 12 | const decrypted = await decryptWallet(masterPassword, wallet); 13 | if (!decrypted) throw new TranslatedError("myWallets.login.errorWalletDecrypt"); 14 | 15 | try { 16 | // Call /login to force create the address 17 | const { authed } = await api.post("/login", { 18 | privatekey: decrypted.privatekey 19 | }); 20 | if (!authed) throw new TranslatedError("myWallets.login.errorAuthFailed"); 21 | 22 | // Fetch the updated address and store it in the Redux store 23 | syncWallet(wallet); 24 | } catch (e) { 25 | console.error(e); 26 | throw new TranslatedError("myWallets.login.errorAuthFailed"); 27 | } 28 | } 29 | 30 | interface LoginRes { 31 | authed: boolean; 32 | address?: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/krist/api/search.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { KristAddress, KristBlock, KristName, KristTransaction } from "./types"; 5 | import * as api from "."; 6 | 7 | export interface SearchQueryMatch { 8 | originalQuery: string; 9 | matchedAddress: boolean; 10 | matchedName: boolean; 11 | matchedBlock: boolean; 12 | matchedTransaction: boolean; 13 | strippedName: string; 14 | } 15 | 16 | export interface SearchResult { 17 | query: SearchQueryMatch; 18 | 19 | matches: { 20 | exactAddress: KristAddress | false; 21 | exactName: KristName | false; 22 | exactBlock: KristBlock | false; 23 | exactTransaction: KristTransaction | false; 24 | }; 25 | } 26 | 27 | export interface SearchExtendedResult { 28 | query: SearchQueryMatch; 29 | 30 | matches: { 31 | transactions: { 32 | addressInvolved: number | false; 33 | nameInvolved: number | false; 34 | metadata: number | false; 35 | }; 36 | }; 37 | } 38 | 39 | export async function search(query?: string): Promise { 40 | if (!query) return; 41 | 42 | return api.get( 43 | "search?q=" + encodeURIComponent(query), 44 | 45 | // Don't show the rate limit notification if it is hit, a message will be 46 | // shown in the search box instead 47 | { ignoreRateLimit: true } 48 | ); 49 | } 50 | 51 | export async function searchExtended(query?: string): Promise { 52 | if (!query || query.length < 3) return; 53 | 54 | return api.get( 55 | "search/extended?q=" + encodeURIComponent(query), 56 | 57 | // Don't show the rate limit notification if it is hit, a message will be 58 | // shown in the search box instead 59 | { ignoreRateLimit: true } 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/krist/api/transactions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { TranslatedError } from "@utils/i18n"; 5 | 6 | import { KristTransaction } from "./types"; 7 | import * as api from "."; 8 | 9 | import { Wallet, decryptWallet } from "@wallets"; 10 | 11 | interface MakeTransactionResponse { 12 | transaction: KristTransaction; 13 | } 14 | 15 | export async function makeTransaction( 16 | masterPassword: string, 17 | from: Wallet, 18 | to: string, 19 | amount: number, 20 | metadata?: string 21 | ): Promise { 22 | // Attempt to decrypt the wallet to get the privatekey 23 | const decrypted = await decryptWallet(masterPassword, from); 24 | if (!decrypted) 25 | throw new TranslatedError("sendTransaction.errorWalletDecrypt"); 26 | const { privatekey } = decrypted; 27 | 28 | const { transaction } = await api.post( 29 | "/transactions", 30 | { 31 | privatekey, to, amount, 32 | metadata: metadata || undefined // Clean up empty strings 33 | } 34 | ); 35 | 36 | return transaction; 37 | } 38 | -------------------------------------------------------------------------------- /src/krist/contacts/Contact.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export interface Contact { 5 | // UUID for this contact 6 | id: string; 7 | 8 | address: string; 9 | label?: string; 10 | isName?: boolean; 11 | } 12 | 13 | export interface ContactMap { [key: string]: Contact } 14 | 15 | /** Properties of Contact that are required to create a new contact. */ 16 | export type ContactNewKeys = "address" | "label" | "isName"; 17 | export type ContactNew = Pick; 18 | 19 | /** Properties of Contact that are allowed to be updated. */ 20 | export type ContactUpdatableKeys = "address" | "label" | "isName"; 21 | export const CONTACT_UPDATABLE_KEYS: ContactUpdatableKeys[] 22 | = ["address", "label", "isName"]; 23 | export type ContactUpdatable = Pick; 24 | -------------------------------------------------------------------------------- /src/krist/contacts/functions/addContact.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { v4 as uuid } from "uuid"; 5 | 6 | import { store } from "@app"; 7 | import * as actions from "@actions/ContactsActions"; 8 | 9 | import { Contact, ContactNew, saveContact } from ".."; 10 | import { broadcastAddContact } from "@global/StorageBroadcast"; 11 | 12 | /** 13 | * Adds a new contact, saving it to locale storage, and dispatching the changes 14 | * to the Redux store. 15 | * 16 | * @param contact - The information for the new contact. 17 | */ 18 | export function addContact(contact: ContactNew): Contact { 19 | const id = uuid(); 20 | 21 | const newContact = { 22 | id, 23 | address: contact.address, 24 | label: contact.label?.trim() || undefined, 25 | isName: contact.isName 26 | }; 27 | 28 | // Save the contact to local storage 29 | saveContact(newContact); 30 | broadcastAddContact(newContact.id); // Broadcast changes to other tabs 31 | 32 | // Dispatch the changes to the Redux store 33 | store.dispatch(actions.addContact(newContact)); 34 | 35 | return newContact; 36 | } 37 | -------------------------------------------------------------------------------- /src/krist/contacts/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export * from "./Contact"; 5 | export * from "./functions/addContact"; 6 | export * from "./functions/editContact"; 7 | export * from "./contactStorage"; 8 | export * from "./utils"; 9 | -------------------------------------------------------------------------------- /src/krist/contacts/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useSelector, shallowEqual } from "react-redux"; 5 | import { RootState } from "@store"; 6 | 7 | import { Contact, ContactMap } from "."; 8 | 9 | export type ContactAddressMap = Record; 10 | 11 | export interface ContactsHookResponse { 12 | contacts: ContactMap; 13 | contactAddressMap: ContactAddressMap; 14 | 15 | contactAddressList: string[]; 16 | joinedContactAddressList: string; 17 | } 18 | 19 | /** Hook that fetches the contacts from the Redux store. */ 20 | export function useContacts(): ContactsHookResponse { 21 | const contacts = useSelector((s: RootState) => s.contacts.contacts, shallowEqual); 22 | 23 | const contactAddressMap: ContactAddressMap = {}; 24 | const contactAddressList: string[] = []; 25 | 26 | for (const id in contacts) { 27 | const contact = contacts[id]; 28 | const address = contact.address; 29 | 30 | contactAddressMap[address] = contact; 31 | contactAddressList.push(address); 32 | } 33 | 34 | const joinedContactAddressList = contactAddressList.join(","); 35 | 36 | return { 37 | contacts, contactAddressMap, 38 | contactAddressList, joinedContactAddressList 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/krist/wallets/Wallet.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { WalletFormatName } from "."; 5 | 6 | export interface Wallet { 7 | // UUID for this wallet 8 | id: string; 9 | 10 | // User assignable data 11 | label?: string; 12 | category?: string; 13 | 14 | // Login info 15 | encPassword: string; // Encrypted with master password, decrypted on-demand 16 | encPrivatekey: string; // The password with the password + wallet format applied 17 | username?: string; 18 | format: WalletFormatName; 19 | 20 | // Fetched from API 21 | address: string; 22 | balance?: number; 23 | names?: number; 24 | firstSeen?: string; 25 | lastSynced?: string; 26 | 27 | dontSave?: boolean; // Used to avoid saving when syncing 28 | } 29 | 30 | export interface WalletMap { [key: string]: Wallet } 31 | 32 | /** Properties of Wallet that are required to create a new wallet. */ 33 | export type WalletNewKeys = "label" | "category" | "username" | "format" | "dontSave"; 34 | export type WalletNew = Pick; 35 | 36 | /** Properties of Wallet that are allowed to be updated. */ 37 | export type WalletUpdatableKeys 38 | = "label" | "category" | "encPassword" | "encPrivatekey" | "username" | "format" | "address"; 39 | export const WALLET_UPDATABLE_KEYS: WalletUpdatableKeys[] 40 | = ["label", "category", "encPassword", "encPrivatekey", "username", "format", "address"]; 41 | export type WalletUpdatable = Pick; 42 | 43 | /** Properties of Wallet that are allowed to be synced. */ 44 | export type WalletSyncableKeys 45 | = "balance" | "names" | "firstSeen" | "lastSynced"; 46 | export const WALLET_SYNCABLE_KEYS: WalletSyncableKeys[] 47 | = ["balance", "names", "firstSeen", "lastSynced"]; 48 | export type WalletSyncable = Pick; 49 | -------------------------------------------------------------------------------- /src/krist/wallets/functions/resetMasterPassword.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { store } from "@app"; 5 | 6 | import { getWalletKey } from "@wallets"; 7 | 8 | export function resetMasterPassword(): void { 9 | // Remove the master password from local storage 10 | localStorage.removeItem("salt2"); 11 | localStorage.removeItem("tester2"); 12 | 13 | // Find and remove all the wallets 14 | const wallets = store.getState().wallets.wallets; 15 | for (const id in wallets) { 16 | const key = getWalletKey(id); 17 | localStorage.removeItem(key); 18 | } 19 | 20 | // Reload the page and return to the dashboard 21 | location.href = "/"; 22 | } 23 | -------------------------------------------------------------------------------- /src/krist/wallets/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export * from "./Wallet"; 5 | export * from "./walletFormats"; 6 | export * from "./functions/addWallet"; 7 | export * from "./functions/editWallet"; 8 | export * from "./functions/syncWallets"; 9 | export * from "./functions/decryptWallet"; 10 | export * from "./functions/recalculateWallets"; 11 | export * from "./functions/resetMasterPassword"; 12 | export * from "./masterPassword"; 13 | export * from "./walletStorage"; 14 | export * from "./utils"; 15 | -------------------------------------------------------------------------------- /src/krist/wallets/walletFormats.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { sha256 } from "@utils/crypto"; 5 | 6 | export interface WalletFormat { 7 | (password: string, username?: string): Promise; 8 | } 9 | 10 | export type WalletFormatName = "kristwallet" | "kristwallet_username_appendhashes" | "kristwallet_username" | "jwalelset" | "api"; 11 | export const WALLET_FORMATS: Record = { 12 | "kristwallet": async password => 13 | await sha256("KRISTWALLET" + password) + "-000", 14 | 15 | "kristwallet_username_appendhashes": async (password, username) => 16 | await sha256("KRISTWALLETEXTENSION" + await sha256(await sha256(username || "") + "^" + await sha256(password))) + "-000", 17 | 18 | "kristwallet_username": async (password, username) => 19 | await sha256(await sha256(username || "") + "^" + await sha256(password)), 20 | 21 | "jwalelset": async password => 22 | await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(await sha256(password)))))))))))))))))), 23 | 24 | "api": async password => password 25 | }; 26 | export const ADVANCED_FORMATS: WalletFormatName[] = [ 27 | "kristwallet_username_appendhashes", "kristwallet_username", "jwalelset" 28 | ]; 29 | 30 | export const applyWalletFormat = 31 | (format: WalletFormatName, password: string, username?: string): Promise => 32 | WALLET_FORMATS[format](password, username); 33 | 34 | export const formatNeedsUsername = (format: WalletFormatName): boolean => 35 | WALLET_FORMATS[format].length === 2; 36 | -------------------------------------------------------------------------------- /src/layout/AppLayout.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../App.less"; 5 | 6 | .site-layout { 7 | min-height: calc(100vh - @layout-header-height); 8 | 9 | margin-left: @kw-sidebar-width; 10 | 11 | &.site-layout-mobile { 12 | margin-left: 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/layout/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { Layout, Grid } from "antd"; 6 | 7 | import { AppHeader } from "./nav/AppHeader"; 8 | import { Sidebar } from "./sidebar/Sidebar"; 9 | import { AppRouter } from "../global/AppRouter"; 10 | 11 | import { TopMenuProvider } from "./nav/TopMenu"; 12 | 13 | import "./AppLayout.less"; 14 | 15 | const { useBreakpoint } = Grid; 16 | 17 | export function AppLayout(): JSX.Element { 18 | const [sidebarCollapsed, setSidebarCollapsed] = useState(true); 19 | const bps = useBreakpoint(); 20 | 21 | return 22 | 23 | 27 | 28 | 29 | 33 | 34 | {/* Fade out the background when the sidebar is open on mobile */} 35 | {!bps.md &&
setSidebarCollapsed(true)} 38 | />} 39 | 40 | 41 | 42 | 43 | 44 | 45 | ; 46 | } 47 | -------------------------------------------------------------------------------- /src/layout/PageLayout.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../App.less"; 5 | 6 | .page-layout { 7 | height: 100%; 8 | 9 | .page-layout-header.ant-page-header { 10 | min-height: @kw-page-header-height; 11 | 12 | padding-bottom: 0; 13 | 14 | .ant-page-header-heading-sub-title { 15 | .ant-typography { 16 | color: inherit; 17 | } 18 | } 19 | 20 | // Hide extra table pagination on mobile 21 | @media (max-width: @screen-md) { 22 | .ant-pagination { 23 | display: none; 24 | } 25 | } 26 | } 27 | 28 | .page-layout-contents { 29 | height: calc(100% - @kw-page-header-height); 30 | 31 | padding: @padding-lg; 32 | 33 | // Make tables full-width on mobile (though most should be replaced with 34 | // custom views) 35 | @media (max-width: @screen-md) { 36 | padding: @padding-md; 37 | 38 | >.ant-table-wrapper { 39 | .ant-table { 40 | margin: 0 -@padding-lg; 41 | } 42 | 43 | .ant-pagination { 44 | justify-content: center; 45 | 46 | .ant-pagination-total-text { 47 | display: none; 48 | } 49 | 50 | .ant-pagination-item, .ant-pagination-prev { 51 | margin-right: 3px; 52 | } 53 | 54 | .ant-pagination-next { 55 | margin-right: 0; 56 | } 57 | } 58 | } 59 | } 60 | 61 | @media (max-width: @screen-sm) { 62 | padding: @padding-sm; 63 | } 64 | } 65 | 66 | &.page-layout-no-top-padding { 67 | .page-layout-contents { 68 | padding-top: 0; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/layout/nav/Brand.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Tag } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | import semverMajor from "semver/functions/major"; 9 | import semverMinor from "semver/functions/minor"; 10 | import semverPatch from "semver/functions/patch"; 11 | import semverPrerelease from "semver/functions/prerelease"; 12 | 13 | import { ConditionalLink } from "@comp/ConditionalLink"; 14 | 15 | import { getDevState } from "@utils"; 16 | 17 | declare const __GIT_VERSION__: string; 18 | 19 | const prereleaseTagColours: { [key: string]: string } = { 20 | "dev": "red", 21 | "alpha": "orange", 22 | "beta": "blue", 23 | "rc": "green" 24 | }; 25 | 26 | const GIT_RE = /^\d+-g[a-f0-9]{5,32}(?:-dirty)?$/; 27 | 28 | export function Brand(): JSX.Element { 29 | const { t } = useTranslation(); 30 | 31 | const gitVersion: string = __GIT_VERSION__; 32 | 33 | const major = semverMajor(gitVersion); 34 | const minor = semverMinor(gitVersion); 35 | const patch = semverPatch(gitVersion); 36 | const prerelease = semverPrerelease(gitVersion); 37 | 38 | const isGit = prerelease ? GIT_RE.test(prerelease.join("")) : false; 39 | 40 | const { isDirty, isDev } = getDevState(); 41 | 42 | // Convert semver prerelease parts to Bootstrap badge 43 | const tagContents = isDirty || isDev 44 | ? ["dev"] 45 | : (isGit ? null : prerelease); 46 | let tag = null; 47 | if (tagContents && tagContents.length) { 48 | const variant = prereleaseTagColours[tagContents[0]] || undefined; 49 | tag = {tagContents.join(".")}; 50 | } 51 | 52 | return
53 | 54 | 55 | {t("app.name")} 56 | v{major}.{minor}.{patch} 57 | {tag} 58 | 59 |
; 60 | } 61 | -------------------------------------------------------------------------------- /src/layout/nav/ConnectionIndicator.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .connection-indicator { 7 | vertical-align: middle; 8 | line-height: @layout-header-height; 9 | 10 | &::after { 11 | content: " "; 12 | display: inline-block; 13 | 14 | width: 12px; 15 | height: 12px; 16 | border-radius: 50%; 17 | 18 | background-color: @kw-green; 19 | box-shadow: 0 0 0 3px rgba(@kw-green, 0.3); 20 | } 21 | 22 | &.connection-connecting::after { 23 | background-color: @kw-secondary; 24 | box-shadow: 0 0 0 3px rgba(@kw-secondary, 0.3); 25 | } 26 | 27 | &.connection-disconnected::after { 28 | background-color: @kw-red; 29 | box-shadow: 0 0 0 3px rgba(@kw-red, 0.3); 30 | } 31 | } 32 | 33 | .site-header-element.connection-indicator-el { 34 | // Hide the connection indicator entirely on very small screens 35 | @media (max-width: 350px) { 36 | display: none !important; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/layout/nav/ConnectionIndicator.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Tooltip } from "antd"; 5 | 6 | import { useSelector } from "react-redux"; 7 | import { RootState } from "@store"; 8 | import { useTranslation } from "react-i18next"; 9 | 10 | import { WSConnectionState } from "@api/types"; 11 | 12 | import "./ConnectionIndicator.less"; 13 | 14 | const CONN_STATE_TOOLTIPS: Record = { 15 | "connected": "nav.connection.online", 16 | "disconnected": "nav.connection.offline", 17 | "connecting": "nav.connection.connecting" 18 | }; 19 | 20 | export function ConnectionIndicator(): JSX.Element { 21 | const { t } = useTranslation(); 22 | const connectionState = useSelector((s: RootState) => s.websocket.connectionState); 23 | 24 | return
25 | 26 |
27 | 28 |
; 29 | } 30 | -------------------------------------------------------------------------------- /src/layout/sidebar/SidebarFooter.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTranslation, Trans } from "react-i18next"; 5 | 6 | import { getAuthorInfo, useHostInfo } from "@utils"; 7 | 8 | import { ConditionalLink } from "@comp/ConditionalLink"; 9 | 10 | declare const __GIT_VERSION__: string; 11 | declare const __PKGBUILD__: string; 12 | 13 | export function SidebarFooter(): JSX.Element { 14 | const { t } = useTranslation(); 15 | 16 | const { authorName, authorURL, gitURL } = getAuthorInfo(); 17 | const host = useHostInfo(); 18 | 19 | // Replaced by webpack DefinePlugin and git-revision-webpack-plugin 20 | const gitVersion: string = __GIT_VERSION__; 21 | const pkgbuild = __PKGBUILD__; 22 | 23 | return ( 24 |
25 |
26 | Made by {{authorName}} 27 |
28 | { host && 29 |
30 | Hosted by {{ host: host.host.name }} 31 |
32 | } 33 |
34 | {t("sidebar.github")} 35 |  –  36 | 37 | {t("sidebar.whatsNew")} 38 | 39 |  –  40 | 41 | {t("sidebar.credits")} 42 | 43 |
44 | 45 | {/* Git describe version */} 46 |
47 | {gitVersion}-{pkgbuild} 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/layout/sidebar/SidebarTotalBalance.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import { useWallets } from "@wallets"; 7 | import { KristValue } from "@comp/krist/KristValue"; 8 | 9 | export function SidebarTotalBalance(): JSX.Element { 10 | const { t } = useTranslation(); 11 | 12 | const { wallets } = useWallets(); 13 | const balance = Object.values(wallets) 14 | .filter(w => w.balance !== undefined) 15 | .reduce((acc, w) => acc + w.balance!, 0); 16 | 17 | return ( 18 |
19 |
{t("sidebar.totalBalance")}
20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Button } from "antd"; 5 | import { FrownOutlined } from "@ant-design/icons"; 6 | 7 | import { useHistory } from "react-router-dom"; 8 | import { useTFns } from "@utils/i18n"; 9 | 10 | import { SmallResult } from "@comp/results/SmallResult"; 11 | 12 | interface Props { 13 | nyi?: boolean; 14 | } 15 | 16 | export function NotFoundPage({ nyi }: Props): JSX.Element { 17 | const { tStr } = useTFns("pageNotFound."); 18 | const history = useHistory(); 19 | 20 | return } 22 | status="error" 23 | title={nyi ? tStr("nyiTitle") : tStr("resultTitle")} 24 | subTitle={nyi ? tStr("nyiSubTitle") : undefined} 25 | extra={( 26 | 29 | )} 30 | fullPage 31 | />; 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/addresses/AddressPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .address-page { 7 | .top-address-row { 8 | display: flex; 9 | align-items: center; 10 | 11 | .address { 12 | display: inline-block; 13 | margin-right: @margin-lg; 14 | margin-bottom: 0; 15 | 16 | font-size: @font-size-base * 2; 17 | font-weight: 500; 18 | 19 | .ant-typography-copy { 20 | line-height: 1 !important; 21 | margin-left: @padding-xs; 22 | 23 | .anticon { 24 | font-size: @font-size-base; 25 | vertical-align: 0; 26 | } 27 | } 28 | } 29 | 30 | .ant-btn { 31 | margin-right: @margin-md; 32 | } 33 | 34 | > .ant-btn:last-child { margin-right: 0; } 35 | } 36 | 37 | .address-wallet-row { 38 | margin-top: @padding-xs; 39 | font-size: 90%; 40 | 41 | .prefix { 42 | margin-right: @padding-xs; 43 | } 44 | 45 | .address-wallet-verified, 46 | .address-wallet-label, 47 | .address-wallet-category, 48 | .address-wallet-contact { 49 | &:not(:last-child) { 50 | margin-right: @padding-xs; 51 | } 52 | } 53 | } 54 | 55 | .address-verified-description-row { 56 | margin-top: @margin-md; 57 | margin-bottom: @margin-md; 58 | } 59 | 60 | .address-info-row { 61 | max-width: 768px; 62 | margin-bottom: @margin-lg; 63 | 64 | .kw-statistic { 65 | margin-top: @margin-lg; 66 | } 67 | } 68 | 69 | .address-card-row { 70 | & > .ant-col { 71 | margin-bottom: @margin-md; 72 | } 73 | } 74 | 75 | .address-card-names { 76 | .address-name-item { 77 | display: block; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/pages/addresses/NameItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Row } from "antd"; 5 | 6 | import { useTranslation, Trans } from "react-i18next"; 7 | import { Link } from "react-router-dom"; 8 | 9 | import { KristName } from "@api/types"; 10 | import { KristNameLink } from "@comp/names/KristNameLink"; 11 | import { DateTime } from "@comp/DateTime"; 12 | 13 | export function NameItem({ name }: { name: KristName }): JSX.Element { 14 | const { t } = useTranslation(); 15 | 16 | // Display 'purchased' if this is the original owner, otherwise display 17 | // 'received'. Note that this is different to checking if `transferred` is set 18 | // or not - subjectively I believe if the name is back in the original owner's 19 | // hands, it makes more sense to show when they originally purchased it, 20 | // rather than when they received it back. This may change in the future. 21 | const transferred = name.owner !== name.original_owner; 22 | 23 | const nameEl = ; 24 | const nameLink = "/network/names/" + encodeURIComponent(name.name); 25 | const nameTime = new Date(transferred && name.transferred 26 | ? name.transferred : name.registered); 27 | 28 | return 29 |
30 | {/* Display 'purchased' if this is the original owner, otherwise display 31 | * 'received'. */} 32 | {!transferred 33 | ? Purchased {nameEl} 34 | : Received {nameEl}} 35 |
36 | 37 | {/* Purchase time */} 38 | 39 | 40 | 41 |
; 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/backup/BackupResultsSummary.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .backup-results-summary { 7 | .summary-wallets-imported .positive, .summary-contacts-imported .positive { 8 | color: @kw-green; 9 | font-weight: bold; 10 | } 11 | 12 | .summary-errors-warnings .errors { 13 | color: @kw-red; 14 | font-weight: bold; 15 | } 16 | 17 | .summary-errors-warnings .warnings { 18 | color: @kw-orange; 19 | font-weight: bold; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/backup/BackupResultsTree.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .backup-results-tree { 7 | max-height: 480px; 8 | overflow-y: auto; 9 | 10 | .backup-result-icon { 11 | margin-right: @padding-xs; 12 | } 13 | 14 | // Map the different result type icons to their appropriate colours 15 | .anticon.backup-result-success { color: @kw-green; } 16 | .anticon.backup-result-warning { color: @kw-orange; } 17 | .anticon.backup-result-error { color: @kw-red; } 18 | 19 | // Make the non-leaf nodes bold, so the tree is more readable 20 | .ant-tree-treenode:not(.backup-results-tree-message) .ant-tree-title { 21 | font-weight: 500; 22 | } 23 | 24 | // The leaf nodes have an invisible button which takes up space; remove that 25 | .ant-tree-switcher-noop { 26 | display: none; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/backup/ImportProgress.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { Progress } from "antd"; 6 | 7 | import { Trans } from "react-i18next"; 8 | import { useTFns } from "@utils/i18n"; 9 | 10 | export type IncrProgressFn = () => void; 11 | export type InitProgressFn = (total: number) => void; 12 | 13 | interface ImportProgressHookResponse { 14 | progressBar: JSX.Element; 15 | onProgress: IncrProgressFn; 16 | initProgress: InitProgressFn; 17 | resetProgress: () => void; 18 | } 19 | 20 | export function useImportProgress(): ImportProgressHookResponse { 21 | const { t, tKey } = useTFns("import."); 22 | 23 | const [progress, setProgress] = useState(0); 24 | const [total, setTotal] = useState(1); 25 | 26 | // Increment the progress bar when one of the wallets/contacts have been 27 | // imported 28 | const onProgress = () => setProgress(c => c + 1); 29 | 30 | function initProgress(total: number) { 31 | setProgress(0); 32 | setTotal(total); 33 | } 34 | 35 | function resetProgress() { 36 | setProgress(0); 37 | setTotal(1); 38 | } 39 | 40 | const progressBar = <> 41 | {/* Importing text */} 42 |
46 | 47 | Importing {{ count: total }} items... 48 | 49 |
50 | 51 | {/* Progress bar */} 52 | 56 | ; 57 | 58 | return { progressBar, onProgress, initProgress, resetProgress }; 59 | } 60 | -------------------------------------------------------------------------------- /src/pages/backup/backupExport.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { store } from "@app"; 5 | import { encode } from "js-base64"; 6 | 7 | declare const __GIT_VERSION__: string; 8 | declare const __PKGBUILD__: string; 9 | 10 | export async function backupExport(): Promise { 11 | const { salt, tester } = store.getState().masterPassword; 12 | const { wallets } = store.getState().wallets; 13 | const { contacts } = store.getState().contacts; 14 | 15 | // Get the wallets, skipping those with dontSave set to true 16 | const finalWallets = Object.fromEntries(Object.entries(wallets) 17 | .filter(([_, w]) => w.dontSave !== true)); 18 | 19 | const gitVersion: string = __GIT_VERSION__; 20 | const pkgbuild = __PKGBUILD__; 21 | 22 | const backup = { 23 | version: 2, 24 | gitVersion, 25 | pkgbuild, 26 | 27 | // Store these to verify the master password is correct when importing 28 | salt, tester, 29 | 30 | wallets: finalWallets, 31 | contacts 32 | }; 33 | 34 | // Convert to base64'd JSON 35 | return encode(JSON.stringify(backup)); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/blocks/BlockHash.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .block-hash { 7 | color: @kw-text-tertiary; 8 | 9 | font-family: monospace; 10 | font-size: 90%; 11 | 12 | &-short-part { 13 | color: @kw-text; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/blocks/BlockHash.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import classNames from "classnames"; 5 | import { Typography } from "antd"; 6 | 7 | import { useBooleanSetting } from "@utils/settings"; 8 | 9 | import "./BlockHash.less"; 10 | 11 | const { Text } = Typography; 12 | 13 | const SHORT_HASH_LENGTH = 12; 14 | 15 | interface Props { 16 | hash?: string | null; 17 | alwaysCopyable?: boolean; 18 | neverCopyable?: boolean; 19 | className?: string; 20 | } 21 | 22 | export function BlockHash({ hash, alwaysCopyable, neverCopyable, className }: Props): JSX.Element | null { 23 | const blockHashCopyButtons = useBooleanSetting("blockHashCopyButtons"); 24 | 25 | if (hash === undefined || hash === null) return null; 26 | 27 | // If the hash is longer than 12 characters (i.e. it's not just a short hash 28 | // on its own), then split it into two parts, so the short hash can be 29 | // highlighted. Otherwise, just put the whole hash in restHash. 30 | const shortHash = hash.length > SHORT_HASH_LENGTH 31 | ? hash.substr(0, SHORT_HASH_LENGTH) : ""; 32 | const restHash = hash.length > SHORT_HASH_LENGTH 33 | ? hash.substring(SHORT_HASH_LENGTH, hash.length) : hash; 34 | 35 | const copyable = alwaysCopyable || (!neverCopyable && blockHashCopyButtons) 36 | ? { text: hash } : undefined; 37 | 38 | const classes = classNames("block-hash", className); 39 | 40 | return 41 | {shortHash && {shortHash}} 42 | {restHash} 43 | ; 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/blocks/BlockPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .block-page { 7 | .block-nav-buttons { 8 | .block-prev.ant-btn, .block-prev .ant-btn { 9 | margin-right: @margin-sm; 10 | padding-left: @padding-sm; 11 | } 12 | 13 | .block-next.ant-btn, .block-next .ant-btn { 14 | padding-right: @padding-sm; 15 | } 16 | } 17 | 18 | .block-info-row { 19 | max-width: 768px; 20 | margin-top: -@margin-lg; 21 | margin-bottom: @margin-lg; 22 | 23 | .kw-statistic { 24 | margin-top: @margin-lg; 25 | margin-right: @margin-lg; 26 | 27 | .date-time { 28 | font-size: @font-size-base * 1.5; 29 | } 30 | 31 | &.statistic-block-hash .kw-statistic-value, .block-hash { 32 | font-size: @font-size-base; 33 | line-height: 1; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/blocks/BlocksPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | @import "../../style/table.less"; 6 | 7 | .blocks-page .table-mobile-list-view { 8 | .block-mobile-item { 9 | display: block; 10 | color: @text-color; 11 | 12 | .block-height { 13 | display: block; 14 | font-size: 120%; 15 | } 16 | 17 | .block-value { 18 | float: right; 19 | font-size: 120%; 20 | } 21 | 22 | .block-field { 23 | font-weight: bold; 24 | white-space: nowrap; 25 | color: @text-color-secondary; 26 | } 27 | 28 | .block-technical-row { 29 | display: block; 30 | 31 | .block-mobile-hash, .block-difficulty { 32 | font-size: 90%; 33 | } 34 | 35 | .block-mobile-hash-value { 36 | color: @text-color; 37 | font-family: monospace; 38 | } 39 | 40 | .sep:before { 41 | content: "\2013"; 42 | 43 | display: inline-block; 44 | margin: 0 @padding-xs; 45 | color: @text-color-secondary; 46 | } 47 | } 48 | 49 | .block-mined { 50 | color: @text-color-secondary; 51 | font-size: @font-size-sm; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/pages/contacts/ContactEditButton.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import React, { useState, useCallback, FC } from "react"; 5 | 6 | import { AddContactModal } from "./AddContactModal"; 7 | 8 | import { Contact } from "@contacts"; 9 | 10 | interface Props { 11 | address?: string; 12 | contact?: Contact; 13 | } 14 | 15 | export const ContactEditButton: FC = ({ 16 | address, 17 | contact, 18 | children 19 | }): JSX.Element => { 20 | const [editContactVisible, setEditContactVisible] = useState(false); 21 | 22 | const child = React.Children.only(children) as React.ReactElement; 23 | 24 | return <> 25 | {React.cloneElement(child, { onClick: (e: MouseEvent) => { 26 | e.preventDefault(); 27 | setEditContactVisible(true); 28 | }})} 29 | 30 | 36 | ; 37 | }; 38 | 39 | export type OpenEditContactFn = (address?: string, contact?: Contact) => void; 40 | export type ContactEditHookRes = [ 41 | OpenEditContactFn, 42 | JSX.Element | null, 43 | (visible: boolean) => void 44 | ]; 45 | 46 | export function useEditContactModal(): ContactEditHookRes { 47 | const [opened, setOpened] = useState(false); 48 | const [visible, setVisible] = useState(false); 49 | const [address, setAddress] = useState(); 50 | const [contact, setContact] = useState(); 51 | 52 | const open = useCallback((address?: string, contact?: Contact) => { 53 | setAddress(address); 54 | setContact(contact); 55 | setVisible(true); 56 | setOpened(true); 57 | }, []); 58 | 59 | const modal = opened 60 | ? 66 | : null; 67 | 68 | return [open, modal, setVisible]; 69 | } 70 | -------------------------------------------------------------------------------- /src/pages/contacts/ContactMobileItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useMemo } from "react"; 5 | import { Collapse } from "antd"; 6 | 7 | import { Contact } from "@contacts"; 8 | 9 | import { ContextualAddress } from "@comp/addresses/ContextualAddress"; 10 | 11 | import { OpenEditContactFn } from "./ContactEditButton"; 12 | import { OpenSendTxFn } from "@comp/transactions/SendTransactionModalLink"; 13 | 14 | import { ContactMobileItemActions } from "./ContactsMobileItemActions"; 15 | 16 | interface Props { 17 | contact: Contact; 18 | 19 | openEditContact: OpenEditContactFn; 20 | openSendTx: OpenSendTxFn; 21 | } 22 | 23 | export function ContactMobileItem({ 24 | contact, 25 | openEditContact, 26 | openSendTx 27 | }: Props): JSX.Element { 28 | const itemHead = useMemo(() => ( 29 |
30 | {/* Label, if possible */} 31 | {contact.label && 32 | {contact.label} 33 | } 34 | 35 | {/* Address */} 36 |
37 | {/* Address */} 38 | 45 |
46 |
47 | ), [contact.address, contact.label]); 48 | 49 | return 50 | 51 | 56 | 57 | ; 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/contacts/ContactsPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | @import "../../style/table.less"; 6 | 7 | .contacts-page .table-mobile-list-view { 8 | .contact-mobile-item { 9 | .contact-label { 10 | display: block; 11 | font-size: 120%; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/contacts/ContactsPage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTFns } from "@utils/i18n"; 5 | 6 | import { PageLayout } from "@layout/PageLayout"; 7 | 8 | import { useContacts } from "@contacts"; 9 | 10 | import { useContactsPageActions } from "./ContactsPageActions"; 11 | import { ContactsTable } from "./ContactsTable"; 12 | 13 | import { useEditContactModal } from "./ContactEditButton"; 14 | import { useSendTransactionModal } from "@comp/transactions/SendTransactionModalLink"; 15 | 16 | import "./ContactsPage.less"; 17 | 18 | /** Contact count subtitle */ 19 | function ContactsPageSubtitle(): JSX.Element { 20 | const { t, tStr, tKey } = useTFns("addressBook."); 21 | const { contactAddressList } = useContacts(); 22 | 23 | const count = contactAddressList.length; 24 | 25 | return <>{count > 0 26 | ? t(tKey("contactCount"), { count }) 27 | : tStr("contactCountEmpty") 28 | }; 29 | } 30 | 31 | export function ContactsPage(): JSX.Element { 32 | const [openEditContact, editContactModal] = useEditContactModal(); 33 | const [openSendTx, sendTxModal] = useSendTransactionModal(); 34 | 35 | const extra = useContactsPageActions(); 36 | 37 | return } 40 | extra={extra} 41 | className="contacts-page" 42 | > 43 | 47 | 48 | {editContactModal} 49 | {sendTxModal} 50 | ; 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/contacts/ContactsPageActions.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useEffect, useState, Dispatch, SetStateAction } from "react"; 5 | import { Button, Menu } from "antd"; 6 | import { PlusOutlined } from "@ant-design/icons"; 7 | 8 | import { useTFns } from "@utils/i18n"; 9 | 10 | import { useTopMenuOptions } from "@layout/nav/TopMenu"; 11 | 12 | import { AddContactModal } from "./AddContactModal"; 13 | 14 | interface ExtraButtonsProps { 15 | setAddContactVisible: Dispatch>; 16 | } 17 | 18 | function ContactsPageActions({ 19 | setAddContactVisible 20 | }: ExtraButtonsProps): JSX.Element { 21 | const { tStr } = useTFns("addressBook."); 22 | 23 | return <> 24 | {/* Add contact */} 25 | 32 | ; 33 | } 34 | 35 | export function useContactsPageActions(): JSX.Element { 36 | const { tStr } = useTFns("addressBook."); 37 | 38 | const [addContactVisible, setAddContactVisible] = useState(false); 39 | 40 | const [usingTopMenu, set, unset] = useTopMenuOptions(); 41 | useEffect(() => { 42 | set(<> 43 | {/* Add contact */} 44 | setAddContactVisible(true)}> 45 | {tStr("buttonAddContact")} 46 | 47 | ); 48 | 49 | return unset; 50 | }, [tStr, set, unset]); 51 | 52 | return <> 53 | {!usingTopMenu && } 56 | 57 | 58 | ; 59 | } 60 | -------------------------------------------------------------------------------- /src/pages/credits/CreditsPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .page-credits { 7 | text-align: center; 8 | 9 | h1 { 10 | margin-bottom: 0; 11 | } 12 | 13 | .supporter-name { 14 | font-weight: bolder; 15 | } 16 | 17 | .credits-version-info { 18 | max-width: 840px; 19 | margin: @margin-md auto 0 auto; 20 | 21 | text-align: left; 22 | } 23 | 24 | .credits-privacy { 25 | max-width: 840px; 26 | margin: 0 auto; 27 | 28 | text-align: left; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/dashboard/InDevBanner.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Alert } from "antd"; 5 | 6 | import { useTranslation, Trans } from "react-i18next"; 7 | 8 | import { getAuthorInfo, getDevState } from "@utils"; 9 | 10 | export function InDevBanner(): JSX.Element | null { 11 | const { t } = useTranslation(); 12 | 13 | const { gitURL } = getAuthorInfo(); 14 | 15 | // This is not a hook, run this after the hooks (to avoid changing hook count) 16 | const { isDirty, isDev } = getDevState(); 17 | 18 | // Don't show the beta banner unless we are in development mode (push up the 19 | // repository link) 20 | if (!isDev && !isDirty) return null; 21 | 22 | return 26 | Welcome to the KristWeb v2 public beta! This site is relatively new, so 27 | please report any bugs on 28 | GitHub. 29 | Thanks! 30 | } 31 | />; 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/dashboard/MOTDCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Card, Alert } from "antd"; 5 | 6 | import { useSelector } from "react-redux"; 7 | import { RootState } from "@store"; 8 | 9 | import { useTranslation } from "react-i18next"; 10 | 11 | import Markdown from "markdown-to-jsx"; 12 | import { useMarkdownLink } from "@comp/krist/MarkdownLink"; 13 | import { DateTime } from "@comp/DateTime"; 14 | 15 | export function MOTDCard(): JSX.Element { 16 | const { t } = useTranslation(); 17 | const { motd, motdSet, endpoint, debugMode } 18 | = useSelector((s: RootState) => s.node.motd); 19 | 20 | // Make relative links start with the sync node, and override all links to 21 | // open in a new tab 22 | const MarkdownLink = useMarkdownLink(); 23 | 24 | return 25 | {(debugMode || (endpoint ? (btoa([...endpoint] as any) !== atob("YXl4eUxHa3NjeXgwTEM0c1l5eGxMSElzYVN4aExIUXNMaXh1TEdVc2RBPT0=") && btoa([...endpoint] as any) !== atob("YXl4eUxHa3NjeXgwTEM0c1pDeGxMSFk9")) : false)) && } 26 | 27 | 31 | {motd} 32 | 33 | 34 | 35 | ; 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/dashboard/WalletItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Row, Col } from "antd"; 5 | 6 | import { Wallet } from "@wallets"; 7 | 8 | import { KristValue } from "@comp/krist/KristValue"; 9 | import { ContextualAddress } from "@comp/addresses/ContextualAddress"; 10 | 11 | export function WalletItem({ wallet }: { wallet: Wallet }): JSX.Element { 12 | return 13 | 14 | {wallet.label && {wallet.label}} 15 | 22 | 23 | 24 | 25 | 26 | 27 | ; 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/dashboard/WhatsNewCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Card, Button } from "antd"; 5 | import { RightOutlined } from "@ant-design/icons"; 6 | 7 | import { Link } from "react-router-dom"; 8 | 9 | import { useTranslation } from "react-i18next"; 10 | 11 | export function WhatsNewCard(): JSX.Element { 12 | const { t } = useTranslation(); 13 | 14 | return 18 | 19 | 22 | 23 | ; 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/dev/DevPage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { Button, Space } from "antd"; 6 | import { PageLayout } from "@layout/PageLayout"; 7 | 8 | import { useWallets, deleteWallet } from "@wallets"; 9 | 10 | import Debug from "debug"; 11 | const debug = Debug("kristweb:dev-page"); 12 | 13 | export function DevPage(): JSX.Element { 14 | const { wallets } = useWallets(); 15 | 16 | const [forceError, setForceError] = useState(false); 17 | if (forceError) throw new Error("Whoops!"); 18 | 19 | return 23 | 24 | {/* Delete all wallets with zero balance */} 25 | 34 | 35 | {/* Delete all wallets */} 36 | 39 | 40 | {/* Clear local storage */} 41 | 44 | 45 | {/* Cause an error */} 46 | 49 | 50 | ; 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/names/NamePage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .name-page { 7 | .top-name-row { 8 | display: flex; 9 | align-items: center; 10 | 11 | .name { 12 | display: inline-block; 13 | margin-right: @margin-lg; 14 | margin-bottom: 0; 15 | 16 | font-size: @font-size-base * 2; 17 | font-weight: 500; 18 | 19 | .ant-typography-copy { 20 | line-height: 1 !important; 21 | margin-left: @padding-xs; 22 | 23 | .anticon { 24 | font-size: @font-size-base; 25 | vertical-align: 0; 26 | } 27 | } 28 | } 29 | 30 | .ant-btn { 31 | margin-right: @margin-md; 32 | &:last-child { margin-right: 0; } 33 | } 34 | } 35 | 36 | .name-info-row { 37 | max-width: 768px; 38 | margin-bottom: @margin-lg; 39 | 40 | .kw-statistic { 41 | margin-top: @margin-lg; 42 | margin-right: @margin-lg; 43 | 44 | .date-time { 45 | font-size: @font-size-base * 1.5; 46 | } 47 | } 48 | } 49 | 50 | .name-a-record-row { 51 | max-width: 768px; 52 | margin-bottom: @margin-lg; 53 | 54 | .name-a-record-edit { 55 | margin-left: @padding-xs; 56 | } 57 | } 58 | 59 | .name-card-row { 60 | & > .ant-col { 61 | margin-bottom: @margin-md; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/names/NamesPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | @import "../../style/table.less"; 6 | 7 | .names-page { 8 | // Highlight unpaid names 9 | .ant-table .name-row-unpaid { 10 | background: fade(@kw-primary, 15%); 11 | 12 | td.ant-table-cell { 13 | border-bottom-color: fade(@kw-primary, 20%); 14 | } 15 | 16 | .ant-table-cell.ant-table-column-sort { 17 | background: fade(@kw-primary, 20%); 18 | } 19 | 20 | &:hover td.ant-table-cell { 21 | background: fade(@kw-primary, 20%); 22 | 23 | &.ant-table-cell.ant-table-column-sort { 24 | background: fade(@kw-primary, 35%); 25 | } 26 | } 27 | } 28 | 29 | .table-mobile-list-view { 30 | .name-mobile-item { 31 | .name-name { 32 | display: block; 33 | font-size: 120%; 34 | } 35 | 36 | .name-tags { 37 | float: right; 38 | 39 | .ant-tag { 40 | white-space: nowrap; 41 | } 42 | } 43 | 44 | .name-field { 45 | font-weight: bold; 46 | white-space: nowrap; 47 | color: @text-color-secondary; 48 | } 49 | 50 | .name-registered, .name-updated { 51 | margin-top: @padding-xss; 52 | color: @text-color-secondary; 53 | font-size: @font-size-sm; 54 | } 55 | } 56 | } 57 | 58 | @media (max-width: @screen-sm) { 59 | // Make the "Purchase name" button full width on mobile 60 | .ant-page-header-heading-extra { 61 | width: 100%; 62 | 63 | .ant-btn { 64 | display: block; 65 | width: 100%; 66 | margin-top: @margin-sm; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/pages/names/mgmt/ConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTranslation, Trans, TFunction } from "react-i18next"; 5 | 6 | import { ContextualAddress } from "@comp/addresses/ContextualAddress"; 7 | import { ModalStaticFunctions } from "antd/lib/modal/confirm"; 8 | 9 | interface Props { 10 | count: number; 11 | recipient?: string; 12 | allNamesCount: number; 13 | } 14 | 15 | export function showConfirmModal( 16 | t: TFunction, 17 | confirmModal: Omit, 18 | count: number, 19 | allNamesCount: number, 20 | recipient: string, 21 | triggerSubmit: () => void, 22 | setSubmitting: (value: boolean) => void, 23 | ): void { 24 | confirmModal.confirm({ 25 | title: t("nameTransfer.modalTitle"), 26 | content: , 31 | 32 | okText: t("nameTransfer.buttonSubmit"), 33 | onOk: triggerSubmit, 34 | 35 | cancelText: t("dialog.cancel"), 36 | onCancel: () => setSubmitting(false) 37 | }); 38 | } 39 | 40 | // No 'Mode' necessary, this is only shown for transfers 41 | function ConfirmModalContent({ 42 | count, 43 | recipient, 44 | allNamesCount 45 | }: Props): JSX.Element { 46 | const { t } = useTranslation(); 47 | 48 | // Show the appropriate message, if this is all the owner's names 49 | return = allNamesCount 52 | ? "nameTransfer.warningAllNames" 53 | : "nameTransfer.warningMultipleNames"} 54 | count={count} 55 | > 56 | Are you sure you want to transfer {{ count }} names to 57 | ? 58 | ; 59 | } 60 | -------------------------------------------------------------------------------- /src/pages/names/mgmt/EditProgress.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { Progress } from "antd"; 6 | 7 | import { Trans } from "react-i18next"; 8 | import { TFns } from "@utils/i18n"; 9 | 10 | interface EditProgressHookResponse { 11 | progressBar: JSX.Element; 12 | onProgress: () => void; 13 | initProgress: (total: number) => void; 14 | resetProgress: () => void; 15 | } 16 | 17 | export function useEditProgress( 18 | { t, tKey }: TFns 19 | ): EditProgressHookResponse { 20 | const [submitProgress, setSubmitProgress] = useState(0); 21 | const [submitTotal, setSubmitTotal] = useState(1); 22 | 23 | // Increment the progress bar when one of the names has been edited 24 | const onProgress = () => setSubmitProgress(c => c + 1); 25 | 26 | function initProgress(total: number) { 27 | setSubmitProgress(0); 28 | setSubmitTotal(total); 29 | } 30 | 31 | function resetProgress() { 32 | setSubmitProgress(0); 33 | setSubmitTotal(1); 34 | } 35 | 36 | const progressBar = <> 37 | {/* Submitting text */} 38 |
42 | 43 | Editing {{ count: submitTotal }} names... 44 | 45 |
46 | 47 | {/* Progress bar */} 48 | 52 | ; 53 | 54 | return { progressBar, onProgress, initProgress, resetProgress }; 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/names/mgmt/NameDataInput.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Form, Input } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | const A_RECORD_REGEXP = /^[^\s.?#].[^\s]*/; 9 | 10 | export function NameDataInput(): JSX.Element { 11 | const { t } = useTranslation(); 12 | 13 | return 255) 22 | throw t("nameUpdate.errorParameterData"); 23 | } 24 | }]} 25 | > 26 | 31 | ; 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/names/mgmt/NamePurchaseModalLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC, useState } from "react"; 5 | 6 | import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; 7 | import { NamePurchaseModal } from "./NamePurchaseModal"; 8 | 9 | export const NamePurchaseModalLink: FC = ({ children }): JSX.Element => { 10 | const [modalVisible, setModalVisible] = useState(false); 11 | 12 | return <> 13 | setModalVisible(true)}> 14 | {children} 15 | 16 | 17 | 21 | ; 22 | }; 23 | -------------------------------------------------------------------------------- /src/pages/names/mgmt/NoNamesModal.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Dispatch, SetStateAction } from "react"; 5 | import classNames from "classnames"; 6 | import { Modal } from "antd"; 7 | 8 | import { useTranslation } from "react-i18next"; 9 | import { useHistory } from "react-router-dom"; 10 | 11 | interface Props { 12 | visible?: boolean; 13 | setVisible?: Dispatch>; 14 | className?: string; 15 | } 16 | 17 | export function NoWalletsModal({ 18 | className, 19 | visible, 20 | setVisible 21 | }: Props): JSX.Element { 22 | const { t } = useTranslation(); 23 | const history = useHistory(); 24 | 25 | const classes = classNames("kw-no-names-modal", className); 26 | 27 | return { 34 | setVisible?.(false); 35 | history.push("/me/names"); 36 | }} 37 | okText={t("noNamesResult.button")} 38 | 39 | onCancel={() => setVisible?.(false)} 40 | cancelText={t("dialog.cancel")} 41 | > 42 | {t("noNamesResult.subTitle")} 43 | ; 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/names/mgmt/SuccessNotifContent.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTranslation, Trans } from "react-i18next"; 5 | 6 | import { ContextualAddress } from "@comp/addresses/ContextualAddress"; 7 | 8 | import { Mode } from "./NameEditModal"; 9 | 10 | interface Props { 11 | count: number; 12 | recipient?: string; 13 | mode: Mode; 14 | } 15 | 16 | export function SuccessNotifContent({ 17 | count, 18 | recipient, 19 | mode 20 | }: Props): JSX.Element | null { 21 | const { t } = useTranslation(); 22 | 23 | // Show the appropriate message, if this is all the owner's names 24 | if (mode === "transfer") { 25 | // Transfer names success notification 26 | return 31 | Transferred {{ count }} names to 32 | . 33 | ; 34 | } else if (mode === "update") { 35 | // Update names success notification 36 | return 41 | Updated {{ count }} names. 42 | ; 43 | } else { 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/pages/names/mgmt/checkName.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Dispatch, SetStateAction } from "react"; 5 | 6 | import * as api from "@api"; 7 | import { criticalError } from "@utils"; 8 | 9 | interface CheckNameResponse { 10 | available: boolean; 11 | } 12 | 13 | export async function checkName( 14 | name: string, 15 | setNameAvailable: Dispatch> 16 | ): Promise { 17 | try { 18 | const url = `names/check/${encodeURIComponent(name)}`; 19 | const { available } = await api.get(url); 20 | setNameAvailable(available); 21 | } catch (err: any) { 22 | criticalError(err); 23 | setNameAvailable(undefined); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/names/tableLock.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useSelector } from "react-redux"; 5 | import { RootState } from "@store"; 6 | import { store } from "@app"; 7 | import { 8 | incrementNameTableLock, decrementNameTableLock 9 | } from "@actions/MiscActions"; 10 | 11 | import Debug from "debug"; 12 | const debug = Debug("kristweb:name-table-lock"); 13 | 14 | export function useNameTableLock(): boolean { 15 | const nameTableLock = useSelector((s: RootState) => s.misc.nameTableLock); 16 | return nameTableLock > 0; 17 | } 18 | 19 | export interface NameTableLock { 20 | release: () => void; 21 | } 22 | 23 | export function lockNameTable(timeoutMs = 20000): NameTableLock { 24 | let _timeout: ReturnType | undefined = setTimeout(() => { 25 | debug("timeout reached, releasing name table lock"); 26 | release(); 27 | }, timeoutMs); 28 | 29 | function release() { 30 | if (_timeout !== undefined) { 31 | debug("name table lock being released"); 32 | store.dispatch(decrementNameTableLock()); 33 | 34 | debug("clearing name table lock timeout %o", _timeout); 35 | clearTimeout(_timeout); 36 | _timeout = undefined; 37 | } 38 | } 39 | 40 | debug("name table being locked"); 41 | store.dispatch(incrementNameTableLock()); 42 | 43 | return { release }; 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/settings/SettingBoolean.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Switch } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | import { SettingName, setBooleanSetting, useBooleanSetting } from "@utils/settings"; 9 | import { SettingDescription } from "./SettingDescription"; 10 | 11 | interface Props { 12 | setting: SettingName; 13 | title?: string; 14 | titleKey?: string; 15 | description?: string; 16 | descriptionKey?: string; 17 | } 18 | 19 | export function SettingBoolean({ 20 | setting, 21 | title, titleKey, 22 | description, descriptionKey 23 | }: Props): JSX.Element { 24 | const settingValue = useBooleanSetting(setting); 25 | 26 | const { t } = useTranslation(); 27 | 28 | function onChange(value: boolean) { 29 | setBooleanSetting(setting, value); 30 | } 31 | 32 | return
onChange(!settingValue)} 35 | > 36 | 43 | 44 | {titleKey ? t(titleKey) : title} 45 | 46 | 47 |
; 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/settings/SettingDescription.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | import { useTranslation } from "react-i18next"; 6 | 7 | interface Props { 8 | description?: string; 9 | descriptionKey?: string; 10 | } 11 | 12 | export function SettingDescription({ description, descriptionKey }: Props): JSX.Element | null { 13 | const { t } = useTranslation(); 14 | 15 | if (!description && !descriptionKey) return null; 16 | 17 | return ( 18 |
19 | {descriptionKey ? t(descriptionKey) : description} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/settings/SettingInteger.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { Input, InputNumber, Button } from "antd"; 6 | 7 | import { useTranslation } from "react-i18next"; 8 | 9 | import { SettingName, setIntegerSetting, useIntegerSetting, validateIntegerSetting } from "@utils/settings"; 10 | import { SettingDescription } from "./SettingDescription"; 11 | 12 | interface Props { 13 | setting: SettingName; 14 | title?: string; 15 | titleKey?: string; 16 | description?: string; 17 | descriptionKey?: string; 18 | } 19 | 20 | export function SettingInteger({ 21 | setting, 22 | title, titleKey, 23 | description, descriptionKey 24 | }: Props): JSX.Element { 25 | const settingValue = useIntegerSetting(setting); 26 | const [value, setValue] = useState(settingValue); 27 | 28 | const { t } = useTranslation(); 29 | 30 | const numVal = value ? Number(value) : undefined; 31 | const isValid = numVal !== undefined 32 | && !isNaN(numVal) 33 | && validateIntegerSetting(setting, numVal); 34 | 35 | function onSave() { 36 | if (!isValid) return; 37 | setIntegerSetting(setting, numVal!); 38 | } 39 | 40 | return
41 | 42 | {/* Number input */} 43 | 48 | 49 | {/* Save button */} 50 | 57 | 58 | 59 | {titleKey ? t(titleKey) : title} 60 | 61 | 62 |
; 63 | } 64 | -------------------------------------------------------------------------------- /src/pages/settings/SettingLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Menu } from "antd"; 5 | import { LinkOutlined } from "@ant-design/icons"; 6 | 7 | import { HashLink } from "react-router-hash-link"; 8 | 9 | import { useTranslation } from "react-i18next"; 10 | 11 | import { SettingDescription } from "./SettingDescription"; 12 | 13 | interface Props { 14 | link: string; 15 | title?: string; 16 | titleKey?: string; 17 | description?: string; 18 | descriptionKey?: string; 19 | } 20 | 21 | export function SettingLink({ 22 | link, 23 | title, titleKey, 24 | description, descriptionKey, 25 | ...props 26 | }: Props): JSX.Element { 27 | const { t } = useTranslation(); 28 | 29 | return 30 | 31 |
32 | {titleKey ? t(titleKey) : title} 33 | 34 |
35 |
36 |
; 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/settings/SettingsPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | 6 | .settings-page { 7 | .big-menu.ant-menu.ant-menu-inline { 8 | .ant-menu-item { 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | 13 | .menu-item-setting { 14 | margin: -@padding-xs 0; 15 | padding: @padding-xs 0; 16 | } 17 | } 18 | } 19 | 20 | .settings-language-item { 21 | // ant-design adds `width: calc(100% + 1px)` to menu items for some reason 22 | width: 100%; 23 | 24 | &.ant-menu-item { 25 | flex-direction: row !important; 26 | justify-content: flex-start !important; 27 | } 28 | 29 | &.settings-language-item-current { 30 | background: lighten(@kw-darker, 5%); 31 | color: @primary-color; 32 | } 33 | 34 | .settings-language-flag { 35 | display: inline-block; 36 | 37 | width: 30px; 38 | height: 20px; 39 | 40 | vertical-align: -0.25em; 41 | 42 | margin-right: @margin-sm; 43 | } 44 | 45 | .settings-language-native-name { 46 | color: @text-color-secondary; 47 | margin-left: @padding-xs; 48 | } 49 | } 50 | 51 | .settings-translations-extra { 52 | // Force the fake label button to vertically align correctly 53 | display: flex; 54 | 55 | .ant-btn { 56 | margin-right: @margin-sm; 57 | &:last-child { margin-right: 0; } 58 | } 59 | } 60 | 61 | .menu-item-setting-integer { 62 | .ant-input-group.ant-input-group-compact { 63 | display: inline-block; 64 | width: auto; 65 | margin-right: @margin-sm; 66 | margin-bottom: @padding-xs; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/pages/settings/translations/LanguageItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC } from "react"; 5 | import classNames from "classnames"; 6 | import { Menu } from "antd"; 7 | 8 | import { useTranslation } from "react-i18next"; 9 | import { getLanguages, Language } from "@utils/i18n"; 10 | 11 | import { Flag } from "@comp/Flag"; 12 | 13 | interface LanguageItemProps { 14 | code: string; 15 | lang: Language; 16 | } 17 | const LanguageItem: FC = ({ code, lang, ...props }): JSX.Element => { 18 | const { i18n } = useTranslation(); 19 | 20 | const isCurrent = i18n.language === code; 21 | const classes = classNames("settings-language-item", { 22 | "settings-language-item-current": isCurrent 23 | }); 24 | 25 | function changeLanguage() { 26 | i18n.changeLanguage(code); 27 | } 28 | 29 | return 30 | {/* Flag of language country */} 31 | 36 | 37 | {/* Language name */} 38 | {lang.name} 39 | 40 | {/* Native name, if applicable */} 41 | {lang.nativeName && ( 42 | 43 | ({lang.nativeName}) 44 | 45 | )} 46 | ; 47 | }; 48 | 49 | export function getLanguageItems(): JSX.Element[] { 50 | const languages = getLanguages(); 51 | if (!languages) return []; 52 | 53 | const entries = Object.entries(languages); 54 | entries.sort((a, b) => a[1].name.localeCompare(b[1].name)); 55 | 56 | return entries 57 | .map(([code, lang]) => ( 58 | 63 | )); 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/settings/translations/MissingKeysTable.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Table, Typography } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | import { AnalysedLanguage } from "./analyseLangs"; 9 | 10 | const { Text } = Typography; 11 | 12 | interface MissingKeysTableProps { 13 | lang: AnalysedLanguage; 14 | } 15 | 16 | export function MissingKeysTable({ lang }: MissingKeysTableProps): JSX.Element { 17 | const { t } = useTranslation(); 18 | 19 | return t("settings.translations.tableUntranslatedKeys")} 21 | size="small" 22 | 23 | dataSource={lang.missingKeys} 24 | rowKey="k" 25 | 26 | columns={[ 27 | { 28 | title: t("settings.translations.columnKey"), 29 | dataIndex: "k", 30 | key: "k", 31 | render: k => {k} 32 | }, 33 | { 34 | title: t("settings.translations.columnEnglishString"), 35 | dataIndex: "v", 36 | key: "v", 37 | render: v => {v} 38 | } 39 | ]} 40 | />; 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/settings/translations/exportCSV.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import csvStringify from "csv-stringify"; 5 | 6 | import { AnalysedLanguage } from "./analyseLangs"; 7 | 8 | interface CSVRow { 9 | Code: string; 10 | Language?: string; 11 | Key: string; 12 | Value?: string; 13 | } 14 | export async function generateLanguageCSV(languages: AnalysedLanguage[]): Promise { 15 | return new Promise((resolve, reject) => { 16 | const en = languages.find(l => l.code === "en"); 17 | if (!en) return reject("en missing"); 18 | const enKeyNames = Object.keys(en.keys || {}); 19 | 20 | // Merge all the languages and their keys together into one array 21 | const data = languages.reduce((out, lang) => { 22 | const { code, language, keys } = lang; 23 | if (code === "und" || !keys || !language) return out; 24 | const languageName = language.name; 25 | 26 | // Keys from both en and this language 27 | const combinedKeys = [...new Set([...enKeyNames, ...Object.keys(keys)])]; 28 | // Find the value for this key from the language, or null if not 29 | const keysWithValues = combinedKeys.map(k => [k, keys[k]]); 30 | 31 | // Generate all the rows for this language 32 | return [ 33 | ...out, 34 | ...keysWithValues.map(([k, v]) => ({ 35 | "Code": code, 36 | "Language": languageName, 37 | "Key": k, "Value": v 38 | })) 39 | ]; 40 | }, [] as CSVRow[]); 41 | 42 | csvStringify(data, { header: true, quoted: true }, (err, data) => { 43 | if (err) return reject(err); 44 | resolve(data); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/transactions/TransactionRawDataCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Card, Table } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | import { KristTransaction } from "@api/types"; 9 | 10 | import { HelpIcon } from "@comp/HelpIcon"; 11 | 12 | export function TransactionRawDataCard({ transaction }: { transaction: KristTransaction }): JSX.Element { 13 | const { t } = useTranslation(); 14 | 15 | // Convert the transaction object to an array of entries {key, value} 16 | const processed = Object.entries(transaction) 17 | .map(([key, value]) => ({ key, value })); 18 | 19 | return 22 | {t("transaction.cardRawDataTitle")} 23 | 24 | } 25 | > 26 |
{ 47 | if (value === null || value === undefined) 48 | return null; 49 | 50 | return value.toString(); 51 | } 52 | } 53 | ]} 54 | 55 | pagination={false} 56 | /> 57 | ; 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/transactions/TransactionsPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | @import "../../style/table.less"; 6 | 7 | .transactions-page { 8 | .transactions-mined-switch { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | 13 | .ant-switch { 14 | margin-right: @margin-sm; 15 | } 16 | 17 | @media (max-width: @screen-md) { 18 | justify-content: center; 19 | margin-bottom: @margin-sm; 20 | } 21 | } 22 | 23 | // Highlight own transactions 24 | .ant-table .transaction-row-own { 25 | background: fade(@kw-primary, 15%); 26 | 27 | td.ant-table-cell { 28 | border-bottom-color: fade(@kw-primary, 20%); 29 | } 30 | 31 | .ant-table-cell.ant-table-column-sort { 32 | background: fade(@kw-primary, 20%); 33 | } 34 | 35 | &:hover td.ant-table-cell { 36 | background: fade(@kw-primary, 20%); 37 | 38 | &.ant-table-cell.ant-table-column-sort { 39 | background: fade(@kw-primary, 35%); 40 | } 41 | } 42 | } 43 | 44 | // Highlight verified addresses 45 | .ant-table .transaction-row-verified { 46 | background: fade(@kw-orange, 10%); 47 | 48 | td.ant-table-cell { 49 | border-bottom-color: fade(@kw-orange, 20%); 50 | } 51 | 52 | .ant-table-cell.ant-table-column-sort { 53 | background: fade(@kw-orange, 10%); 54 | } 55 | 56 | &:hover td.ant-table-cell { 57 | background: fade(@kw-orange, 20%); 58 | 59 | &.ant-table-cell.ant-table-column-sort { 60 | background: fade(@kw-orange, 35%); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/transactions/request/RequestPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../../App.less"; 5 | 6 | .request-page { 7 | .request-container { 8 | flex-direction: column; 9 | 10 | margin: 0 auto; 11 | width: 100%; 12 | max-width: 768px; 13 | 14 | background: @kw-light; 15 | border-radius: @kw-big-card-border-radius; 16 | 17 | padding: @padding-lg; 18 | 19 | .cb { 20 | margin-bottom: 0; 21 | margin-right: @margin-sm; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/transactions/request/RequestPage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTFns } from "@utils/i18n"; 5 | 6 | import { PageLayout } from "@layout/PageLayout"; 7 | 8 | import { RequestForm } from "./RequestForm"; 9 | 10 | import "./RequestPage.less"; 11 | 12 | export function RequestPage(): JSX.Element { 13 | const { tKey } = useTFns("request."); 14 | 15 | return 20 |
21 | 22 |
23 |
; 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/transactions/send/SendTransactionConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { FC, Attributes } from "react"; 5 | 6 | import { Trans } from "react-i18next"; 7 | import { useTFns } from "@utils/i18n"; 8 | 9 | import { KristValue } from "@comp/krist/KristValue"; 10 | 11 | interface SendTransactionConfirmModalContentsProps { 12 | amount: number; 13 | balance: number; 14 | } 15 | 16 | export const SendTransactionConfirmModalContents: FC = ({ amount, balance, key2 }) => { 17 | const { t, tKey } = useTFns("sendTransaction."); 18 | 19 | // Show the appropriate message, if this is just over half the 20 | // balance, or if it is the entire balance. 21 | return = balance 24 | ? "payLargeConfirmAll" 25 | : "payLargeConfirmHalf"))} 26 | > 27 | Are you sure you want to send ? 28 | This is over half your balance! 29 | ; 30 | }; 31 | -------------------------------------------------------------------------------- /src/pages/transactions/send/SendTransactionPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../../App.less"; 5 | 6 | .send-transaction-page { 7 | .send-transaction-container { 8 | //display: flex; 9 | flex-direction: column; 10 | 11 | margin: 0 auto; 12 | width: 100%; 13 | max-width: 768px; 14 | 15 | background: @kw-light; 16 | border-radius: @kw-big-card-border-radius; 17 | 18 | padding: @padding-lg; 19 | 20 | .send-transaction-submit { 21 | float: right; 22 | } 23 | } 24 | 25 | .send-transaction-alert { 26 | max-width: 768px; 27 | margin: 0 auto @margin-md auto; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/transactions/send/Success.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Button } from "antd"; 5 | 6 | import { useTranslation, Trans } from "react-i18next"; 7 | 8 | import { Link } from "react-router-dom"; 9 | 10 | import { KristTransaction } from "@api/types"; 11 | import { KristValue } from "@comp/krist/KristValue"; 12 | import { ContextualAddress } from "@comp/addresses/ContextualAddress"; 13 | 14 | export function NotifSuccessContents({ tx }: { tx: KristTransaction }): JSX.Element { 15 | const { t } = useTranslation(); 16 | 17 | return 18 | You sent 19 | 20 | from 21 | 27 | to 28 | 33 | ; 34 | } 35 | 36 | export function NotifSuccessButton({ tx }: { tx: KristTransaction }): JSX.Element { 37 | const { t } = useTranslation(); 38 | 39 | return 40 | 43 | ; 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/transactions/send/handleErrors.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { TranslatedError } from "@utils/i18n"; 5 | 6 | import { APIError } from "@api"; 7 | import { ShowAuthFailedFn } from "@api/AuthFailed"; 8 | 9 | import { Wallet } from "@wallets"; 10 | 11 | export function handleTransactionError( 12 | onError: ((error: Error) => void) | undefined, 13 | showAuthFailed: ShowAuthFailedFn, 14 | err: Error, 15 | from?: Wallet 16 | ): void { 17 | // Construct a TranslatedError pre-keyed to sendTransaction 18 | const tErr = (key: string) => new TranslatedError("sendTransaction." + key); 19 | 20 | switch (err.message) { 21 | case "missing_parameter": 22 | case "invalid_parameter": 23 | switch ((err as APIError).parameter) { 24 | case "to": 25 | return onError?.(tErr("errorParameterTo")); 26 | case "amount": 27 | return onError?.(tErr("errorParameterAmount")); 28 | case "metadata": 29 | return onError?.(tErr("errorParameterMetadata")); 30 | } 31 | break; 32 | case "insufficient_funds": 33 | return onError?.(tErr("errorInsufficientFunds")); 34 | case "name_not_found": 35 | return onError?.(tErr("errorNameNotFound")); 36 | case "auth_failed": 37 | return showAuthFailed(from!); 38 | } 39 | 40 | // Pass through any other unknown errors 41 | onError?.(err); 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/wallets/ManageBackupsDropdown.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Dispatch, SetStateAction } from "react"; 5 | import { Button, Dropdown, Menu } from "antd"; 6 | import { 7 | DatabaseOutlined, DownOutlined, ImportOutlined, ExportOutlined 8 | } from "@ant-design/icons"; 9 | 10 | import { useTFns } from "@utils/i18n"; 11 | 12 | import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; 13 | import { useMasterPassword } from "@wallets"; 14 | 15 | interface Props { 16 | setImportVisible: Dispatch>; 17 | setExportVisible: Dispatch>; 18 | } 19 | 20 | export function ManageBackupsDropdown({ 21 | setImportVisible, 22 | setExportVisible 23 | }: Props): JSX.Element { 24 | const { tStr } = useTFns("myWallets."); 25 | 26 | // Used to disable the export button if a master password hasn't been set up 27 | const { hasMasterPassword, salt, tester } = useMasterPassword(); 28 | const allowExport = !!hasMasterPassword && !!salt && !!tester; 29 | 30 | return <> 31 | 33 | {/* Import backup button */} 34 | 35 | setImportVisible(true)}> 36 |
{tStr("importBackup")}
37 |
38 |
39 | 40 | {/* Export backup button */} 41 | setExportVisible(true)} 45 | > 46 | {tStr("exportBackup")} 47 | 48 | 49 | )}> 50 | 53 |
54 | ; 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/wallets/NoWalletsMobileResult.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { MoreOutlined } from "@ant-design/icons"; 5 | 6 | import { Trans } from "react-i18next"; 7 | import { useTFns } from "@utils/i18n"; 8 | 9 | import { SmallResult } from "@comp/results/SmallResult"; 10 | 11 | export function NoWalletsMobileResult(): JSX.Element { 12 | const { tStr, tKey } = useTFns("myWallets."); 13 | 14 | return 18 | Add or create a wallet by clicking the menu in the 19 | top right! 20 | } 21 | />; 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/wallets/WalletEditButton.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState, useCallback, FC } from "react"; 5 | 6 | import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; 7 | import { AddWalletModal } from "./AddWalletModal"; 8 | 9 | import { Wallet } from "@wallets"; 10 | 11 | interface Props { 12 | wallet: Wallet; 13 | } 14 | 15 | export const WalletEditButton: FC = ({ wallet, children }): JSX.Element => { 16 | const [editWalletVisible, setEditWalletVisible] = useState(false); 17 | 18 | return <> 19 | setEditWalletVisible(true)}> 20 | {children} 21 | 22 | 23 | 24 | ; 25 | }; 26 | 27 | export type OpenEditWalletFn = (wallet: Wallet) => void; 28 | export type WalletEditHookRes = [ 29 | OpenEditWalletFn, 30 | JSX.Element | null, 31 | (visible: boolean) => void 32 | ]; 33 | 34 | export function useEditWalletModal(): WalletEditHookRes { 35 | // The modal will only be rendered if it is opened at least once 36 | const [opened, setOpened] = useState(false); 37 | const [visible, setVisible] = useState(false); 38 | const [wallet, setWallet] = useState(); 39 | 40 | const open = useCallback((wallet: Wallet) => { 41 | setWallet(wallet); 42 | setVisible(true); 43 | setOpened(true); 44 | }, []); 45 | 46 | const modal = opened 47 | ? 48 | : null; 49 | 50 | return [open, modal, setVisible]; 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/wallets/WalletsPage.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../App.less"; 5 | @import "../../style/table.less"; 6 | 7 | .wallets-page .table-mobile-list-view { 8 | .wallet-mobile-item { 9 | .wallet-label { 10 | display: block; 11 | font-size: 120%; 12 | } 13 | 14 | .wallet-value { 15 | float: right; 16 | font-size: 120%; 17 | } 18 | 19 | .wallet-first-seen { 20 | display: block; 21 | font-size: @font-size-sm; 22 | color: @text-color-secondary; 23 | } 24 | 25 | .wallet-names { 26 | white-space: nowrap; 27 | } 28 | 29 | .sep:before { 30 | content: "\2013"; 31 | 32 | display: inline-block; 33 | margin: 0 @padding-xs; 34 | color: @text-color-secondary; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/wallets/WalletsPage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import { PageLayout } from "@layout/PageLayout"; 7 | 8 | import { useWallets } from "@wallets"; 9 | 10 | import { useWalletsPageActions } from "./WalletsPageActions"; 11 | import { WalletsTable } from "./WalletsTable"; 12 | 13 | import { useEditWalletModal } from "./WalletEditButton"; 14 | import { useSendTransactionModal } from "@comp/transactions/SendTransactionModalLink"; 15 | import { useWalletInfoModal } from "./info/WalletInfoModal"; 16 | 17 | import "./WalletsPage.less"; 18 | 19 | /** Extract the subtitle into its own component to avoid re-rendering the 20 | * entire page when a wallet is added. */ 21 | function WalletsPageSubtitle(): JSX.Element { 22 | const { t } = useTranslation(); 23 | const { addressList } = useWallets(); 24 | 25 | const count = addressList.length; 26 | 27 | return <>{count > 0 28 | ? t("myWallets.walletCount", { count }) 29 | : t("myWallets.walletCountEmpty") 30 | }; 31 | } 32 | 33 | export function WalletsPage(): JSX.Element { 34 | const [openEditWallet, editWalletModal] = useEditWalletModal(); 35 | const [openSendTx, sendTxModal] = useSendTransactionModal(); 36 | const [openWalletInfo, walletInfoModal] = useWalletInfoModal(); 37 | 38 | const extra = useWalletsPageActions(); 39 | 40 | return } 43 | extra={extra} 44 | className="wallets-page" 45 | > 46 | 51 | 52 | {/* Rendered only once, as an optimisation */} 53 | {editWalletModal} 54 | {sendTxModal} 55 | {walletInfoModal} 56 | ; 57 | } 58 | -------------------------------------------------------------------------------- /src/pages/wallets/info/BooleanText.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Typography } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | const { Text } = Typography; 9 | 10 | export function BooleanText({ value }: { value?: boolean }): JSX.Element { 11 | const { t } = useTranslation(); 12 | 13 | return value 14 | ? {t("myWallets.info.true")} 15 | : {t("myWallets.info.false")}; 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/wallets/info/WalletDescAdvancedInfo.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Descriptions } from "antd"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | import { OptionalField } from "@comp/OptionalField"; 9 | import { DecryptReveal } from "./DecryptReveal"; 10 | import { BooleanText } from "./BooleanText"; 11 | 12 | import { WalletDescProps } from "./WalletInfoModal"; 13 | 14 | export function WalletDescAdvancedInfo({ wallet, descProps }: WalletDescProps): JSX.Element { 15 | const { t } = useTranslation(); 16 | 17 | return 18 | {/* Wallet Encrypted Password */} 19 | 20 | 22 | }/> 23 | 24 | 25 | {/* Wallet Encrypted Private key */} 26 | 27 | 29 | }/> 30 | 31 | 32 | {/* Wallet Saved */} 33 | 34 | 35 | 36 | ; 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/wallets/info/WalletInfoModal.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../../../App.less"; 5 | 6 | .wallet-info-modal { 7 | .ant-modal-body { 8 | max-height: 500px; 9 | overflow-y: auto; 10 | } 11 | 12 | .ant-descriptions { 13 | margin-bottom: @margin-lg; 14 | 15 | .ant-descriptions-header { 16 | margin-bottom: @padding-xs; 17 | } 18 | 19 | &:last-child { 20 | margin-bottom: 0; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/whatsnew/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export interface WhatsNewItem { 5 | commitHash?: string; 6 | date: string; 7 | authorUsername?: string; 8 | authorName?: string; 9 | body: string; 10 | new?: boolean; 11 | } 12 | 13 | export interface Commit { 14 | type?: "feat" | "fix" | string; 15 | subject?: string; 16 | body: string; 17 | hash: string; 18 | authorName?: string; 19 | authorEmail?: string; 20 | authorDate: string; 21 | authorDateRel: string; 22 | avatar?: string; 23 | } 24 | 25 | export interface WhatsNewResponse { 26 | whatsNew: WhatsNewItem[]; 27 | commits: Commit[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | /// 6 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { ReportHandler } from "web-vitals"; 5 | 6 | const reportWebVitals = (onPerfEntry?: ReportHandler): void => { 7 | if (onPerfEntry && onPerfEntry instanceof Function) { 8 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 9 | getCLS(onPerfEntry); 10 | getFID(onPerfEntry); 11 | getFCP(onPerfEntry); 12 | getLCP(onPerfEntry); 13 | getTTFB(onPerfEntry); 14 | }); 15 | } 16 | }; 17 | 18 | export default reportWebVitals; 19 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import "@testing-library/jest-dom"; 5 | -------------------------------------------------------------------------------- /src/store/actions/ContactsActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createAction } from "typesafe-actions"; 5 | 6 | import * as constants from "../constants"; 7 | 8 | import { Contact, ContactMap, ContactUpdatable } from "@contacts"; 9 | 10 | export const loadContacts = createAction(constants.LOAD_CONTACTS)(); 11 | export const addContact = createAction(constants.ADD_CONTACT)(); 12 | export const removeContact = createAction(constants.REMOVE_CONTACT)(); 13 | 14 | export interface UpdateContactPayload { id: string; contact: ContactUpdatable } 15 | export const updateContact = createAction(constants.UPDATE_CONTACT)(); 16 | -------------------------------------------------------------------------------- /src/store/actions/MasterPasswordActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createAction } from "typesafe-actions"; 5 | 6 | import * as constants from "../constants"; 7 | 8 | export interface AuthMasterPasswordPayload { password: string } 9 | export const authMasterPassword = createAction(constants.AUTH_MASTER_PASSWORD, 10 | (password): AuthMasterPasswordPayload => 11 | ({ password }))(); 12 | 13 | export interface SetMasterPasswordPayload { 14 | salt: string; 15 | tester: string; 16 | password: string; 17 | } 18 | export const setMasterPassword = createAction(constants.SET_MASTER_PASSWORD, 19 | (salt, tester, password): SetMasterPasswordPayload => 20 | ({ salt, tester, password }))(); 21 | -------------------------------------------------------------------------------- /src/store/actions/MiscActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createAction } from "typesafe-actions"; 5 | import * as constants from "../constants"; 6 | 7 | export const incrementNameTableLock = createAction(constants.INCR_NAME_TABLE_LOCK)(); 8 | export const decrementNameTableLock = createAction(constants.DECR_NAME_TABLE_LOCK)(); 9 | 10 | export const setTip = createAction(constants.SET_TIP)(); 11 | -------------------------------------------------------------------------------- /src/store/actions/NodeActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createAction } from "typesafe-actions"; 5 | import { 6 | KristWorkDetailed, KristCurrency, KristConstants, KristMOTDBase, 7 | KristMOTDPackage 8 | } from "@api/types"; 9 | 10 | import * as constants from "../constants"; 11 | 12 | export const setLastBlockID = createAction(constants.LAST_BLOCK_ID)(); 13 | export const setLastTransactionID = createAction(constants.LAST_TRANSACTION_ID)(); 14 | export const setLastNonMinedTransactionID = createAction(constants.LAST_NON_MINED_TRANSACTION_ID)(); 15 | export const setLastOwnTransactionID = createAction(constants.LAST_OWN_TRANSACTION_ID)(); 16 | export const setLastNameTransactionID = createAction(constants.LAST_NAME_TRANSACTION_ID)(); 17 | export const setLastOwnNameTransactionID = createAction(constants.LAST_OWN_NAME_TRANSACTION_ID)(); 18 | 19 | export const setSyncNode = createAction(constants.SYNC_NODE)(); 20 | export const setDetailedWork = createAction(constants.DETAILED_WORK)(); 21 | export const setPackage = createAction(constants.PACKAGE)(); 22 | export const setCurrency = createAction(constants.CURRENCY)(); 23 | export const setConstants = createAction(constants.CONSTANTS)(); 24 | export const setMOTD = createAction(constants.MOTD)(); 25 | -------------------------------------------------------------------------------- /src/store/actions/SettingsActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { PickByValue } from "utility-types"; 5 | import { createAction } from "typesafe-actions"; 6 | 7 | import * as constants from "../constants"; 8 | 9 | import { State } from "@reducers/SettingsReducer"; 10 | 11 | import { AnalysedLanguages } from "@pages/settings/translations/analyseLangs"; 12 | 13 | // Boolean settings 14 | export interface SetBooleanSettingPayload { 15 | settingName: keyof PickByValue; 16 | value: boolean; 17 | } 18 | export const setBooleanSetting = createAction(constants.SET_BOOLEAN_SETTING, 19 | (settingName, value): SetBooleanSettingPayload => 20 | ({ settingName, value }))(); 21 | 22 | // Integer settings 23 | export interface SetIntegerSettingPayload { 24 | settingName: keyof PickByValue; 25 | value: number; 26 | } 27 | export const setIntegerSetting = createAction(constants.SET_INTEGER_SETTING, 28 | (settingName, value): SetIntegerSettingPayload => 29 | ({ settingName, value }))(); 30 | 31 | // Set imported language 32 | export const setImportedLang = createAction(constants.SET_IMPORTED_LANG)(); 33 | -------------------------------------------------------------------------------- /src/store/actions/WebsocketActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createAction } from "typesafe-actions"; 5 | import { WSConnectionState } from "@api/types"; 6 | 7 | import { WSSubscription } from "@global/ws/WebsocketSubscription"; 8 | 9 | import * as constants from "../constants"; 10 | 11 | export const setConnectionState = createAction(constants.CONNECTION_STATE)(); 12 | 13 | export interface InitSubscriptionPayload { id: string; subscription: WSSubscription } 14 | export const initSubscription = createAction(constants.INIT_SUBSCRIPTION, 15 | (id, subscription): InitSubscriptionPayload => 16 | ({ id, subscription }))(); 17 | 18 | export interface UpdateSubscriptionPayload { id: string; lastTransactionID: number } 19 | export const updateSubscription = createAction(constants.UPDATE_SUBSCRIPTION, 20 | (id, lastTransactionID): UpdateSubscriptionPayload => 21 | ({ id, lastTransactionID }))(); 22 | 23 | export const removeSubscription = createAction(constants.REMOVE_SUBSCRIPTION)(); 24 | -------------------------------------------------------------------------------- /src/store/actions/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import * as masterPasswordActions from "./MasterPasswordActions"; 5 | import * as walletsActions from "./WalletsActions"; 6 | import * as contactsActions from "./ContactsActions"; 7 | import * as settingsActions from "./SettingsActions"; 8 | import * as websocketActions from "./WebsocketActions"; 9 | import * as nodeActions from "./NodeActions"; 10 | import * as miscActions from "./MiscActions"; 11 | 12 | const RootAction = { 13 | masterPassword: masterPasswordActions, 14 | wallets: walletsActions, 15 | contacts: contactsActions, 16 | settings: settingsActions, 17 | websocket: websocketActions, 18 | node: nodeActions, 19 | misc: miscActions 20 | }; 21 | export default RootAction; 22 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { ActionType, StateType } from "typesafe-actions"; 5 | 6 | export type Store = StateType; 7 | export type RootAction = ActionType; 8 | export type RootState = StateType; 9 | -------------------------------------------------------------------------------- /src/store/init.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { getInitialMasterPasswordState } from "./reducers/MasterPasswordReducer"; 5 | import { getInitialWalletsState } from "./reducers/WalletsReducer"; 6 | import { getInitialContactsState } from "./reducers/ContactsReducer"; 7 | import { getInitialSettingsState } from "./reducers/SettingsReducer"; 8 | import { getInitialNodeState } from "./reducers/NodeReducer"; 9 | 10 | import { createStore, Store } from "redux"; 11 | import { devToolsEnhancer } from "redux-devtools-extension"; 12 | import rootReducer from "./reducers/RootReducer"; 13 | 14 | import { RootState, RootAction } from "./index"; 15 | 16 | export const initStore = (): Store => createStore( 17 | rootReducer, 18 | { 19 | masterPassword: getInitialMasterPasswordState(), 20 | wallets: getInitialWalletsState(), 21 | contacts: getInitialContactsState(), 22 | settings: getInitialSettingsState(), 23 | node: getInitialNodeState() 24 | }, 25 | devToolsEnhancer({}) 26 | ); 27 | -------------------------------------------------------------------------------- /src/store/reducers/MiscReducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createReducer } from "typesafe-actions"; 5 | import { 6 | incrementNameTableLock, decrementNameTableLock, setTip 7 | } from "@actions/MiscActions"; 8 | 9 | export interface State { 10 | readonly nameTableLock: number; 11 | readonly tip: number; 12 | } 13 | 14 | const initialState: State = { 15 | nameTableLock: 0, 16 | tip: localStorage.getItem("tip") !== null 17 | ? parseInt(localStorage.getItem("tip")!) 18 | : -1 19 | }; 20 | 21 | export const MiscReducer = createReducer(initialState) 22 | .handleAction(incrementNameTableLock, (state, _) => ({ 23 | ...state, 24 | nameTableLock: state.nameTableLock + 1 25 | })) 26 | .handleAction(decrementNameTableLock, (state, _) => ({ 27 | ...state, 28 | nameTableLock: state.nameTableLock - 1 29 | })) 30 | .handleAction(setTip, (state, { payload }) => ({ 31 | ...state, tip: payload 32 | })); 33 | -------------------------------------------------------------------------------- /src/store/reducers/RootReducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { combineReducers } from "redux"; 5 | 6 | import { MasterPasswordReducer } from "./MasterPasswordReducer"; 7 | import { WalletsReducer } from "./WalletsReducer"; 8 | import { ContactsReducer } from "./ContactsReducer"; 9 | import { SettingsReducer } from "./SettingsReducer"; 10 | import { WebsocketReducer } from "./WebsocketReducer"; 11 | import { NodeReducer } from "./NodeReducer"; 12 | import { MiscReducer } from "./MiscReducer"; 13 | 14 | export default combineReducers({ 15 | masterPassword: MasterPasswordReducer, 16 | wallets: WalletsReducer, 17 | contacts: ContactsReducer, 18 | settings: SettingsReducer, 19 | websocket: WebsocketReducer, 20 | node: NodeReducer, 21 | misc: MiscReducer 22 | }); 23 | -------------------------------------------------------------------------------- /src/store/reducers/SettingsReducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createReducer } from "typesafe-actions"; 5 | import { loadSettings, SettingsState } from "@utils/settings"; 6 | import { 7 | setBooleanSetting, setIntegerSetting, setImportedLang 8 | } from "@actions/SettingsActions"; 9 | 10 | import { AnalysedLanguages } from "@pages/settings/translations/analyseLangs"; 11 | 12 | export type State = SettingsState & { 13 | /** Language imported by JSON in the translations debug page. */ 14 | readonly importedLang?: AnalysedLanguages; 15 | }; 16 | 17 | export function getInitialSettingsState(): State { 18 | return { 19 | ...loadSettings(), 20 | importedLang: undefined 21 | }; 22 | } 23 | 24 | export const SettingsReducer = createReducer({} as State) 25 | .handleAction(setBooleanSetting, (state, action) => ({ 26 | ...state, 27 | [action.payload.settingName]: action.payload.value 28 | })) 29 | .handleAction(setIntegerSetting, (state, action) => ({ 30 | ...state, 31 | [action.payload.settingName]: action.payload.value 32 | })) 33 | .handleAction(setImportedLang, (state, { payload }) => ({ 34 | ...state, 35 | importedLang: payload 36 | })); 37 | -------------------------------------------------------------------------------- /src/store/reducers/WebsocketReducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { createReducer } from "typesafe-actions"; 5 | import { WSConnectionState } from "@api/types"; 6 | import * as actions from "@actions/WebsocketActions"; 7 | 8 | import { WSSubscription } from "@global/ws/WebsocketSubscription"; 9 | 10 | export interface State { 11 | readonly connectionState: WSConnectionState; 12 | readonly subscriptions: Record; 13 | } 14 | 15 | export const initialState: State = { 16 | connectionState: "disconnected", 17 | subscriptions: {} 18 | }; 19 | 20 | export const WebsocketReducer = createReducer(initialState) 21 | // Set websocket connection state 22 | .handleAction(actions.setConnectionState, (state, { payload }) => ({ 23 | ...state, 24 | connectionState: payload 25 | })) 26 | // Initialise websocket subscription 27 | .handleAction(actions.initSubscription, (state, { payload }) => ({ 28 | ...state, 29 | subscriptions: { 30 | ...state.subscriptions, 31 | [payload.id]: payload.subscription 32 | } 33 | })) 34 | // Update websocket subscription 35 | .handleAction(actions.updateSubscription, (state, { payload }) => ({ 36 | ...state, 37 | subscriptions: { 38 | ...state.subscriptions, 39 | [payload.id]: { 40 | ...state.subscriptions[payload.id], 41 | lastTransactionID: payload.lastTransactionID 42 | } 43 | } 44 | })) 45 | // Remove websocket subscription 46 | .handleAction(actions.removeSubscription, (state, { payload }) => { 47 | // Get the subscriptions without the one we want to remove 48 | const { [payload]: _, ...subscriptions } = state.subscriptions; 49 | return { ...state, subscriptions }; 50 | }); 51 | -------------------------------------------------------------------------------- /src/store/types.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { Store, RootAction, RootState } from "./"; 5 | 6 | declare module "typesafe-actions" { 7 | interface Types { 8 | Store: Store; 9 | RootAction: RootAction; 10 | RootState: RootState; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/style/table.less: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | @import (reference) "../App.less"; 5 | 6 | .table-mobile-list-view { 7 | .ant-list-items .card-list-item { 8 | padding: @padding-sm @padding-md; 9 | margin-bottom: 0; 10 | 11 | border-bottom: 1px solid @border-color-split; 12 | } 13 | 14 | .mobile-item-collapse { 15 | background: transparent; 16 | transition: background @animation-duration-base @ease-in-out; 17 | 18 | 19 | &.card-list-item { padding: 0; } 20 | 21 | // Move the padding from the mobile-item to the collapse header, to make the 22 | // clickable area the full size 23 | .ant-collapse-header { 24 | padding: @padding-sm @padding-md; 25 | 26 | // Darken the background when expanded 27 | &[aria-expanded=true] { 28 | background: @kw-darker; 29 | } 30 | } 31 | 32 | .ant-collapse-content > .ant-collapse-content-box { 33 | // Make the actions menu flush 34 | padding: 0; 35 | 36 | .ant-menu { 37 | .ant-menu-item { 38 | margin-bottom: 0; 39 | } 40 | } 41 | } 42 | } 43 | 44 | .ant-list-pagination { 45 | display: grid; 46 | justify-content: center; 47 | 48 | margin-bottom: @margin-lg; 49 | 50 | .ant-pagination-total-text { 51 | display: block; 52 | text-align: center; 53 | } 54 | 55 | .ant-pagination-item, .ant-pagination-prev { 56 | margin-right: 3px; 57 | } 58 | 59 | .ant-pagination-next { 60 | margin-right: 0; 61 | } 62 | } 63 | 64 | @media (max-width: @screen-md) { 65 | // Make the list full-width 66 | width: 100vw; 67 | margin: 0 -@padding-md; 68 | } 69 | 70 | @media (max-width: @screen-sm) { 71 | margin: 0 -@padding-sm; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/consoleWarning.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | // Present a warning to the user warning about the dangers of Self-XSS. 6 | // Shamelessly based on Facebook and Discord's warning. 7 | // 8 | // REVIEW: There's an interesting article debating whether a warning is the best 9 | // way forward. Since this is sort of a cryptocurrency app, and we deal 10 | // with far too many midiots on a daily basis, I figured that a 11 | // semi-aggressive warning is probably going to be better in the long 12 | // run. That said, this is still a pretty good read: 13 | // http://booktwo.org/notebook/welcome-js/ 14 | export function showConsoleWarning(): void { 15 | console.log("%cHold up!", "color: CornFlowerBlue; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;"); 16 | console.log("%cDon't paste anything here!", "color: red; font-size: 18px; font-weight: bold;"); 17 | console.log("%cThis console is a feature intended for developers. Pasting code in here may result in you getting scammed, and losing your Krist.", "font-size: 18px; font-weight: bold;"); 18 | console.log("%cIf you know what you're doing, then please, carry on. Check out the GitHub: https://github.com/tmpim/KristWeb2", "font-size: 13px;"); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/crypto/generatePassword.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | /** 5 | * Generates a secure random password based on a length and character set. 6 | * 7 | * Implementation mostly sourced from: {@link https://stackoverflow.com/a/51540480/1499974} 8 | * 9 | * See also: {@link https://github.com/chancejs/chancejs/issues/232#issuecomment-182500222} 10 | * 11 | * @param length - The desired length of the password. 12 | * @param charset - A string containing all the characters the password may 13 | * contain. 14 | */ 15 | export function generatePassword( 16 | length = 32, 17 | charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-" 18 | ): string { 19 | // NOTE: talk about correctness with modulo and its bias (the charset is 64 20 | // characters right now anyway) 21 | return Array.from(crypto.getRandomValues(new Uint32Array(length))) 22 | .map(x => charset[x % charset.length]) 23 | .join(""); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export * from "./crypto"; 5 | export * from "./CryptoJS"; 6 | export * from "./generatePassword"; 7 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import * as Sentry from "@sentry/react"; 5 | import { CaptureContext } from "@sentry/types"; 6 | import { Integrations } from "@sentry/tracing"; 7 | 8 | import { message } from "antd"; 9 | 10 | declare const __GIT_VERSION__: string; 11 | const gitVersion: string = __GIT_VERSION__; 12 | 13 | const ls = localStorage.getItem("settings.errorReporting"); 14 | export const errorReporting = process.env.DISABLE_SENTRY !== "true" && 15 | (ls === null || ls === "true"); 16 | export const messageOnErrorReport = localStorage.getItem("settings.messageOnErrorReport") === "true"; 17 | 18 | Sentry.init({ 19 | dsn: errorReporting 20 | ? "https://51a018424102449b88f94c795cf62bb7@sentry.lemmmy.pw/2" 21 | : undefined, 22 | release: "kristweb2-react@" + gitVersion, 23 | integrations: [new Integrations.BrowserTracing()], 24 | 25 | // Disable Sentry error reporting if the setting is disabled: 26 | tracesSampleRate: errorReporting ? 0.2 : 0, 27 | 28 | beforeSend(event) { 29 | // Don't send an error event if error reporting is disabled 30 | if (!errorReporting) return null; 31 | 32 | // Show a message on report if the setting is enabled 33 | if (messageOnErrorReport) { 34 | // TODO: Find a way to translate this, while still ensuring that this file 35 | // is imported before everything else. 36 | message.info("An error was automatically reported. See console for details."); 37 | } 38 | 39 | return event; 40 | }, 41 | 42 | beforeBreadcrumb(breadcrumb) { 43 | // Don't send a breadcrumb event if error reporting is disabled 44 | if (!errorReporting) return null; 45 | return breadcrumb; 46 | } 47 | }); 48 | 49 | export function criticalError( 50 | err: Error | string, 51 | captureContext?: CaptureContext 52 | ): void { 53 | Sentry.captureException(err, captureContext); 54 | console.error("Critical error: ", err); 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export * from "./useBreakpoint"; 5 | export * from "./useHistoryState"; 6 | export * from "./useMountEffect"; 7 | -------------------------------------------------------------------------------- /src/utils/hooks/useHistoryState.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { useHistory, useLocation } from "react-router-dom"; 6 | 7 | import Debug from "debug"; 8 | const debug = Debug("kristweb:useHistoryState"); 9 | 10 | /** 11 | * Wrapper for useState that saves its value in the browser history stack 12 | * as location state. Note that this doesn't yet support computed state. 13 | * 14 | * The state's value must be serialisable, and less than 2 MiB. 15 | * 16 | * @param initialState - The initial value of the state. 17 | * @param stateKey - The key by which to store the state's value in the history 18 | * stack. 19 | */ 20 | export function useHistoryState( 21 | initialState: S, 22 | stateKey: string 23 | ): [S, (s: S) => void] { 24 | const history = useHistory(); 25 | const location = useLocation>>(); 26 | 27 | const [state, setState] = useState( 28 | location?.state?.[stateKey] ?? initialState 29 | ); 30 | 31 | // Wraps setState to update the stored state value and replace the entry on 32 | // the history stack (via `updateLocation`). 33 | function wrappedSetState(newState: S): void { 34 | debug("useHistoryState: setting state %s to %o", stateKey, newState); 35 | updateLocation(newState); 36 | setState(newState); 37 | } 38 | 39 | // Merge the new state into the location state (using stateKey) and replace 40 | // the entry on the history stack. 41 | function updateLocation(newState: S) { 42 | const updatedLocation = { 43 | ...location, 44 | state: { 45 | ...location?.state, 46 | [stateKey]: newState 47 | } 48 | }; 49 | 50 | debug("useHistoryState: replacing updated location:", updatedLocation); 51 | history.replace(updatedLocation); 52 | } 53 | 54 | return [state, wrappedSetState]; 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/hooks/useMountEffect.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useEffect, EffectCallback } from "react"; 5 | 6 | // eslint-disable-next-line react-hooks/exhaustive-deps 7 | export const useMountEffect = (fn: EffectCallback): void => useEffect(fn, []); 8 | -------------------------------------------------------------------------------- /src/utils/i18n/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { TFunction } from "react-i18next"; 5 | 6 | export class TranslatedError extends Error { 7 | constructor(message: string) { super(message); } 8 | } 9 | 10 | export function translateError(t: TFunction, error: Error, unknownErrorKey?: string): string { 11 | if (error instanceof TranslatedError) { 12 | return t(error.message); 13 | } else { 14 | return unknownErrorKey ? t(unknownErrorKey) : error.message; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/i18n/fns.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useCallback, useMemo } from "react"; 5 | import { i18n } from "i18next"; 6 | import { useTranslation, TFunction } from "react-i18next"; 7 | import { TranslatedError } from "./errors"; 8 | 9 | export type TKeyFn = (key: string) => string; 10 | export type TStrFn = (key: string) => string; 11 | export type TErrFn = (key: string) => TranslatedError; 12 | export interface TFns { 13 | t: TFunction; 14 | tKey: TKeyFn; 15 | tStr: TStrFn; 16 | tErr: TErrFn; 17 | i18n: i18n; 18 | } 19 | export function useTranslationFns(prefix?: string): TFns { 20 | const { t, i18n } = useTranslation(); 21 | const tKey = useCallback((key: string) => prefix + key, [prefix]); 22 | const tStr = useCallback((key: string) => t(tKey(key)), [t, tKey]); 23 | const tErr = useCallback((key: string) => new TranslatedError(tKey(key)), [tKey]); 24 | 25 | return useMemo(() => ({ t, tKey, tStr, tErr, i18n }), [ 26 | t, i18n, tKey, tStr, tErr 27 | ]); 28 | } 29 | export const useTFns = useTranslationFns; 30 | -------------------------------------------------------------------------------- /src/utils/i18n/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import i18n from "./init"; 5 | export * from "./init"; 6 | export * from "./languages"; 7 | export * from "./fns"; 8 | export * from "./errors"; 9 | 10 | export default i18n; 11 | -------------------------------------------------------------------------------- /src/utils/i18n/languages.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import languagesJson from "../../__data__/languages.json"; 5 | 6 | export interface Language { 7 | name: string; 8 | nativeName?: string; 9 | country?: string; 10 | dayjsLocale?: string; 11 | timeagoLocale?: string; 12 | antLocale?: string; 13 | contributors: Contributor[]; 14 | } 15 | 16 | export interface Contributor { 17 | name: string; 18 | url?: string; 19 | } 20 | 21 | export type Languages = { [key: string]: Language } | null; 22 | export function getLanguages(): Languages { 23 | return languagesJson; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export * from "./errors"; 5 | export * from "./misc/credits"; 6 | export * from "./misc/devState"; 7 | export * from "./misc/math"; 8 | export * from "./misc/promiseThrottle"; 9 | export * from "./misc/sort"; 10 | 11 | export const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.0/8 are considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | /** Returns the ⌘ (command) symbol on macOS, and "Ctrl" everywhere else. */ 22 | export const ctrl = /mac/i.test(navigator.platform) ? "\u2318" : "Ctrl"; 23 | 24 | export function toLookup(arr: string[]): Record { 25 | const out: Record = {}; 26 | if (!arr) return out; 27 | 28 | for (let i = 0; i < arr.length; i++) { 29 | out[arr[i]] = true; 30 | } 31 | 32 | return out; 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/krist/addressAlgo.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { sha256, doubleSHA256 } from "@utils/crypto"; 5 | 6 | const hexToBase36 = (input: number): string => { 7 | const byte = 48 + Math.floor(input / 7); 8 | return String.fromCharCode(byte + 39 > 122 ? 101 : byte > 57 ? byte + 39 : byte); 9 | }; 10 | 11 | export const makeV2Address = async (addressPrefix: string, key: string): Promise => { 12 | const chars = ["", "", "", "", "", "", "", "", ""]; 13 | let chain = addressPrefix; 14 | let hash = await doubleSHA256(key); 15 | 16 | for (let i = 0; i <= 8; i++) { 17 | chars[i] = hash.substring(0, 2); 18 | hash = await doubleSHA256(hash); 19 | } 20 | 21 | for (let i = 0; i <= 8;) { 22 | const index = parseInt(hash.substring(2 * i, 2 + (2 * i)), 16) % 9; 23 | 24 | if (chars[index] === "") { 25 | hash = await sha256(hash); 26 | } else { 27 | chain += hexToBase36(parseInt(chars[index], 16)); 28 | chars[index] = ""; 29 | i++; 30 | } 31 | } 32 | 33 | return chain; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/krist/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export * from "./addressAlgo"; 5 | export * from "./commonmeta"; 6 | export * from "./currency"; 7 | -------------------------------------------------------------------------------- /src/utils/misc/credits.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { useState } from "react"; 5 | import { useMountEffect } from "@utils/hooks"; 6 | import packageJson from "../../../package.json"; 7 | 8 | export function getAuthorInfo(): { authorName: string; authorURL: string; gitURL: string } { 9 | const authorName = packageJson.author || "Lemmmy"; 10 | const authorURL = `https://github.com/${authorName}`; 11 | const gitURL = packageJson.repository.url.replace(/\.git$/, ""); 12 | 13 | return { authorName, authorURL, gitURL }; 14 | } 15 | 16 | export interface HostInfo { 17 | host: { 18 | name: string; 19 | url: string; 20 | }; 21 | } 22 | 23 | export function useHostInfo(): HostInfo | undefined { 24 | const [host, setHost] = useState(); 25 | 26 | useMountEffect(() => { 27 | (async () => { 28 | try { 29 | // Add the host information if host.json exists 30 | const hostFile = "host-attribution"; // Trick webpack into dynamic importing 31 | const hostData = await import("../../__data__/" + hostFile + ".json"); 32 | setHost(hostData); 33 | } catch (ignored) { 34 | // Ignored 35 | } 36 | })(); 37 | }); 38 | 39 | return host; 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/misc/devState.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | declare const __GIT_VERSION__: string; 5 | 6 | const devEnvs = ["development", "local", "test"]; 7 | const dirtyRegex = /-dirty$/; 8 | 9 | interface DevState { 10 | gitVersion: string; 11 | isDirty: boolean; 12 | isDev: boolean; 13 | } 14 | 15 | export function getDevState(): DevState { 16 | // Determine if the 'dev' tag should be shown 17 | // Replaced by webpack DefinePlugin and git-revision-webpack-plugin 18 | const gitVersion: string = __GIT_VERSION__; 19 | const isDirty = dirtyRegex.test(gitVersion); 20 | const isDev = devEnvs.includes(process.env.NODE_ENV || "development"); 21 | 22 | return { gitVersion, isDirty, isDev }; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/misc/math.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | export const toHex = (input: ArrayBufferLike | Uint8Array): string => 5 | [...(input instanceof Uint8Array ? input : new Uint8Array(input))] 6 | .map(b => b.toString(16).padStart(2, "0")) 7 | .join(""); 8 | 9 | export const fromHex = (input: string): Uint8Array => 10 | new Uint8Array((input.match(/.{1,2}/g) || []).map(b => parseInt(b, 16))); 11 | 12 | export const mod = (n: number, m: number): number => ((n % m) + m) % m; 13 | -------------------------------------------------------------------------------- /src/utils/misc/sort.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | /** Sort an array in-place in a human-friendly manner. */ 5 | export function localeSort(arr: any[]): void { 6 | arr.sort((a, b) => a.localeCompare(b, undefined, { 7 | sensitivity: "base", 8 | numeric: true 9 | })); 10 | } 11 | 12 | /** 13 | * Sorting function that pushes nullish to the end of the array. 14 | * 15 | * @param key - The property of T to sort by. 16 | * @param human - Whether or not to use a human-friendly locale sort for 17 | * string values. 18 | */ 19 | export const keyedNullSort = (key: keyof T, human?: boolean) => (a: T, b: T, sortOrder?: "ascend" | "descend" | null): number => { 20 | // We effectively reverse the sort twice when sorting in 'descend' mode, as 21 | // ant-design will internally reverse the array, but we always want to push 22 | // nullish values to the end. 23 | const va = sortOrder === "descend" ? b[key] : a[key]; 24 | const vb = sortOrder === "descend" ? a[key] : b[key]; 25 | 26 | // Push nullish values to the end 27 | if (va === vb) return 0; 28 | if (va === undefined || va === null) return 1; 29 | if (vb === undefined || vb === null) return -1; 30 | 31 | if (typeof va === "string" && typeof vb === "string") { 32 | // Use localeCompare for strings 33 | const ret = va.localeCompare(vb, undefined, human ? { 34 | sensitivity: "base", 35 | numeric: true 36 | } : undefined); 37 | return sortOrder === "descend" ? -ret : ret; 38 | } else { 39 | // Use the built-in comparison for everything else (mainly numbers) 40 | return sortOrder === "descend" 41 | ? (vb as any) - (va as any) 42 | : (va as any) - (vb as any); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/setup.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under AGPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | import { toHex } from "./"; 5 | import Debug from "debug"; 6 | 7 | // Set up custom debug formatters 8 | // Booleans (%b) 9 | Debug.formatters.b = (v: boolean) => v ? "true" : "false"; 10 | // Buffers as hex strings (%x) 11 | Debug.formatters.x = (v: ArrayBufferLike | Uint8Array) => toHex(v); 12 | 13 | import { showConsoleWarning } from "./consoleWarning"; 14 | showConsoleWarning(); 15 | -------------------------------------------------------------------------------- /tools/commitLog.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under GPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | const gitlog = require("gitlog").default; 6 | 7 | // Based on the Krist code 8 | const messageTypeRe = /^(\w+): (.+)/; 9 | function formatCommits(commits) { 10 | const newCommits = []; 11 | 12 | for (const commit of commits) { 13 | if (!commit.subject) continue; 14 | 15 | const [, type, rest] = messageTypeRe.exec(commit.subject) || []; 16 | if (type) { 17 | commit.type = type; 18 | commit.subject = rest; 19 | } 20 | 21 | // Not possible until async is figured out 22 | // commit.avatar = await getAvatar(commit); 23 | 24 | newCommits.push({ 25 | type: commit.type, 26 | subject: commit.subject, 27 | body: commit.body, 28 | hash: commit.hash, 29 | authorName: commit.authorName, 30 | authorEmail: commit.authorEmail, 31 | // Git dates are not strict ISO-8601 by default 32 | authorDate: new Date(commit.authorDate).toISOString(), 33 | authorDateRel: commit.authorDateRel, 34 | avatar: commit.avatar, 35 | }); 36 | } 37 | 38 | return newCommits; 39 | } 40 | 41 | // This is performed synchronously until I can find a way to do async defines 42 | // in webpack. This unfortunately also means that there won't be any avatars. 43 | const commits = formatCommits(gitlog({ 44 | repo: __dirname, 45 | number: 5, 46 | fields: [ 47 | "subject", "body", "hash", 48 | "authorName", "authorEmail", "authorDate", "authorDateRel" 49 | ] 50 | })); 51 | 52 | module.exports = { commits }; 53 | -------------------------------------------------------------------------------- /tsconfig.extend.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "@app": ["./App.tsx"], 6 | 7 | "@actions": ["./store/actions"], 8 | "@actions/*": ["./store/actions/*"], 9 | "@reducers/*": ["./store/reducers/*"], 10 | "@store": ["./store"], 11 | "@store/*": ["./store/*"], 12 | 13 | "@comp/*": ["./components/*"], 14 | "@layout/*": ["./layout/*"], 15 | "@pages/*": ["./pages/*"], 16 | 17 | "@api": ["./krist/api"], 18 | "@api/*": ["./krist/api/*"], 19 | "@wallets": ["./krist/wallets"], 20 | "@wallets/*": ["./krist/wallets/*"], 21 | "@contacts": ["./krist/contacts"], 22 | "@contacts/*": ["./krist/contacts/*"], 23 | "@krist/*": ["./krist/*"], 24 | "@global/*": ["./global/*"], 25 | 26 | "@utils": ["./utils"], 27 | "@utils/*": ["./utils/*"] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.extend.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "downlevelIteration": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "noImplicitAny": true, 24 | "jsx": "react-jsx", 25 | "typeRoots": [ 26 | "./node_modules/@types", 27 | "./typings" 28 | ] 29 | }, 30 | "include": [ 31 | "src", 32 | "typings" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /typings/react-timeago/lib/formatters/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Drew Lemmy 2 | // This file is part of KristWeb 2 under GPL-3.0. 3 | // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt 4 | 5 | // TODO: PR this to DefinitelyTyped 6 | 7 | // Based off of the Flow types: 8 | // https://github.com/nmn/react-timeago/blob/master/src/formatters/buildFormatter.js 9 | 10 | type Unit = 11 | | "second" 12 | | "minute" 13 | | "hour" 14 | | "day" 15 | | "week" 16 | | "month" 17 | | "year"; 18 | 19 | type Suffix = "ago" | "from now"; 20 | 21 | type Formatter = ( 22 | value: number, 23 | unit: Unit, 24 | suffix: Suffix, 25 | epochMiliseconds: number, 26 | nextFormatter?: Formatter 27 | ) => React.ReactNode; 28 | 29 | type StringOrFn = string | ((value: number, millisDelta: number) => string); 30 | type NumberArray = [ 31 | string, 32 | string, 33 | string, 34 | string, 35 | string, 36 | string, 37 | string, 38 | string, 39 | string, 40 | string, 41 | ]; 42 | 43 | interface L10nsStrings { 44 | prefixAgo?: StringOrFn; 45 | prefixFromNow?: StringOrFn; 46 | suffixAgo?: StringOrFn; 47 | suffixFromNow?: StringOrFn; 48 | second?: StringOrFn; 49 | seconds?: StringOrFn; 50 | minute?: StringOrFn; 51 | minutes?: StringOrFn; 52 | hour?: StringOrFn; 53 | hours?: StringOrFn; 54 | day?: StringOrFn; 55 | days?: StringOrFn; 56 | week?: StringOrFn; 57 | weeks?: StringOrFn; 58 | month?: StringOrFn; 59 | months?: StringOrFn; 60 | year?: StringOrFn; 61 | years?: StringOrFn; 62 | wordSeparator?: string; 63 | numbers?: NumberArray; 64 | } 65 | 66 | declare module "react-timeago/lib/formatters/*" { 67 | export default function buildFormatter(strings: L10nsStrings): Formatter; 68 | } 69 | --------------------------------------------------------------------------------