feat: privacy settings (#37)

* feat:  privacy configs

* chore: 🔧 chat polling speed

* refactor: ♻️ chat state

* chore: 🔧 move config to cookie
This commit is contained in:
Anthony Potdevin 2020-05-11 06:21:16 +02:00 committed by GitHub
parent ef5c2d16f4
commit 3d29616300
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 664 additions and 434 deletions

View file

@ -4,6 +4,11 @@ module.exports = {
parserOptions: {
ecmaFeatures: { jsx: true },
},
env: {
browser: true,
amd: true,
node: true,
},
plugins: ['react', 'jest', 'import', 'prettier'],
settings: {
react: {
@ -37,7 +42,12 @@ module.exports = {
'import/no-unresolved': 'off',
camelcase: 'off',
'@typescript-eslint/camelcase': 'off',
'prettier/prettier': 'error',
'react/prop-types': 'off',
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
},
};

View file

@ -1,6 +1,3 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
"editor.formatOnSave": true
}

127
README.md
View file

@ -1,22 +1,32 @@
# **ThunderHub - Lightning Node Manager**
![Home Screenshot](./docs/Home.png)
![Home Screenshot](/docs/Home.png)
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE)
## Table Of Contents
- [Introduction](#introduction)
- [Integrations](#integrations)
- [Features](#features)
- [Installation](#installation)
- [Development](#development)
- [Docker deployment](#docker)
## Introduction
ThunderHub is an **open-source** LND node manager where you can manage and monitor your node on any device or browser. It allows you to take control of the lightning network with a simple and intuitive UX and the most up-to-date tech stack.
### Integrations
**BTCPay Server**
ThunderHub is currently integrated into BTCPay for easier deployment. If you already have a BTCPay server and want to add ThunderHub or even want to start a BTCPay server from zero, be sure to check out this [tutorial](https://apotdevin.com/blog/thunderhub-btcpay)
**Raspiblitz**
For Raspiblitz users you can also get ThunderHub running by following this [gist](https://gist.github.com/openoms/8ba963915c786ce01892f2c9fa2707bc)
### Tech Stack
This repository consists of a **NextJS** server that handles both the backend **Graphql Server** and the frontend **React App**.
This repository consists of a **NextJS** server that handles both the backend **Graphql Server** and the frontend **React App**. ThunderHub connects to your Lightning Network node by using the gRPC ports.
- NextJS
- ReactJS
@ -50,7 +60,7 @@ This repository consists of a **NextJS** server that handles both the backend **
- Send and Receive Bitcoin payments.
- Decode lightning payment requests.
- Open and close channels.
- Balance your channels through circular payments. ([Check out the Tutorial](https://medium.com/coinmonks/lightning-network-channel-balancing-with-thunderhub-972b41bf9243))
- Balance your channels through circular payments. ([Check out the Tutorial](https://apotdevin.com/blog/thunderhub-balancing))
- Update your all your channels fees or individual ones.
- Backup, verify and recover all your channels.
- Sign and verify messages.
@ -70,54 +80,135 @@ This repository consists of a **NextJS** server that handles both the backend **
### Deployment
- Docker images for easier deployment (WIP)
- Docker images for easier deployment
### Future Features
- Channel health/recommendations view
- Loop In and Out to provide liquidity or remove it from your channels.
- Integration with HodlHodl
- Storefront interface
## **Requirements**
- Yarn/npm installed
- Node installed (Version 12.16.0 or higher)
**Older Versions of Node**
Earlier versions of Node can be used if you replace the following commands:
```js
//Yarn
yarn start -> yarn start:compatible
yarn dev -> yarn dev:compatible
//NPM
npm start -> npm start:compatible
npm run dev -> npm run dev:compatible
```
**HodlHodl integration will not work with older versions of Node!**
## Config
You can define some environment variables that ThunderHub can start with. To do this create a `.env` file in the root directory with the following parameters:
```js
THEME = 'dark' | 'light'; // Default: 'dark'
CURRENCY = 'sat' | 'btc' | 'eur' | 'usd'; // Default: 'sat'
FETCH_PRICES = true | false // Default: true
FETCH_FEES = true | false // Default: true
HODL_KEY='[Key provided by HodlHodl]' //Default: ''
BASE_PATH='[Base path where you want to have thunderhub running i.e. '/btcpay']' //Default: '/'
```
### Fetching prices and fees
ThunderHub fetches fiat prices from [Blockchain.com](https://blockchain.info/ticker)'s api and bitcoin on chain fees from [Earn.com](https://bitcoinfees.earn.com/api/v1/fees/recommended)'s api.
If you want to deactivate these requests you can set `FETCH_PRICES=false` and `FETCH_FEES=false` in your `.env` file or manually change them inside the settings view of ThunderHub.
### Running on different base path
Adding a BASE_PATH will run the ThunderHub server on a different base path.
For example:
- default base path of `/` runs ThunderHub on `http://localhost:3000`
- base path of `/thub` runs ThunderHub on `http://localhost:3000/thub`
To run on a base path, ThunderHub needs to be behind a proxy with the following configuration (NGINX example):
```nginx
location /thub/ {
rewrite ^/thub(.*)$ $1 break;
proxy_pass http://localhost:3000/;
}
```
## Installation
To run ThunderHub you first need to clone this repository.
```javascript
```js
git clone https://github.com/apotdevin/thunderhub.git
```
### **Requirements**
After cloning the repository run `yarn` or `npm install` to get all the necessary modules installed.
- Node installed
- Yarn installed
After cloning the repository run `yarn` to get all the necessary modules installed.
After `yarn` has finished installing all the dependencies you can proceed to build and run the app with the following commands.
After all the dependencies have finished installing, you can proceed to build and run the app with the following commands.
```javascript
//Yarn
yarn build
yarn start
//NPM
npm run build
npm start
```
This will start the server on port 3000, so just head to `localhost:3000` to see the app running.
This will start the server on port 3000, so just go to `localhost:3000` to see the app running.
#### HodlHodl Integration
If you want to specify a different port (for example port `4000`) run with:
To be able to use the HodlHodl integration create a `.env` file in the root folder with `HODL_KEY='[YOUR API KEY]'` and replace `[YOUR API KEY]` with the one that HodlHodl provides you.
```js
// Yarn
yarn start -p 4000
// NPM
npm start -p 4000
```
## Development
If you want to develop on ThunderHub and want hot reloading when you do changes, use the following commands:
```javascript
```js
//Yarn
yarn dev
//NPM
npm run dev
```
#### Storybook
You can also get storybook running for quicker component development.
```javascript
```js
//Yarn
yarn storybook
//NPM
npm run storybook
```
## Docker
ThunderHub also provides docker images for easier deployment. [Docker Hub](https://hub.docker.com/repository/docker/apotdevin/thunderhub)
To get ThunderHub running with docker follow these steps:
1. `docker pull apotdevin/thunderhub:v0.5.5` (Or the latest version you find)
2. `docker run --rm -it -p 3000:3000/tcp apotdevin/thunderhub:v0.5.5`
You can now go to `localhost:3000` to see your running instance of ThunderHub

View file

@ -1,3 +1,4 @@
/* eslint @typescript-eslint/no-var-requires: 0 */
require('dotenv').config();
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
@ -17,6 +18,9 @@ module.exports = withBundleAnalyzer({
apiBaseUrl: `${process.env.API_BASE_URL || ''}/api/v1`,
basePath: process.env.BASE_PATH || '',
npmVersion: process.env.npm_package_version || '0.0.0',
trustNeeded: process.env.TRUST || false,
defaultTheme: process.env.THEME || 'dark',
defaultCurrency: process.env.CURRENCY || 'sat',
fetchPrices: process.env.FETCH_PRICES === 'true' ? true : false,
fetchFees: process.env.FETCH_FEES === 'true' ? true : false,
},
});

View file

@ -5,8 +5,10 @@
"main": "index.js",
"scripts": {
"dev": "cross-env NODE_OPTIONS='--insecure-http-parser' next",
"dev:compatible": "next",
"build": "next build",
"start": "cross-env NODE_OPTIONS='--insecure-http-parser' next start",
"start:compatible": "next start",
"lint": "eslint */**/*.{js,ts,tsx} --quiet --fix",
"prettier": "prettier --write **/*.{ts,tsx,js,css,html}",
"release": "standard-version",
@ -33,6 +35,7 @@
"apollo-boost": "^0.4.7",
"apollo-server-micro": "^2.12.0",
"base64url": "^3.0.1",
"cookie": "^0.4.1",
"crypto-js": "^4.0.0",
"date-fns": "^2.12.0",
"dotenv": "^8.2.0",
@ -42,6 +45,7 @@
"graphql-tag": "^2.10.3",
"intersection-observer": "^0.10.0",
"isomorphic-unfetch": "^3.0.0",
"js-cookie": "^2.2.1",
"ln-service": "^48.0.5",
"lodash.debounce": "^4.0.8",
"lodash.groupby": "^4.6.0",

View file

@ -1,8 +1,7 @@
import App from 'next/app';
import React from 'react';
import * as React from 'react';
import { ContextProvider } from '../src/context/ContextProvider';
import { ThemeProvider } from 'styled-components';
import { useSettings } from '../src/context/SettingsContext';
import { useConfigState, ConfigProvider } from '../src/context/ConfigContext';
import { ModalProvider, BaseModalBackground } from 'styled-react-modal';
import { GlobalStyles } from '../src/styles/GlobalStyle';
import { Header } from '../src/layouts/header/Header';
@ -20,11 +19,12 @@ import { PageWrapper, HeaderBodyWrapper } from '../src/layouts/Layout.styled';
import { useStatusState } from '../src/context/StatusContext';
import { ChatFetcher } from '../src/components/chat/ChatFetcher';
import { ChatInit } from '../src/components/chat/ChatInit';
import { parseCookies } from '../src/utils/cookies';
toast.configure({ draggable: false, pauseOnFocusLoss: false });
const Wrapper: React.FC = ({ children }) => {
const { theme } = useSettings();
const { theme } = useConfigState();
const { pathname } = useRouter();
const { connected } = useStatusState();
@ -63,24 +63,36 @@ const Wrapper: React.FC = ({ children }) => {
);
};
class MyApp extends App<any> {
render() {
const { Component, pageProps, apollo } = this.props;
return (
<>
<Head>
<title>ThunderHub - Lightning Node Manager</title>
</Head>
<ApolloProvider client={apollo}>
<ContextProvider>
<Wrapper>
<Component {...pageProps} />
</Wrapper>
</ContextProvider>
</ApolloProvider>
</>
);
}
}
const App = ({ Component, pageProps, apollo, initialConfig }: any) => (
<>
<Head>
<title>ThunderHub - Lightning Node Manager</title>
</Head>
<ApolloProvider client={apollo}>
<ConfigProvider initialConfig={initialConfig}>
<ContextProvider>
<Wrapper>
<Component {...pageProps} />
</Wrapper>
</ContextProvider>
</ConfigProvider>
</ApolloProvider>
</>
);
export default withApollo(MyApp);
App.getInitialProps = async props => {
const cookies = parseCookies(props.ctx.req);
if (!cookies?.config) {
return { initialConfig: {} };
}
try {
const config = JSON.parse(cookies.config);
return {
initialConfig: config,
};
} catch (error) {
return { initialConfig: {} };
}
};
export default withApollo(App);

View file

@ -10,7 +10,7 @@ import {
ColorButton,
} from '../src/components/generic/Styled';
import { useAccount } from '../src/context/AccountContext';
import { useSettings } from '../src/context/SettingsContext';
import { useConfigState } from '../src/context/ConfigContext';
import { textColorMap } from '../src/styles/Themes';
import { useGetChannelAmountInfoQuery } from '../src/generated/graphql';
@ -22,7 +22,7 @@ const ChannelView = () => {
closed: 0,
});
const { theme } = useSettings();
const { theme } = useConfigState();
const { auth } = useAccount();
const { data } = useGetChannelAmountInfoQuery({

View file

@ -48,7 +48,7 @@ const FeesView = () => {
? toast.success('Fees Updated')
: toast.error('Error updating fees');
},
refetchQueries: ['GetChannelFees'],
refetchQueries: ['ChannelFees'],
});
if (loading || !data || !data.getChannelFees) {

View file

@ -13,7 +13,7 @@ import { getErrorContent } from '../src/utils/error';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { ForwardCard } from '../src/views/forwards/ForwardsCard';
import { textColorMap } from '../src/styles/Themes';
import { useSettings } from '../src/context/SettingsContext';
import { useConfigState } from '../src/context/ConfigContext';
import { ForwardBox } from '../src/views/home/reports/forwardReport';
import { useGetForwardsQuery } from '../src/generated/graphql';
@ -28,7 +28,7 @@ const ForwardsView = () => {
const [time, setTime] = useState('week');
const [indexOpen, setIndexOpen] = useState(0);
const { theme } = useSettings();
const { theme } = useConfigState();
const { auth } = useAccount();
const { loading, data } = useGetForwardsQuery({

View file

@ -8,6 +8,7 @@ import { DangerView } from '../src/views/settings/Danger';
import { CurrentSettings } from '../src/views/settings/Current';
import { SyncSettings } from '../src/views/settings/Sync';
import { ChatSettings } from '../src/views/settings/Chat';
import { PrivacySettings } from '../src/views/settings/Privacy';
export const ButtonRow = styled.div`
width: auto;
@ -30,6 +31,7 @@ const SettingsView = () => {
return (
<>
<InterfaceSettings />
<PrivacySettings />
<ChatSettings />
<SyncSettings />
<CurrentSettings />

View file

@ -1,7 +1,7 @@
import React from 'react';
import { useSpring, animated } from 'react-spring';
import { getValue } from '../../utils/helpers';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState } from '../../context/ConfigContext';
import { usePriceState } from '../../context/PriceContext';
type PriceProps = {
@ -19,8 +19,12 @@ export const AnimatedNumber = ({ amount = 0 }: AnimatedProps) => {
from: { value: 0 },
value: amount,
});
const { currency } = useSettings();
const { prices } = usePriceState();
const { currency, displayValues } = useConfigState();
const { prices, dontShow } = usePriceState();
if (!displayValues) {
return <>-</>;
}
let priceProps: PriceProps = {
price: 0,
@ -28,7 +32,7 @@ export const AnimatedNumber = ({ amount = 0 }: AnimatedProps) => {
currency: currency !== 'btc' && currency !== 'sat' ? 'sat' : currency,
};
if (prices) {
if (prices && !dontShow) {
const current: { last: number; symbol: string } = prices[currency] ?? {
last: 0,
symbol: '',

View file

@ -1,13 +1,7 @@
import React from 'react';
import { Checkbox } from '../../checkbox/Checkbox';
import { CheckboxText, StyledContainer, FixedWidth } from '../Auth.styled';
import { AlertCircle } from 'react-feather';
import { fontColors } from '../../../styles/Themes';
import { CheckboxText } from '../Auth.styled';
import { ColorButton } from '../../buttons/colorButton/ColorButton';
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
const { trustNeeded } = publicRuntimeConfig;
type CheckboxProps = {
handleClick: () => void;
@ -37,24 +31,5 @@ export const RiskCheckboxAndConfirm = ({
>
Connect
</ColorButton>
<WarningBox />
</>
);
export const WarningBox = () => {
if (!trustNeeded) {
return null;
}
return (
<StyledContainer>
<FixedWidth>
<AlertCircle size={18} color={fontColors.grey7} />
</FixedWidth>
<CheckboxText>
Macaroons are handled by the ThunderHub server to connect to your LND
node but are never stored. Still, this involves a certain degree of
trust you must be aware of.
</CheckboxText>
</StyledContainer>
);
};

View file

@ -1,27 +1,34 @@
import { useEffect } from 'react';
import { useBitcoinDispatch } from '../../context/BitcoinContext';
import { useGetBitcoinFeesQuery } from '../../generated/graphql';
import { useConfigState } from '../../context/ConfigContext';
export const BitcoinFees = () => {
const { fetchFees } = useConfigState();
const setInfo = useBitcoinDispatch();
const { loading, data, stopPolling } = useGetBitcoinFeesQuery({
skip: !fetchFees,
fetchPolicy: 'network-only',
onError: () => {
setInfo({ type: 'error' });
setInfo({ type: 'dontShow' });
stopPolling();
},
pollInterval: 60000,
});
useEffect(() => {
if (!loading && data && data.getBitcoinFees) {
const { fast, halfHour, hour } = data.getBitcoinFees;
setInfo({
type: 'fetched',
state: { loading: false, error: false, fast, halfHour, hour },
});
if (!fetchFees) {
setInfo({ type: 'dontShow' });
}
}, [data, loading, setInfo]);
}, [fetchFees, setInfo]);
useEffect(() => {
if (!loading && data && data.getBitcoinFees && fetchFees) {
const { fast, halfHour, hour } = data.getBitcoinFees;
setInfo({ type: 'fetched', state: { fast, halfHour, hour } });
}
}, [data, loading, setInfo, fetchFees]);
return null;
};

View file

@ -1,29 +1,38 @@
import { useEffect } from 'react';
import { usePriceDispatch } from '../../context/PriceContext';
import { useGetBitcoinPriceQuery } from '../../generated/graphql';
import { useConfigState } from '../../context/ConfigContext';
export const BitcoinPrice = () => {
const { fetchPrices } = useConfigState();
const setPrices = usePriceDispatch();
const { loading, data, stopPolling } = useGetBitcoinPriceQuery({
skip: !fetchPrices,
fetchPolicy: 'network-only',
onError: () => setPrices({ type: 'error' }),
onError: () => {
setPrices({ type: 'dontShow' });
stopPolling();
},
pollInterval: 60000,
});
useEffect(() => {
if (!loading && data && data.getBitcoinPrice) {
if (!fetchPrices) {
setPrices({ type: 'dontShow' });
}
}, [fetchPrices, setPrices]);
useEffect(() => {
if (!loading && data && data.getBitcoinPrice && fetchPrices) {
try {
const prices = JSON.parse(data.getBitcoinPrice);
setPrices({
type: 'fetched',
state: { loading: false, error: false, prices },
});
setPrices({ type: 'fetched', state: { prices } });
} catch (error) {
setPrices({ type: 'dontShow' });
stopPolling();
setPrices({ type: 'error' });
}
}
}, [data, loading, setPrices, stopPolling]);
}, [data, loading, setPrices, stopPolling, fetchPrices]);
return null;
};

View file

@ -5,10 +5,13 @@ import { useAccount } from '../../context/AccountContext';
import { toast } from 'react-toastify';
import { getErrorContent } from '../../utils/error';
import { useRouter } from 'next/router';
import { useConfigState } from '../../context/ConfigContext';
export const ChatFetcher = () => {
const newChatToastId = 'newChatToastId';
const { chatPollingSpeed } = useConfigState();
const { auth } = useAccount();
const { pathname } = useRouter();
const { lastChat, chats, sentChats, initialized } = useChatState();
@ -18,7 +21,7 @@ export const ChatFetcher = () => {
const { data, loading, error } = useGetMessagesQuery({
skip: !auth || initialized || noChatsAvailable,
pollInterval: 1000,
pollInterval: chatPollingSpeed,
fetchPolicy: 'network-only',
variables: { auth, initialize: !noChatsAvailable },
onError: error => toast.error(getErrorContent(error)),
@ -58,7 +61,7 @@ export const ChatFetcher = () => {
const last = newMessages[0]?.id;
dispatch({ type: 'additional', chats: newMessages, lastChat: last });
}
}, [data, loading, error]);
}, [data, loading, error, dispatch, lastChat, pathname]);
return null;
};

View file

@ -19,10 +19,6 @@ export const ChatInit = () => {
React.useEffect(() => {
const storageChats = localStorage.getItem(`${id}-sentChats`) || '';
const hideFee = localStorage.getItem('hideFee') === 'true' ? true : false;
const hideNonVerified =
localStorage.getItem('hideNonVerified') === 'true' ? true : false;
const maxFee = Number(localStorage.getItem('maxChatFee')) || 20;
if (storageChats !== '') {
try {
@ -33,9 +29,6 @@ export const ChatInit = () => {
type: 'initialized',
sentChats: savedChats,
sender,
hideFee,
hideNonVerified,
maxFee,
});
}
} catch (error) {
@ -43,7 +36,7 @@ export const ChatInit = () => {
}
}
getMessages();
}, []);
}, [dispatch, getMessages, id]);
React.useEffect(() => {
if (!initLoading && !initError && initData?.getMessages) {
@ -64,7 +57,7 @@ export const ChatInit = () => {
sender,
});
}
}, [initLoading, initError, initData]);
}, [initLoading, initError, initData, dispatch]);
return null;
};

View file

@ -41,13 +41,13 @@ export const CloseChannel = ({
channelId,
channelName,
}: CloseChannelProps) => {
const { fast, halfHour, hour, dontShow } = useBitcoinState();
const [isForce, setIsForce] = useState<boolean>(false);
const [isType, setIsType] = useState<string>('none');
const [isType, setIsType] = useState<string>(dontShow ? 'fee' : 'none');
const [amount, setAmount] = useState<number>(0);
const [isConfirmed, setIsConfirmed] = useState<boolean>(false);
const { fast, halfHour, hour } = useBitcoinState();
const [closeChannel] = useCloseChannelMutation({
onCompleted: data => {
if (data.closeChannel) {
@ -113,7 +113,8 @@ export const CloseChannel = ({
<Sub4Title>Fee:</Sub4Title>
</SingleLine>
<MultiButton>
{renderButton(() => setIsType('none'), 'Auto', isType === 'none')}
{!dontShow &&
renderButton(() => setIsType('none'), 'Auto', isType === 'none')}
{renderButton(() => setIsType('fee'), 'Fee', isType === 'fee')}
{renderButton(() => setIsType('target'), 'Target', isType === 'target')}
</MultiButton>

View file

@ -11,7 +11,7 @@ import {
import { HelpCircle } from 'react-feather';
import styled from 'styled-components';
import ReactTooltip from 'react-tooltip';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState } from '../../context/ConfigContext';
import { getTooltipType } from '../generic/helpers';
const StyledQuestion = styled(HelpCircle)`
@ -20,10 +20,9 @@ const StyledQuestion = styled(HelpCircle)`
export const NodeBar = () => {
const { accounts } = useAccount();
const { nodeInfo } = useSettings();
const { multiNodeInfo, theme } = useConfigState();
const slider = React.useRef<HTMLDivElement>(null);
const { theme } = useSettings();
const tooltipType: any = getTooltipType(theme);
const viewOnlyAccounts = accounts.filter(account => account.viewOnly !== '');
@ -38,7 +37,7 @@ export const NodeBar = () => {
}
};
if (viewOnlyAccounts.length <= 1 || !nodeInfo) {
if (viewOnlyAccounts.length <= 1 || !multiNodeInfo) {
return null;
}

View file

@ -1,5 +1,5 @@
import React from 'react';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState } from '../../context/ConfigContext';
import { getValue } from '../../utils/helpers';
import { usePriceState } from '../../context/PriceContext';
@ -16,8 +16,12 @@ export const Price = ({
amount: number | string;
breakNumber?: boolean;
}) => {
const { currency } = useSettings();
const { prices, loading, error } = usePriceState();
const { currency, displayValues } = useConfigState();
const { prices, dontShow } = usePriceState();
if (!displayValues) {
return <>-</>;
}
let priceProps: PriceProps = {
price: 0,
@ -25,7 +29,7 @@ export const Price = ({
currency: currency !== 'btc' && currency !== 'sat' ? 'sat' : currency,
};
if (prices && !loading && !error) {
if (prices && !dontShow) {
const current: { last: number; symbol: string } = prices[currency] ?? {
last: 0,
symbol: '',
@ -48,13 +52,17 @@ interface GetPriceProps {
export const getPrice = (
currency: string,
displayValues: boolean,
priceContext: {
error: boolean;
loading: boolean;
dontShow: boolean;
prices?: { [key: string]: { last: number; symbol: string } };
}
) => ({ amount, breakNumber = false }: GetPriceProps): string => {
const { prices, loading, error } = priceContext;
const { prices, dontShow } = priceContext;
if (!displayValues) {
return '-';
}
let priceProps: PriceProps = {
price: 0,
@ -62,7 +70,7 @@ export const getPrice = (
currency: currency !== 'btc' && currency !== 'sat' ? 'sat' : currency,
};
if (prices && !loading && !error) {
if (prices && !dontShow) {
const current: { last: number; symbol: string } = prices[currency] ?? {
last: 0,
symbol: '',

View file

@ -1,16 +1,21 @@
import React, { createContext, useContext, useReducer } from 'react';
type State = {
loading: boolean;
error: boolean;
dontShow: boolean;
fast: number;
halfHour: number;
hour: number;
};
type ChangeState = {
fast: number;
halfHour: number;
hour: number;
};
type ActionType = {
type: 'fetched' | 'error';
state?: State;
type: 'fetched' | 'dontShow';
state?: ChangeState;
};
type Dispatch = (action: ActionType) => void;
@ -19,8 +24,7 @@ export const StateContext = createContext<State | undefined>(undefined);
export const DispatchContext = createContext<Dispatch | undefined>(undefined);
const initialState = {
loading: true,
error: false,
dontShow: true,
fast: 0,
halfHour: 0,
hour: 0,
@ -28,16 +32,12 @@ const initialState = {
const stateReducer = (state: State, action: ActionType): State => {
switch (action.type) {
case 'dontShow':
return { ...initialState, dontShow: true };
case 'fetched':
return action.state || initialState;
case 'error':
return {
...initialState,
loading: false,
error: true,
};
return { ...initialState, ...action.state, dontShow: false };
default:
return initialState;
return state;
}
};

View file

@ -28,9 +28,6 @@ type State = {
sentChats: SentChatProps[];
lastChat: string;
sender: string;
hideFee: boolean;
hideNonVerified: boolean;
maxFee: number;
};
type ActionType = {
@ -39,9 +36,6 @@ type ActionType = {
| 'additional'
| 'changeActive'
| 'newChat'
| 'hideNonVerified'
| 'hideFee'
| 'changeFee'
| 'disconnected';
chats?: ChatProps[];
sentChats?: SentChatProps[];
@ -49,9 +43,6 @@ type ActionType = {
lastChat?: string;
sender?: string;
userId?: string;
hideFee?: boolean;
hideNonVerified?: boolean;
maxFee?: number;
};
type Dispatch = (action: ActionType) => void;
@ -59,15 +50,12 @@ type Dispatch = (action: ActionType) => void;
const StateContext = createContext<State | undefined>(undefined);
const DispatchContext = createContext<Dispatch | undefined>(undefined);
const initialState = {
const initialState: State = {
initialized: false,
chats: [],
lastChat: '',
sender: '',
sentChats: [],
hideFee: false,
hideNonVerified: false,
maxFee: 20,
};
const stateReducer = (state: State, action: ActionType): State => {
@ -100,29 +88,8 @@ const stateReducer = (state: State, action: ActionType): State => {
sentChats: [...state.sentChats, action.newChat],
...(action.sender && { sender: action.sender }),
};
case 'hideFee':
localStorage.setItem('hideFee', JSON.stringify(action.hideFee));
return {
...state,
hideFee: action.hideFee,
};
case 'hideNonVerified':
localStorage.setItem(
'hideNonVerified',
JSON.stringify(action.hideNonVerified)
);
return {
...state,
hideNonVerified: action.hideNonVerified,
};
case 'changeFee':
localStorage.setItem('maxChatFee', JSON.stringify(action.maxFee));
return {
...state,
maxFee: action.maxFee,
};
default:
return initialState;
return state;
}
};

View file

@ -0,0 +1,117 @@
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import getConfig from 'next/config';
import Cookies from 'js-cookie';
const themeTypes = ['dark', 'light'];
const currencyTypes = ['sat', 'btc', 'EUR', 'USD'];
type State = {
currency: string;
theme: string;
sidebar: boolean;
multiNodeInfo: boolean;
fetchFees: boolean;
fetchPrices: boolean;
displayValues: boolean;
hideFee: boolean;
hideNonVerified: boolean;
maxFee: number;
chatPollingSpeed: number;
};
type ConfigInitProps = {
initialConfig: State;
};
type ActionType = {
type: 'change';
currency?: string;
theme?: string;
sidebar?: boolean;
multiNodeInfo?: boolean;
fetchFees?: boolean;
fetchPrices?: boolean;
displayValues?: boolean;
hideFee?: boolean;
hideNonVerified?: boolean;
maxFee?: number;
chatPollingSpeed?: number;
};
type Dispatch = (action: ActionType) => void;
const StateContext = createContext<State | undefined>(undefined);
const DispatchContext = createContext<Dispatch | undefined>(undefined);
const { publicRuntimeConfig } = getConfig();
const {
defaultTheme: defT,
defaultCurrency: defC,
fetchPrices,
fetchFees,
} = publicRuntimeConfig;
const initialState: State = {
currency: currencyTypes.indexOf(defC) > -1 ? defC : 'sat',
theme: themeTypes.indexOf(defT) > -1 ? defT : 'dark',
sidebar: true,
multiNodeInfo: false,
fetchFees,
fetchPrices,
displayValues: true,
hideFee: false,
hideNonVerified: false,
maxFee: 20,
chatPollingSpeed: 1000,
};
const stateReducer = (state: State, action: ActionType): State => {
const { type, ...settings } = action;
switch (type) {
case 'change':
return {
...state,
...settings,
};
default:
return state;
}
};
const ConfigProvider: React.FC<ConfigInitProps> = ({
children,
initialConfig,
}) => {
const [state, dispatch] = useReducer(stateReducer, {
...initialState,
...initialConfig,
});
useEffect(() => {
Cookies.set('config', state, { expires: 365, sameSite: 'strict' });
}, [state]);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
const useConfigState = () => {
const context = useContext(StateContext);
if (context === undefined) {
throw new Error('useConfigState must be used within a ConfigProvider');
}
return context;
};
const useConfigDispatch = () => {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error('useConfigDispatch must be used within a ConfigProvider');
}
return context;
};
export { ConfigProvider, useConfigState, useConfigDispatch };

View file

@ -1,6 +1,5 @@
import React from 'react';
import { AccountProvider } from './AccountContext';
import { SettingsProvider } from './SettingsContext';
import { BitcoinInfoProvider } from './BitcoinContext';
import { StatusProvider } from './StatusContext';
import { PriceProvider } from './PriceContext';
@ -8,14 +7,12 @@ import { ChatProvider } from './ChatContext';
export const ContextProvider: React.FC = ({ children }) => (
<AccountProvider>
<SettingsProvider>
<BitcoinInfoProvider>
<PriceProvider>
<ChatProvider>
<StatusProvider>{children}</StatusProvider>
</ChatProvider>
</PriceProvider>
</BitcoinInfoProvider>
</SettingsProvider>
<BitcoinInfoProvider>
<PriceProvider>
<ChatProvider>
<StatusProvider>{children}</StatusProvider>
</ChatProvider>
</PriceProvider>
</BitcoinInfoProvider>
</AccountProvider>
);

View file

@ -6,14 +6,17 @@ type PriceProps = {
};
type State = {
loading: boolean;
error: boolean;
dontShow: boolean;
prices?: { [key: string]: PriceProps };
};
type ChangeState = {
prices?: { [key: string]: PriceProps };
};
type ActionType = {
type: 'fetched' | 'error';
state?: State;
type: 'fetched' | 'dontShow';
state?: ChangeState;
};
type Dispatch = (action: ActionType) => void;
@ -22,23 +25,18 @@ export const StateContext = createContext<State | undefined>(undefined);
export const DispatchContext = createContext<Dispatch | undefined>(undefined);
const initialState: State = {
loading: true,
error: false,
dontShow: true,
prices: { EUR: { last: 0, symbol: '€' } },
};
const stateReducer = (state: State, action: ActionType): State => {
switch (action.type) {
case 'dontShow':
return { ...initialState, dontShow: true };
case 'fetched':
return action.state || initialState;
case 'error':
return {
...initialState,
loading: false,
error: true,
};
return { ...initialState, ...action.state, dontShow: false };
default:
return initialState;
return state;
}
};

View file

@ -1,93 +0,0 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import React, { createContext, useState, useContext, useEffect } from 'react';
import merge from 'lodash.merge';
interface ChangeProps {
theme?: string;
sidebar?: boolean;
currency?: string;
nodeInfo?: boolean;
}
interface SettingsProps {
currency: string;
theme: string;
sidebar: boolean;
nodeInfo: boolean;
setSettings: (newProps: ChangeProps) => void;
refreshSettings: () => void;
}
export const SettingsContext = createContext<SettingsProps>({
currency: '',
theme: '',
sidebar: true,
nodeInfo: false,
setSettings: () => ({}),
refreshSettings: () => ({}),
});
const SettingsProvider = ({ children }: any) => {
// const savedTheme = localStorage.getItem('theme') || 'light';
// const savedSidebar = localStorage.getItem('sidebar') === 'false' ? false : true;
// const savedCurrency = localStorage.getItem('currency') || 'sat';
// const savedNodeInfo = localStorage.getItem('nodeInfo') === 'true' ? true : false;
useEffect(() => {
refreshSettings();
}, []);
const refreshSettings = (account?: string) => {
const savedTheme = localStorage.getItem('theme') || 'light';
const savedSidebar =
localStorage.getItem('sidebar') === 'false' ? false : true;
const savedCurrency = localStorage.getItem('currency') || 'sat';
const savedNodeInfo =
localStorage.getItem('nodeInfo') === 'true' ? true : false;
updateSettings((prevState: any) => {
const newState = { ...prevState };
return merge(newState, {
currency: savedCurrency,
theme: savedTheme,
sidebar: savedSidebar,
nodeInfo: savedNodeInfo,
});
});
};
const setSettings = ({ currency, theme, sidebar }: ChangeProps) => {
updateSettings((prevState: any) => {
const newState = { ...prevState };
return merge(newState, {
currency,
theme,
sidebar,
});
});
};
const settingsState = {
prices: { EUR: { last: 0, symbol: '€' } },
price: 0,
symbol: '',
currency: 'sat',
theme: 'dark',
sidebar: true,
nodeInfo: false,
setSettings,
refreshSettings,
};
const [settings, updateSettings] = useState(settingsState);
return (
<SettingsContext.Provider value={settings}>
{children}
</SettingsContext.Provider>
);
};
const useSettings = () => useContext(SettingsContext);
export { SettingsProvider, useSettings };

View file

@ -24,7 +24,7 @@ import {
CreditCard,
MessageCircle,
} from 'react-feather';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState } from '../../context/ConfigContext';
import { useRouter } from 'next/router';
import { Link } from '../../components/link/Link';
import { useStatusState } from '../../context/StatusContext';
@ -131,7 +131,7 @@ interface NavigationProps {
export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
const { pathname } = useRouter();
const { sidebar, setSettings } = useSettings();
const { sidebar } = useConfigState();
const { connected } = useStatusState();
const renderNavButton = (
@ -204,7 +204,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
<LinkView>
{connected && <NodeInfo isOpen={sidebar} />}
{renderLinks()}
<SideSettings isOpen={sidebar} setIsOpen={setSettings} />
<SideSettings />
</LinkView>
</StickyCard>
</NavigationStyle>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { useSettings } from '../../../context/SettingsContext';
import { useConfigState } from '../../../context/ConfigContext';
import {
Separation,
SingleLine,
@ -84,9 +84,9 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => {
onError: error => toast.error(getErrorContent(error)),
});
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const tooltipType: any = getTooltipType(theme);

View file

@ -1,6 +1,9 @@
import React from 'react';
import { Separation, SingleLine } from '../../../components/generic/Styled';
import { useSettings } from '../../../context/SettingsContext';
import {
useConfigState,
useConfigDispatch,
} from '../../../context/ConfigContext';
import { Sun, Moon, ChevronLeft, ChevronRight } from 'react-feather';
import styled from 'styled-components';
import {
@ -9,6 +12,7 @@ import {
inverseTextColor,
unSelectedNavButton,
} from '../../../styles/Themes';
import { usePriceState } from '../../../context/PriceContext';
const SelectedIcon = styled.div<{ selected: boolean }>`
display: flex;
@ -48,13 +52,20 @@ const BurgerPadding = styled(SingleLine)`
`;
const currencyArray = ['sat', 'btc', 'EUR', 'USD'];
const currencyNoFiatArray = ['sat', 'btc'];
const themeArray = ['light', 'dark'];
const currencyMap: { [key: string]: string } = {
sat: 'S',
btc: '₿',
EUR: '€',
USD: '$',
};
const currencyNoFiatMap: { [key: string]: string } = {
sat: 'S',
btc: '₿',
};
const getNextValue = (array: string[], current: string): string => {
const length = array.length;
@ -71,17 +82,16 @@ const getNextValue = (array: string[], current: string): string => {
};
interface SideSettingsProps {
isOpen?: boolean;
isBurger?: boolean;
setIsOpen?: (state: any) => void;
}
export const SideSettings = ({
isOpen,
isBurger,
setIsOpen,
}: SideSettingsProps) => {
const { theme, currency, setSettings } = useSettings();
export const SideSettings = ({ isBurger }: SideSettingsProps) => {
const { dontShow } = usePriceState();
const { theme, currency, sidebar } = useConfigState();
const dispatch = useConfigDispatch();
const correctMap = dontShow ? currencyNoFiatMap : currencyMap;
const correctArray = dontShow ? currencyNoFiatArray : currencyArray;
const renderIcon = (
type: string,
@ -97,11 +107,12 @@ export const SideSettings = ({
onClick={() => {
localStorage.setItem(type, value);
type === 'currency' &&
setSettings({
dispatch({
type: 'change',
currency:
isOpen || isBurger ? value : getNextValue(currencyArray, value),
sidebar || isBurger ? value : getNextValue(correctArray, value),
});
type === 'theme' && setSettings({ theme: value });
type === 'theme' && dispatch({ type: 'change', theme: value });
}}
>
{type === 'currency' && <Symbol>{text}</Symbol>}
@ -110,12 +121,12 @@ export const SideSettings = ({
);
const renderContent = () => {
if (!isOpen) {
if (!sidebar) {
return (
<>
<Separation lineColor={unSelectedNavButton} />
<IconRow center={true}>
{renderIcon('currency', currency, currencyMap[currency], true)}
{renderIcon('currency', currency, correctMap[currency], true)}
</IconRow>
<IconRow center={true}>
{renderIcon(
@ -135,8 +146,8 @@ export const SideSettings = ({
<IconRow>
{renderIcon('currency', 'sat', 'S')}
{renderIcon('currency', 'btc', '₿')}
{renderIcon('currency', 'EUR', '€')}
{renderIcon('currency', 'USD', '$')}
{!dontShow && renderIcon('currency', 'EUR', '€')}
{!dontShow && renderIcon('currency', 'USD', '$')}
</IconRow>
<IconRow>
{renderIcon('theme', 'light', '', false, Sun)}
@ -152,8 +163,8 @@ export const SideSettings = ({
<IconRow>
{renderIcon('currency', 'sat', 'S')}
{renderIcon('currency', 'btc', '₿')}
{renderIcon('currency', 'EUR', '€')}
{renderIcon('currency', 'USD', '$')}
{!dontShow && renderIcon('currency', 'EUR', '€')}
{!dontShow && renderIcon('currency', 'USD', '$')}
</IconRow>
<IconRow>
{renderIcon('theme', 'light', '', false, Sun)}
@ -166,19 +177,17 @@ export const SideSettings = ({
return (
<>
{renderContent()}
{setIsOpen && (
<IconRow center={!isOpen}>
<SelectedIcon
selected={true}
onClick={() => {
localStorage.setItem('sidebar', (!isOpen).toString());
setIsOpen({ sidebar: !isOpen });
}}
>
{isOpen ? <ChevronLeft size={18} /> : <ChevronRight size={18} />}
</SelectedIcon>
</IconRow>
)}
<IconRow center={!sidebar}>
<SelectedIcon
selected={true}
onClick={() => {
localStorage.setItem('sidebar', (!sidebar).toString());
dispatch({ type: 'change', sidebar: !sidebar });
}}
>
{sidebar ? <ChevronLeft size={18} /> : <ChevronRight size={18} />}
</SelectedIcon>
</IconRow>
</>
);
};

5
src/utils/cookies.ts Normal file
View file

@ -0,0 +1,5 @@
import cookie from 'cookie';
export const parseCookies = req => {
return cookie.parse(req ? req.headers.cookie || '' : document.cookie);
};

View file

@ -14,7 +14,7 @@ import {
} from '../../../components/generic/helpers';
import styled from 'styled-components';
import { getPrice } from '../../../components/price/Price';
import { useSettings } from '../../../context/SettingsContext';
import { useConfigState } from '../../../context/ConfigContext';
import { usePriceState } from '../../../context/PriceContext';
const AddMargin = styled.div`
@ -34,9 +34,9 @@ export const TransactionsCard = ({
setIndexOpen,
indexOpen,
}: TransactionsCardProps) => {
const { currency } = useSettings();
const { currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const {
block_id,

View file

@ -3,7 +3,7 @@ import { Separation, SubCard } from '../../../components/generic/Styled';
import { MainInfo } from '../../../components/generic/CardGeneric';
import { renderLine } from '../../../components/generic/helpers';
import { getPrice } from '../../../components/price/Price';
import { useSettings } from '../../../context/SettingsContext';
import { useConfigState } from '../../../context/ConfigContext';
import { usePriceState } from '../../../context/PriceContext';
interface TransactionsCardProps {
@ -19,9 +19,9 @@ export const UtxoCard = ({
setIndexOpen,
indexOpen,
}: TransactionsCardProps) => {
const { currency } = useSettings();
const { currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const {
address,

View file

@ -17,7 +17,7 @@ import {
ResponsiveSingle,
ResponsiveCol,
} from '../../../components/generic/Styled';
import { useSettings } from '../../../context/SettingsContext';
import { useConfigState } from '../../../context/ConfigContext';
import {
getStatusDot,
getTooltipType,
@ -64,9 +64,9 @@ export const ChannelCard = ({
}: ChannelCardProps) => {
const [modalOpen, setModalOpen] = useState(false);
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const tooltipType: any = getTooltipType(theme);

View file

@ -16,7 +16,7 @@ import {
ResponsiveSingle,
ResponsiveCol,
} from '../../../components/generic/Styled';
import { useSettings } from '../../../context/SettingsContext';
import { useConfigState } from '../../../context/ConfigContext';
import {
getStatusDot,
getTooltipType,
@ -66,9 +66,9 @@ export const PendingCard = ({
setIndexOpen,
indexOpen,
}: PendingCardProps) => {
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const tooltipType: any = getTooltipType(theme);

View file

@ -19,7 +19,7 @@ import {
ChatBoxTopAlias,
} from './Chat.styled';
import { ChatBubble } from './ChatBubble';
import { useChatState } from '../../context/ChatContext';
import { useConfigState } from '../../context/ConfigContext';
export const MessageCard = ({
message,
@ -28,7 +28,7 @@ export const MessageCard = ({
message: MessageType;
key?: string;
}) => {
const { hideFee, hideNonVerified } = useChatState();
const { hideFee, hideNonVerified } = useConfigState();
if (!message.message && message.contentType === 'text') {
return null;
}

View file

@ -20,7 +20,7 @@ import { useChatState, useChatDispatch } from '../../context/ChatContext';
import { useAccount } from '../../context/AccountContext';
import { Circle } from 'react-feather';
import ScaleLoader from 'react-spinners/ScaleLoader';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState } from '../../context/ConfigContext';
import { usePriceState } from '../../context/PriceContext';
import { getPrice } from '../../components/price/Price';
@ -29,7 +29,8 @@ interface SendButtonProps {
}
const SendButton = ({ amount }: SendButtonProps) => {
const { sender, maxFee } = useChatState();
const { maxFee } = useConfigState();
const { sender } = useChatState();
const dispatch = useChatDispatch();
const { id } = useAccount();
@ -54,7 +55,7 @@ const SendButton = ({ amount }: SendButtonProps) => {
sender,
});
}
}, [loading, data]);
}, [loading, data, amount, dispatch, sender, id]);
return (
<SecureWrapper
@ -80,9 +81,9 @@ interface ChatBubbleProps {
}
export const ChatBubble = ({ message }: ChatBubbleProps) => {
const { currency } = useSettings();
const { currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const {
contentType,

View file

@ -8,6 +8,7 @@ import { toast } from 'react-toastify';
import { getErrorContent } from '../../utils/error';
import { useAccount } from '../../context/AccountContext';
import { handleMessage } from './helpers/chatHelpers';
import { useConfigState } from '../../context/ConfigContext';
export const ChatInput = ({
alias,
@ -21,7 +22,8 @@ export const ChatInput = ({
const [message, setMessage] = React.useState('');
const { id } = useAccount();
const { sender, maxFee } = useChatState();
const { maxFee } = useConfigState();
const { sender } = useChatState();
const dispatch = useChatDispatch();
const [sendMessage, { loading, data }] = useSendMessageMutation({
@ -50,7 +52,17 @@ export const ChatInput = ({
sender: customSender || sender,
});
}
}, [loading, data]);
}, [
loading,
data,
formattedMessage,
customSender,
sender,
contentType,
tokens,
id,
dispatch,
]);
return (
<SingleLine>

View file

@ -17,7 +17,7 @@ import { toast } from 'react-toastify';
import { getErrorContent } from '../../utils/error';
import { ChevronRight } from 'react-feather';
import { SecureButton } from '../../components/buttons/secureButton/SecureButton';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState } from '../../context/ConfigContext';
import { textColorMap } from '../../styles/Themes';
import { Input } from '../../components/input/Input';
import { AdminSwitch } from '../../components/adminSwitch/AdminSwitch';
@ -39,7 +39,7 @@ export const FeeCard = ({
const [newBaseFee, setBaseFee] = useState(0);
const [newFeeRate, setFeeRate] = useState(0);
const { theme } = useSettings();
const { theme } = useConfigState();
const {
alias,
@ -59,7 +59,7 @@ export const FeeCard = ({
? toast.success('Channel fees updated')
: toast.error('Error updating channel fees');
},
refetchQueries: ['GetChannelFees'],
refetchQueries: ['ChannelFees'],
});
const handleClick = () => {

View file

@ -38,6 +38,7 @@ export const PayCard = ({ setOpen }: { setOpen: () => void }) => {
setModalType('none');
setOpen();
},
refetchQueries: ['GetInOut', 'GetNodeInfo', 'GetBalances'],
});
const handleClick = () => {

View file

@ -19,7 +19,7 @@ import {
} from '../../../../components/buttons/multiButton/MultiButton';
import { Price, getPrice } from '../../../../components/price/Price';
import { mediaWidths } from '../../../../styles/Themes';
import { useSettings } from '../../../../context/SettingsContext';
import { useConfigState } from '../../../../context/ConfigContext';
import Modal from '../../../../components/modal/ReactModal';
import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton';
import { renderLine } from '../../../../components/generic/helpers';
@ -43,22 +43,21 @@ const Margin = styled.div`
`;
export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
const { currency } = useSettings();
const { fast, halfHour, hour, dontShow } = useBitcoinState();
const { currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const [modalOpen, setModalOpen] = useState(false);
const [address, setAddress] = useState('');
const [tokens, setTokens] = useState(0);
const [type, setType] = useState('none');
const [type, setType] = useState(dontShow ? 'fee' : 'none');
const [amount, setAmount] = useState(0);
const [sendAll, setSendAll] = useState(false);
const canSend = address !== '' && (sendAll || tokens > 0) && amount > 0;
const { fast, halfHour, hour } = useBitcoinState();
const [payAddress, { loading }] = usePayAddressMutation({
onError: error => toast.error(getErrorContent(error)),
onCompleted: () => {
@ -143,14 +142,15 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
<SingleLine>
<NoWrapTitle>Fee:</NoWrapTitle>
<MultiButton margin={'8px 0 8px 16px'}>
{renderButton(
() => {
setType('none');
setAmount(fast);
},
'Auto',
type === 'none'
)}
{!dontShow &&
renderButton(
() => {
setType('none');
setAmount(fast);
},
'Auto',
type === 'none'
)}
{renderButton(
() => {
setType('fee');

View file

@ -34,13 +34,12 @@ interface OpenChannelProps {
}
export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
const { fast, halfHour, hour, dontShow } = useBitcoinState();
const [size, setSize] = useState(0);
const [fee, setFee] = useState(0);
const [publicKey, setPublicKey] = useState('');
const [privateChannel, setPrivateChannel] = useState(false);
const [type, setType] = useState('none');
const { fast, halfHour, hour } = useBitcoinState();
const [type, setType] = useState(dontShow ? 'fee' : 'none');
const [openChannel] = useOpenChannelMutation({
onError: error => toast.error(getErrorContent(error)),
@ -108,24 +107,26 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
</MultiButton>
</SingleLine>
<Separation />
<SingleLine>
<NoWrapTitle>Fee:</NoWrapTitle>
<MultiButton margin={'8px 0 8px 16px'}>
{renderButton(
() => {
setType('none');
setFee(fast);
},
'Auto',
type === 'none'
)}
{renderButton(
() => setType('fee'),
'Fee (Sats/Byte)',
type === 'fee'
)}
</MultiButton>
</SingleLine>
{!dontShow && (
<SingleLine>
<NoWrapTitle>Fee:</NoWrapTitle>
<MultiButton margin={'8px 0 8px 16px'}>
{renderButton(
() => {
setType('none');
setFee(fast);
},
'Auto',
type === 'none'
)}
{renderButton(
() => setType('fee'),
'Fee (Sats/Byte)',
type === 'fee'
)}
</MultiButton>
</SingleLine>
)}
<SingleLine>
<ResponsiveWrap>
<NoWrapTitle>Fee Amount:</NoWrapTitle>
@ -141,7 +142,6 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
type={'number'}
onChange={e => setFee(Number(e.target.value))}
/>
// </MultiButton>
)}
{type === 'none' && (
<MultiButton margin={'8px 0 8px 16px'}>

View file

@ -1,6 +1,6 @@
import React from 'react';
import { DarkSubTitle } from '../../../../components/generic/Styled';
import { useSettings } from '../../../../context/SettingsContext';
import { useConfigState } from '../../../../context/ConfigContext';
import { VictoryPie } from 'victory';
import { chartAxisColor } from '../../../../styles/Themes';
import { Row, Col, PieRow } from '.';
@ -13,9 +13,9 @@ interface Props {
}
export const FlowPie = ({ flowPie, isType }: Props) => {
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
return (
<Row>

View file

@ -1,6 +1,6 @@
import React from 'react';
import numeral from 'numeral';
import { useSettings } from '../../../../context/SettingsContext';
import { useConfigState } from '../../../../context/ConfigContext';
import {
VictoryBar,
VictoryChart,
@ -41,9 +41,9 @@ export const FlowReport = ({
parsedData2,
}: // waterfall,
Props) => {
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
let domain = 24;
let barWidth = 3;

View file

@ -1,6 +1,6 @@
import React from 'react';
import { DarkSubTitle } from '../../../../components/generic/Styled';
import { useSettings } from '../../../../context/SettingsContext';
import { useConfigState } from '../../../../context/ConfigContext';
import { VictoryPie } from 'victory';
import { chartAxisColor } from '../../../../styles/Themes';
import { Row, Col, PieRow } from '.';
@ -10,7 +10,7 @@ interface Props {
}
export const InvoicePie = ({ invoicePie }: Props) => {
const { theme } = useSettings();
const { theme } = useConfigState();
return (
<Row>

View file

@ -12,7 +12,7 @@ import { GitCommit, ArrowDown, ArrowUp } from 'react-feather';
import styled from 'styled-components';
import { LoadingCard } from '../../../../components/loading/LoadingCard';
import { getPrice } from '../../../../components/price/Price';
import { useSettings } from '../../../../context/SettingsContext';
import { useConfigState } from '../../../../context/ConfigContext';
import { usePriceState } from '../../../../context/PriceContext';
import { useGetForwardChannelsReportQuery } from '../../../../generated/graphql';
@ -64,9 +64,9 @@ interface Props {
export const ForwardChannelsReport = ({ isTime, isType, color }: Props) => {
const [type, setType] = useState('route');
const { currency } = useSettings();
const { currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const { auth } = useAccount();

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Sub4Title } from '../../../../components/generic/Styled';
import numeral from 'numeral';
import { useSettings } from '../../../../context/SettingsContext';
import { useConfigState } from '../../../../context/ConfigContext';
import { useAccount } from '../../../../context/AccountContext';
import {
VictoryBar,
@ -34,9 +34,9 @@ const timeMap: { [key: string]: string } = {
};
export const ForwardReport = ({ isTime, isType }: Props) => {
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const { auth } = useAccount();

View file

@ -12,7 +12,7 @@ import {
VictoryVoronoiContainer,
VictoryTooltip,
} from 'victory';
import { useSettings } from '../../../../context/SettingsContext';
import { useConfigState } from '../../../../context/ConfigContext';
import {
chartGridColor,
chartAxisColor,
@ -26,9 +26,9 @@ import { useGetLiquidReportQuery } from '../../../../generated/graphql';
export const LiquidReport = () => {
const { auth } = useAccount();
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const { data, loading } = useGetLiquidReportQuery({
skip: !auth,

View file

@ -24,7 +24,7 @@ import {
MainInfo,
} from '../../components/generic/CardGeneric';
import { getPercent } from '../../utils/helpers';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState } from '../../context/ConfigContext';
import ReactTooltip from 'react-tooltip';
import { usePriceState } from '../../context/PriceContext';
import { getPrice } from '../../components/price/Price';
@ -57,10 +57,10 @@ export const PeersCard = ({
}: PeerProps) => {
const [modalOpen, setModalOpen] = useState(false);
const { theme, currency } = useSettings();
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, priceContext);
const format = getPrice(currency, displayValues, priceContext);
const tooltipType: any = getTooltipType(theme);
const {

View file

@ -11,11 +11,16 @@ import {
MultiButton,
SingleButton,
} from '../../components/buttons/multiButton/MultiButton';
import { useChatState, useChatDispatch } from '../../context/ChatContext';
import { useConfigState, useConfigDispatch } from '../../context/ConfigContext';
export const ChatSettings = () => {
const { hideFee, hideNonVerified, maxFee } = useChatState();
const dispatch = useChatDispatch();
const {
hideFee,
hideNonVerified,
maxFee,
chatPollingSpeed: cps,
} = useConfigState();
const dispatch = useConfigDispatch();
const renderButton = (
title: string,
@ -29,15 +34,19 @@ export const ChatSettings = () => {
switch (type) {
case 'fee':
typeof value === 'boolean' &&
dispatch({ type: 'hideFee', hideFee: value });
dispatch({ type: 'change', hideFee: value });
break;
case 'nonverified':
typeof value === 'boolean' &&
dispatch({ type: 'hideNonVerified', hideNonVerified: value });
dispatch({ type: 'change', hideNonVerified: value });
break;
case 'pollingSpeed':
typeof value === 'number' &&
dispatch({ type: 'change', chatPollingSpeed: value });
break;
default:
typeof value === 'number' &&
dispatch({ type: 'changeFee', maxFee: value });
dispatch({ type: 'change', maxFee: value });
break;
}
}}
@ -74,6 +83,18 @@ export const ChatSettings = () => {
{renderButton('100', 'maxFee', maxFee === 100, 100)}
</MultiButton>
</SettingsLine>
<SettingsLine>
<Sub4Title>{'Polling Speed:'}</Sub4Title>
<MultiButton>
{renderButton('1s', 'pollingSpeed', cps === 1000, 1000)}
{renderButton('5s', 'pollingSpeed', cps === 5000, 5000)}
{renderButton('10s', 'pollingSpeed', cps === 10000, 10000)}
{renderButton('1m', 'pollingSpeed', cps === 60000, 60000)}
{renderButton('10m', 'pollingSpeed', cps === 600000, 600000)}
{renderButton('30m', 'pollingSpeed', cps === 1800000, 1800000)}
{renderButton('None', 'pollingSpeed', cps === 0, 0)}
</MultiButton>
</SettingsLine>
</Card>
</CardWithTitle>
);

View file

@ -21,6 +21,7 @@ import { useStatusDispatch } from '../../context/StatusContext';
import { useRouter } from 'next/router';
import { appendBasePath } from '../../utils/basePath';
import { useChatDispatch } from '../../context/ChatContext';
import Cookies from 'js-cookie';
export const ButtonRow = styled.div`
width: auto;
@ -90,6 +91,7 @@ export const DangerView = () => {
chatDispatch({ type: 'disconnected' });
deleteStorage();
refreshAccount();
Cookies.remove('config');
push(appendBasePath('/'));
};

View file

@ -6,22 +6,19 @@ import {
Sub4Title,
} from '../../components/generic/Styled';
import { SettingsLine } from '../../../pages/settings';
import { useSettings } from '../../context/SettingsContext';
import { useConfigState, useConfigDispatch } from '../../context/ConfigContext';
import {
MultiButton,
SingleButton,
} from '../../components/buttons/multiButton/MultiButton';
import { useAccount } from '../../context/AccountContext';
import { usePriceState } from '../../context/PriceContext';
export const InterfaceSettings = () => {
const {
theme,
currency,
nodeInfo,
setSettings,
refreshSettings,
} = useSettings();
const { dontShow } = usePriceState();
const { theme, currency, multiNodeInfo } = useConfigState();
const dispatch = useConfigDispatch();
const { accounts } = useAccount();
@ -37,11 +34,13 @@ export const InterfaceSettings = () => {
selected={current === value}
onClick={() => {
localStorage.setItem(type, value);
type === 'theme' && setSettings({ theme: value });
type === 'currency' && setSettings({ currency: value });
type === 'theme' && dispatch({ type: 'change', theme: value });
type === 'currency' && dispatch({ type: 'change', currency: value });
type === 'nodeInfo' &&
setSettings({ nodeInfo: value === 'true' ? true : false });
refreshSettings();
dispatch({
type: 'change',
multiNodeInfo: value === 'true' ? true : false,
});
}}
>
{title}
@ -63,18 +62,18 @@ export const InterfaceSettings = () => {
<SettingsLine>
<Sub4Title>Show all accounts on homepage:</Sub4Title>
<MultiButton>
{renderButton('Yes', 'true', 'nodeInfo', `${nodeInfo}`)}
{renderButton('No', 'false', 'nodeInfo', `${nodeInfo}`)}
{renderButton('Yes', 'true', 'nodeInfo', `${multiNodeInfo}`)}
{renderButton('No', 'false', 'nodeInfo', `${multiNodeInfo}`)}
</MultiButton>
</SettingsLine>
)}
<SettingsLine>
<Sub4Title>Currency:</Sub4Title>
<MultiButton margin={'0 0 0 16px'}>
{renderButton('Bitcoin', 'btc', 'currency', currency)}
{renderButton('Satoshis', 'sat', 'currency', currency)}
{renderButton('Euro', 'EUR', 'currency', currency)}
{renderButton('US Dollar', 'USD', 'currency', currency)}
{renderButton('Bitcoin', 'btc', 'currency', currency)}
{!dontShow && renderButton('Euro', 'EUR', 'currency', currency)}
{!dontShow && renderButton('USD', 'USD', 'currency', currency)}
</MultiButton>
</SettingsLine>
</Card>

View file

@ -0,0 +1,65 @@
import React from 'react';
import {
CardWithTitle,
SubTitle,
Card,
Sub4Title,
} from '../../components/generic/Styled';
import { SettingsLine } from '../../../pages/settings';
import { useConfigState, useConfigDispatch } from '../../context/ConfigContext';
import {
MultiButton,
SingleButton,
} from '../../components/buttons/multiButton/MultiButton';
export const PrivacySettings = () => {
const { fetchFees, fetchPrices, displayValues } = useConfigState();
const dispatch = useConfigDispatch();
const renderButton = (
title: string,
value: boolean,
type: string,
current: boolean
) => (
<SingleButton
selected={current === value}
onClick={() => {
localStorage.setItem(type, JSON.stringify(value));
dispatch({ type: 'change', [type]: value });
}}
>
{title}
</SingleButton>
);
return (
<CardWithTitle>
<SubTitle>Privacy</SubTitle>
<Card>
<SettingsLine>
<Sub4Title>Fetch Bitcoin Fees:</Sub4Title>
<MultiButton>
{renderButton('On', true, 'fetchFees', fetchFees)}
{renderButton('Off', false, 'fetchFees', fetchFees)}
</MultiButton>
</SettingsLine>
<SettingsLine>
<Sub4Title>Fetch Fiat Prices:</Sub4Title>
<MultiButton margin={'0 0 0 16px'}>
{renderButton('On', true, 'fetchPrices', fetchPrices)}
{renderButton('Off', false, 'fetchPrices', fetchPrices)}
</MultiButton>
</SettingsLine>
<SettingsLine>
<Sub4Title>Values:</Sub4Title>
<MultiButton margin={'0 0 0 16px'}>
{renderButton('Show', true, 'displayValues', displayValues)}
{renderButton('Hide', false, 'displayValues', displayValues)}
</MultiButton>
</SettingsLine>
</Card>
</CardWithTitle>
);
};

View file

@ -16,5 +16,5 @@
"downlevelIteration": true
},
"exclude": ["node_modules", ".next"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"]
}

View file

@ -6106,6 +6106,11 @@ cookie@0.4.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cookie@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@ -10327,6 +10332,11 @@ jest@^25.5.4:
import-local "^3.0.2"
jest-cli "^25.5.4"
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
js-levenshtein@^1.1.3:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"