feat: chat integration (#34)

* feat: chat wip

* chore: more chat stuff

* chore: fallback alias

* chore: more chat progress

* chore: chat continues

* chore: disconnect on account change

* chore: add start chat view

* fix: no messages fix

* chore: πŸ”§ more chat changes

* feat: ✨ move to react feather

* chore: πŸ”§ add fee paid

* style: 🎨 change fee paid style

* fix: πŸ› wrong sidesettings icon

* chore: πŸ”§ add signed verified messages

* style: 🎨 chat mobile styling

* style: 🎨 contacts button icon

* chore: πŸ”§ add message types

* style: 🎨 chat changes

* chore: πŸ”§ error handling and styling

* chore: πŸ”§ chat settings

* chore: πŸ”§ contact last message

* chore: πŸ”§ small alias styling

* chore: πŸ”§ improve error handling

* chore: πŸ”§ juggernaut compatible

* chore: πŸ”§ multi currency

* chore: πŸ”§ add maxfee setting

* chore: πŸ”§ small fixes

* chore: πŸ”§ docker multistage
This commit is contained in:
Anthony Potdevin 2020-05-01 14:08:30 +02:00 β€’ committed by GitHub
parent b8ea1597a0
commit 24c5109a06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
144 changed files with 4218 additions and 447 deletions

View file

@ -10,6 +10,4 @@
.env
.vscode
.storybook
CHANGELOG.md
.elasticbeanstalk/*
.ebextensions/*
CHANGELOG.md

8
.gitignore vendored
View file

@ -28,10 +28,4 @@ yarn-error.log*
.env.local
.env.development.local
.env.test.local
.env.production.local
# Elastic Beanstalk Files
.elasticbeanstalk/*
.ebextensions/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
.env.production.local

View file

@ -1,16 +1,32 @@
FROM node:12-alpine
# ---------------
# Install Dependencies
# ---------------
FROM node:12-alpine as build
# Create app directory
WORKDIR /usr/src/app
# Install dependencies neccesary for node-gyp on node alpine
RUN apk add --update --no-cache \
python \
make \
g++
# Install app dependencies
COPY package.json /usr/src/app/
COPY yarn.lock /usr/src/app/
RUN yarn --network-timeout 300000 --production=true
RUN yarn add cross-env
COPY package.json .
COPY yarn.lock .
RUN yarn install --silent --production=true
RUN yarn add --dev cross-env
# ---------------
# Build App
# ---------------
FROM node:12-alpine
# Copy dependencies from build stage
COPY --from=build node_modules node_modules
# Bundle app source
COPY . /usr/src/app
COPY . .
RUN yarn tslint
RUN yarn test
RUN yarn build
EXPOSE 3000

19
jest.config.js Normal file
View file

@ -0,0 +1,19 @@
module.exports = {
collectCoverageFrom: [
'**/*.{js,jsx,ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**',
],
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
},
transformIgnorePatterns: [
'/node_modules/',
'^.+\\.module\\.(css|sass|scss)$',
],
moduleNameMapper: {
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
},
};

View file

@ -14,7 +14,11 @@
"release:test": "standard-version --dry-run",
"analyze": "cross-env ANALYZE=true next build",
"storybook": "start-storybook -p 6006 -c .storybook",
"codegen": "graphql-codegen --config codegen.yml"
"generate": "graphql-codegen --config codegen.yml",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"build:image": "docker build --no-cache -t thunderhub/test ."
},
"keywords": [],
"author": "",
@ -35,6 +39,7 @@
"isomorphic-unfetch": "^3.0.0",
"ln-service": "^47.16.0",
"lodash.debounce": "^4.0.8",
"lodash.groupby": "^4.6.0",
"lodash.merge": "^4.6.2",
"micro-cors": "^0.1.1",
"next": "^9.3.5",
@ -44,6 +49,7 @@
"react": "^16.13.1",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "^16.13.1",
"react-feather": "^2.0.8",
"react-intersection-observer": "^8.26.1",
"react-qr-reader": "^2.2.1",
"react-spinners": "^0.8.1",
@ -74,14 +80,19 @@
"@storybook/addon-knobs": "^5.3.18",
"@storybook/addon-viewport": "^5.3.18",
"@storybook/react": "^5.3.18",
"@testing-library/jest-dom": "^5.5.0",
"@testing-library/react": "^10.0.3",
"@types/node": "^13.11.1",
"@types/react": "^16.9.34",
"babel-jest": "^25.5.1",
"babel-loader": "^8.1.0",
"babel-plugin-inline-react-svg": "^1.1.1",
"babel-plugin-styled-components": "^1.10.7",
"babel-preset-react-app": "^9.1.2",
"cross-env": "^7.0.2",
"devmoji": "^2.1.9",
"husky": "^4.2.5",
"jest": "^25.5.1",
"lint-staged": "^10.1.3",
"prettier": "^2.0.4",
"standard-version": "^7.1.0",
@ -93,12 +104,14 @@
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"prepare-commit-msg": "devmoji -e",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.+(ts|tsx)": [
"prettier --write",
"jest --bail --findRelatedTests",
"tslint"
]
}

View file

@ -18,6 +18,8 @@ import 'react-toastify/dist/ReactToastify.css';
import Head from 'next/head';
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';
toast.configure({ draggable: false, pauseOnFocusLoss: false });
@ -39,6 +41,8 @@ const Wrapper: React.FC = ({ children }) => {
<>
<BitcoinPrice />
<BitcoinFees />
<ChatFetcher />
<ChatInit />
</>
);

117
pages/chat.tsx Normal file
View file

@ -0,0 +1,117 @@
import * as React from 'react';
import { useChatState } from '../src/context/ChatContext';
import { separateBySender, getSenders } from '../src/utils/chat';
import {
CardWithTitle,
SubTitle,
Card,
SingleLine,
} from '../src/components/generic/Styled';
import { Contacts } from '../src/views/chat/Contacts';
import styled from 'styled-components';
import { ChatBox } from '../src/views/chat/ChatBox';
import { ChatStart } from '../src/views/chat/ChatStart';
import { useStatusState } from '../src/context/StatusContext';
import { Text } from '../src/components/typography/Styled';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { ChatCard } from '../src/views/chat/Chat.styled';
import { ViewSwitch } from '../src/components/viewSwitch/ViewSwitch';
import { ColorButton } from '../src/components/buttons/colorButton/ColorButton';
import { Users } from 'react-feather';
const ChatLayout = styled.div`
display: flex;
${({ withHeight = true }: { withHeight: boolean }) =>
withHeight && 'height: 600px'}
`;
const ChatView = () => {
const { minorVersion } = useStatusState();
const { chats, sender, sentChats, initialized } = useChatState();
const bySender = separateBySender([...chats, ...sentChats]);
const senders = getSenders(bySender);
const [user, setUser] = React.useState('');
const [showContacts, setShowContacts] = React.useState(false);
if (!initialized) {
return <LoadingCard title={'Chats'} />;
}
if (minorVersion < 9 && initialized) {
return (
<CardWithTitle>
<SingleLine>
<SubTitle>Chat</SubTitle>
</SingleLine>
<Card>
<Text>
Chatting with other nodes is only available for nodes with LND
versions 0.9.0-beta and up.
</Text>
<Text>If you want to use this feature please update your node.</Text>
</Card>
</CardWithTitle>
);
}
const renderChats = () => {
if (showContacts) {
return (
<Contacts
contacts={senders}
user={user}
setUser={setUser}
setShow={setShowContacts}
/>
);
}
return (
<ChatLayout withHeight={user !== 'New Chat'}>
<Contacts
contacts={senders}
user={user}
setUser={setUser}
setShow={setShowContacts}
hide={true}
/>
{user === 'New Chat' ? (
<ChatStart noTitle={true} />
) : (
<ChatBox messages={bySender[sender]} alias={user} />
)}
</ChatLayout>
);
};
return (
<CardWithTitle>
{!showContacts && user !== 'New Chat' && (
<>
<ViewSwitch hideMobile={true}>
<SingleLine>
<SubTitle>Chat</SubTitle>
</SingleLine>
</ViewSwitch>
<ViewSwitch>
<SingleLine>
<ColorButton onClick={() => setShowContacts(prev => !prev)}>
<Users size={18} />
</ColorButton>
<SubTitle>{user}</SubTitle>
</SingleLine>
</ViewSwitch>
</>
)}
<ChatCard mobileCardPadding={'0'}>
{chats.length <= 0 && sentChats.length <= 0 ? (
<ChatStart />
) : (
renderChats()
)}
</ChatCard>
</CardWithTitle>
);
};
export default ChatView;

View file

@ -16,7 +16,7 @@ import { toast } from 'react-toastify';
import { getErrorContent } from '../src/utils/error';
import { LoadingCard } from '../src/components/loading/LoadingCard';
import { FeeCard } from '../src/views/fees/FeeCard';
import { XSvg, ChevronRight } from '../src/components/generic/Icons';
import { ChevronRight, X } from 'react-feather';
import { SecureButton } from '../src/components/buttons/secureButton/SecureButton';
import { AdminSwitch } from '../src/components/adminSwitch/AdminSwitch';
import { ColorButton } from '../src/components/buttons/colorButton/ColorButton';
@ -64,7 +64,7 @@ const FeesView = () => {
<SingleLine>
<Sub4Title>Channel Fees</Sub4Title>
<ColorButton onClick={() => setIsEdit(prev => !prev)}>
{isEdit ? <XSvg /> : 'Update'}
{isEdit ? <X size={18} /> : 'Update'}
</ColorButton>
</SingleLine>
{isEdit && (
@ -102,7 +102,7 @@ const FeesView = () => {
withMargin={'16px 0 0'}
>
Update Fees
<ChevronRight />
<ChevronRight size={18} />
</SecureButton>
</RightAlign>
</>

View file

@ -7,6 +7,7 @@ import { AccountSettings } from '../src/views/settings/Account';
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';
export const ButtonRow = styled.div`
width: auto;
@ -29,6 +30,7 @@ const SettingsView = () => {
return (
<>
<InterfaceSettings />
<ChatSettings />
<SyncSettings />
<CurrentSettings />
<AccountSettings />

1
setupTests.js Normal file
View file

@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect';

View file

@ -0,0 +1,93 @@
const MESSAGE_TYPE = '34349334';
const SIGNATURE_TYPE = '34349337';
const SENDER_TYPE = '34349339';
const ALIAS_TYPE = '34349340';
const CONTENT_TYPE = '34349345';
const REQUEST_TYPE = '34349347';
const KEYSEND_TYPE = '5482373484';
const bufferHexToUtf = (value: string) =>
Buffer.from(value, 'hex').toString('utf8');
const bufferUtfToHex = (value: string) =>
Buffer.from(value, 'utf8').toString('hex');
interface CreateCustomRecordsProps {
message: string;
sender: string;
alias: string;
contentType: string;
requestType: string;
secret: string;
signature: string;
}
interface CustomRecordsProps {
type: string;
value: string;
}
export const createCustomRecords = ({
message,
sender,
alias,
contentType,
requestType,
secret,
signature,
}: CreateCustomRecordsProps): CustomRecordsProps[] => {
return [
{
type: KEYSEND_TYPE,
value: secret,
},
{
type: MESSAGE_TYPE,
value: bufferUtfToHex(message),
},
{
type: SENDER_TYPE,
value: sender,
},
{
type: ALIAS_TYPE,
value: bufferUtfToHex(alias),
},
{
type: CONTENT_TYPE,
value: bufferUtfToHex(contentType),
},
{
type: REQUEST_TYPE,
value: bufferUtfToHex(requestType),
},
{
type: SIGNATURE_TYPE,
value: bufferUtfToHex(signature),
},
];
};
export const decodeMessage = ({
type,
value,
}): { [key: string]: string } | {} => {
switch (type) {
case MESSAGE_TYPE:
return { message: bufferHexToUtf(value) };
case SIGNATURE_TYPE:
return { signature: bufferHexToUtf(value) };
case SENDER_TYPE:
return { sender: value };
case ALIAS_TYPE:
return { alias: bufferHexToUtf(value) };
case CONTENT_TYPE:
return { contentType: bufferHexToUtf(value) };
case REQUEST_TYPE:
return { requestType: bufferHexToUtf(value) };
// case KEYSEND_TYPE:
// return Buffer.from(value, 'hex').toString('utf8');
default:
return {};
}
};

View file

@ -39,24 +39,16 @@ export const getAuthLnd = (auth: {
return lnd;
};
export const getErrorDetails = (error: any[]): string => {
let details = '';
if (error.length > 2) {
if (error[2].err) {
details = error[2].err.details;
} else if (error[2].details) {
details = error[2].details;
}
export const getErrorMsg = (error: any[] | string): string => {
if (typeof error === 'string') {
return error;
}
if (error.length >= 2) {
return error[1];
}
// if (error.length > 2) {
// return error[2].err?.message || 'Error';
// }
return details;
};
export const getErrorMsg = (error: any[]): string => {
const code = error[0];
const msg = error[1];
const details = getErrorDetails(error);
return JSON.stringify({ code, msg, details });
return 'Error';
};

View file

@ -0,0 +1,5 @@
import { sendMessage } from '../chat/sendMessage';
export const chat = {
sendMessage,
};

View file

@ -0,0 +1,109 @@
import {
payViaPaymentDetails,
getWalletInfo,
probeForRoute,
signMessage,
} from 'ln-service';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString, GraphQLNonNull, GraphQLInt } from 'graphql';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { createCustomRecords } from '../../../helpers/customRecords';
import { randomBytes, createHash } from 'crypto';
import { logger } from '../../../helpers/logger';
const to = promise => {
return promise
.then(data => {
return data;
})
.catch(err => {
logger.error('%o', err);
throw new Error(getErrorMsg(err));
});
};
export const sendMessage = {
type: GraphQLInt,
args: {
...defaultParams,
publicKey: { type: new GraphQLNonNull(GraphQLString) },
message: { type: new GraphQLNonNull(GraphQLString) },
messageType: { type: GraphQLString },
tokens: { type: GraphQLInt },
maxFee: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'sendMessage');
const lnd = getAuthLnd(params.auth);
if (params.maxFee) {
const tokens = Math.max(params.tokens || 100, 100);
const { route } = await to(
probeForRoute({
destination: params.publicKey,
lnd,
tokens,
})
);
if (!route) {
throw new Error('NoRouteFound');
}
if (route.safe_fee > params.maxFee) {
throw new Error('Higher fee limit must be set');
}
}
let satsToSend = params.tokens || 1;
let messageToSend = params.message;
if (params.messageType === 'paymentrequest') {
satsToSend = 1;
messageToSend = `${params.tokens},${params.message}`;
}
const nodeInfo = await to(
getWalletInfo({
lnd,
})
);
const userAlias = nodeInfo.alias;
const userKey = nodeInfo.public_key;
const preimage = randomBytes(32);
const secret = preimage.toString('hex');
const id = createHash('sha256').update(preimage).digest().toString('hex');
const messageToSign = JSON.stringify({
sender: userKey,
message: messageToSend,
});
const { signature } = await to(
signMessage({ lnd, message: messageToSign })
);
const customRecords = createCustomRecords({
message: messageToSend,
sender: userKey,
alias: userAlias,
contentType: params.messageType || 'text',
requestType: '',
signature,
secret,
});
const { safe_fee } = await to(
payViaPaymentDetails({
id,
lnd,
tokens: satsToSend,
destination: params.publicKey,
messages: customRecords,
})
);
return safe_fee;
},
};

View file

@ -2,10 +2,12 @@ import { channels } from './channels';
import { invoices } from './invoices';
import { onChain } from './onchain';
import { peers } from './peers';
import { chat } from './chat';
export const mutation = {
...channels,
...invoices,
...onChain,
...peers,
...chat,
};

View file

@ -71,6 +71,7 @@ export const getChannelFees = {
feeRate: fee_rate,
transactionId: transaction_id,
transactionVout: transaction_vout,
public_key: channel.partner_public_key,
};
})
);

View file

@ -0,0 +1,102 @@
import { GraphQLString, GraphQLBoolean } from 'graphql';
import { getInvoices, verifyMessage } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { decodeMessage } from '../../../helpers/customRecords';
import { GetMessagesType } from '../../types/QueryType';
const to = promise => {
return promise
.then(data => {
return [null, data];
})
.catch(err => [err]);
};
export const getMessages = {
type: GetMessagesType,
args: {
...defaultParams,
token: { type: GraphQLString },
initialize: { type: GraphQLBoolean },
lastMessage: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getMessages');
const lnd = getAuthLnd(params.auth);
const [error, invoiceList] = await to(
getInvoices({
lnd,
limit: params.initialize ? 100 : 5,
})
);
if (error) {
logger.error('Error getting invoices: %o', error);
throw new Error(getErrorMsg(error));
}
const getFiltered = () =>
Promise.all(
invoiceList.invoices.map(async invoice => {
if (!invoice.is_confirmed) {
return;
}
const messages = invoice.payments[0].messages;
let customRecords: { [key: string]: string } = {};
messages.map(message => {
const { type, value } = message;
const obj = decodeMessage({ type, value });
customRecords = { ...customRecords, ...obj };
});
if (Object.keys(customRecords).length <= 0) {
return;
}
let isVerified = false;
if (customRecords.signature) {
const messageToVerify = JSON.stringify({
sender: customRecords.sender,
message: customRecords.message,
});
const [error, { signed_by }] = await to(
verifyMessage({
lnd,
message: messageToVerify,
signature: customRecords.signature,
})
);
if (!error && signed_by === customRecords.sender) {
isVerified = true;
}
}
return {
date: invoice.confirmed_at,
id: invoice.id,
tokens: invoice.tokens,
verified: isVerified,
...customRecords,
};
})
);
const filtered = await getFiltered();
const final = filtered.filter(message => !!message);
// logger.warn('Invoices: %o', final);
return { token: invoiceList.next, messages: final };
},
};

View file

@ -0,0 +1,5 @@
import { getMessages } from './getMessages';
export const chatQueries = {
getMessages,
};

View file

@ -1,8 +1,9 @@
import { pay as payRequest } from 'ln-service';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLBoolean } from 'graphql';
import { getAuthLnd, getErrorDetails } from '../../../helpers/helpers';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
import { logger } from '../../../helpers/logger';
export const adminCheck = {
type: GraphQLBoolean,
@ -20,10 +21,17 @@ export const adminCheck = {
request: 'admin check',
});
} catch (error) {
const details = getErrorDetails(error);
if (details.includes('invalid character in string')) return true;
params.logger && logger.error('%o', error);
if (error.length >= 2) {
if (error[2]?.err?.details?.indexOf('permission denied') >= 0) {
throw new Error('PermissionDenied');
}
}
throw new Error();
const errorMessage = getErrorMsg(error);
if (errorMessage.indexOf('UnexpectedSendPaymentError') >= 0) return true;
throw new Error(errorMessage);
}
},
};

View file

@ -10,6 +10,7 @@ import { peerQueries } from './peer';
import { messageQueries } from './message';
import { chainQueries } from './chain';
import { hodlQueries } from './hodlhodl';
import { chatQueries } from './chat';
export const query = {
...channelQueries,
@ -24,4 +25,5 @@ export const query = {
...messageQueries,
...chainQueries,
...hodlQueries,
...chatQueries,
};

View file

@ -34,7 +34,7 @@ export const getRoutes = {
});
if (!route) {
throw new Error('No route found.');
throw new Error('NoRouteFound');
}
return JSON.stringify(route);

View file

@ -17,6 +17,11 @@ export interface PaymentsProps {
payments: PaymentProps[];
}
interface InvoiceMessagesType {
type: string;
value: string;
}
export interface InvoicePaymentProps {
confirmed_at: string;
created_at: string;
@ -25,6 +30,7 @@ export interface InvoicePaymentProps {
is_canceled: boolean;
is_confirmed: boolean;
is_held: boolean;
messages: InvoiceMessagesType[];
mtokens: string;
pending_index: number;
tokens: number;

View file

@ -39,7 +39,7 @@ export const getResume = {
public_key: payment.destination,
});
} catch (error) {
nodeInfo = { alias: 'unknown' };
nodeInfo = { alias: payment.destination?.substring(0, 6) };
}
return {
type: 'payment',

View file

@ -26,6 +26,7 @@ export const ChannelFeeType = new GraphQLObjectType({
feeRate: { type: GraphQLInt },
transactionId: { type: GraphQLString },
transactionVout: { type: GraphQLInt },
public_key: { type: GraphQLString },
};
},
});
@ -288,3 +289,25 @@ export const GetResumeType = new GraphQLObjectType({
resume: { type: GraphQLString },
}),
});
export const GetMessagesType = new GraphQLObjectType({
name: 'getMessagesType',
fields: () => ({
token: { type: GraphQLString },
messages: { type: new GraphQLList(MessagesType) },
}),
});
export const MessagesType = new GraphQLObjectType({
name: 'messagesType',
fields: () => ({
date: { type: GraphQLString },
id: { type: GraphQLString },
verified: { type: GraphQLBoolean },
contentType: { type: GraphQLString },
sender: { type: GraphQLString },
alias: { type: GraphQLString },
message: { type: GraphQLString },
tokens: { type: GraphQLInt },
}),
});

View file

@ -50,4 +50,6 @@ export const RateConfig: RateConfigProps = {
getOffers: { max: 10, window: '1s' },
getCountries: { max: 10, window: '1s' },
getCurrencies: { max: 10, window: '1s' },
sendMessage: { max: 10, window: '1s' },
getMessages: { max: 10, window: '1s' },
};

View file

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 438 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>

Before

Width:  |  Height:  |  Size: 356 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

Before

Width:  |  Height:  |  Size: 424 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor"><circle cx="12" cy="5" r="3"></circle><line x1="12" y1="22" x2="12" y2="8"></line><path d="M5 12H2a10 10 0 0 0 20 0h-3"></path></svg>

Before

Width:  |  Height:  |  Size: 345 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>

Before

Width:  |  Height:  |  Size: 313 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>

Before

Width:  |  Height:  |  Size: 310 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>

Before

Width:  |  Height:  |  Size: 262 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

Before

Width:  |  Height:  |  Size: 269 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>

Before

Width:  |  Height:  |  Size: 270 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>

Before

Width:  |  Height:  |  Size: 270 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>

Before

Width:  |  Height:  |  Size: 268 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-down"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>

Before

Width:  |  Height:  |  Size: 317 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-up"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>

Before

Width:  |  Height:  |  Size: 316 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-circle"><circle cx="12" cy="12" r="10"></circle></svg>

Before

Width:  |  Height:  |  Size: 258 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>

Before

Width:  |  Height:  |  Size: 351 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>

Before

Width:  |  Height:  |  Size: 667 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-credit-card"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg>

Before

Width:  |  Height:  |  Size: 329 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crosshair"><circle cx="12" cy="12" r="10"></circle><line x1="22" y1="12" x2="18" y2="12"></line><line x1="6" y1="12" x2="2" y2="12"></line><line x1="12" y1="6" x2="12" y2="2"></line><line x1="12" y1="22" x2="12" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 437 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>

Before

Width:  |  Height:  |  Size: 365 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>

Before

Width:  |  Height:  |  Size: 460 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>

Before

Width:  |  Height:  |  Size: 316 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-branch"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg>

Before

Width:  |  Height:  |  Size: 377 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-commit"><circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line></svg>

Before

Width:  |  Height:  |  Size: 358 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-pull-request"><circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M13 6h3a2 2 0 0 1 2 2v7"></path><line x1="6" y1="9" x2="6" y2="21"></line></svg>

Before

Width:  |  Height:  |  Size: 387 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>

Before

Width:  |  Height:  |  Size: 527 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>

Before

Width:  |  Height:  |  Size: 409 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

Before

Width:  |  Height:  |  Size: 365 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

Before

Width:  |  Height:  |  Size: 332 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-key"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>

Before

Width:  |  Height:  |  Size: 352 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>

Before

Width:  |  Height:  |  Size: 365 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>

Before

Width:  |  Height:  |  Size: 371 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-loader"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg>

Before

Width:  |  Height:  |  Size: 614 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mail"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>

Before

Width:  |  Height:  |  Size: 354 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 346 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-moon"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>

Before

Width:  |  Height:  |  Size: 281 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>

Before

Width:  |  Height:  |  Size: 341 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pocket"><path d="M4 3h16a2 2 0 0 1 2 2v6a10 10 0 0 1-10 10A10 10 0 0 1 2 11V5a2 2 0 0 1 2-2z"></path><polyline points="8 10 12 14 16 10"></polyline></svg>

Before

Width:  |  Height:  |  Size: 358 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg>

Before

Width:  |  Height:  |  Size: 389 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>

Before

Width:  |  Height:  |  Size: 392 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>

Before

Width:  |  Height:  |  Size: 314 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-server"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 431 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>

Before

Width:  |  Height:  |  Size: 1,011 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>

Before

Width:  |  Height:  |  Size: 279 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sliders"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>

Before

Width:  |  Height:  |  Size: 611 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>

Before

Width:  |  Height:  |  Size: 339 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>

Before

Width:  |  Height:  |  Size: 650 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>

Before

Width:  |  Height:  |  Size: 400 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 299 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap-off"><polyline points="12.41 6.75 13 2 10.57 4.92"></polyline><polyline points="18.57 12.91 21 10 15.66 10"></polyline><polyline points="8 8 3 14 12 14 11 22 16 16"></polyline><line x1="1" y1="1" x2="23" y2="23"></line></svg>

Before

Width:  |  Height:  |  Size: 433 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>

Before

Width:  |  Height:  |  Size: 282 B

View file

@ -2,7 +2,7 @@ import React from 'react';
import { SingleLine, Sub4Title } from '../../generic/Styled';
import ScaleLoader from 'react-spinners/ScaleLoader';
import { themeColors } from '../../../styles/Themes';
import { XSvg, Check } from '../../generic/Icons';
import { X, Check } from 'react-feather';
import { useGetCanAdminQuery } from '../../../generated/graphql';
type AdminProps = {
@ -30,9 +30,9 @@ export const AdminCheck = ({ host, admin, cert, setChecked }: AdminProps) => {
return <ScaleLoader height={20} color={themeColors.blue3} />;
}
if (data?.adminCheck) {
return <Check />;
return <Check size={18} />;
}
return <XSvg />;
return <X size={18} />;
};
return (

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { SingleLine, Sub4Title, Separation } from '../../generic/Styled';
import ScaleLoader from 'react-spinners/ScaleLoader';
import { themeColors } from '../../../styles/Themes';
import { Check, XSvg } from '../../generic/Icons';
import { Check, X } from 'react-feather';
import { ColorButton } from '../../buttons/colorButton/ColorButton';
import { AdminCheck } from './AdminCheck';
import { Text } from '../../typography/Styled';
@ -34,6 +34,7 @@ export const ViewCheck = ({
const [confirmed, setConfirmed] = useState(false);
const { data, loading } = useGetCanConnectQuery({
fetchPolicy: 'network-only',
variables: { auth: { host, macaroon: viewOnly ?? admin ?? '', cert } },
onCompleted: () => setConfirmed(true),
onError: () => setConfirmed(false),
@ -50,9 +51,9 @@ export const ViewCheck = ({
return <ScaleLoader height={20} color={themeColors.blue3} />;
}
if (data?.getNodeInfo.alias && viewOnly) {
return <Check />;
return <Check size={18} />;
}
return <XSvg />;
return <X size={18} />;
};
const renderInfo = () => {

View file

@ -12,6 +12,7 @@ import { useRouter } from 'next/router';
import { toast } from 'react-toastify';
import { LoadingCard } from '../loading/LoadingCard';
import { appendBasePath } from '../../utils/basePath';
import { useChatDispatch } from '../../context/ChatContext';
const PasswordInput = dynamic(() => import('./views/Password'), {
ssr: false,
@ -34,6 +35,7 @@ export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
const { changeAccount, accounts } = useAccount();
const { push } = useRouter();
const dispatchChat = useChatDispatch();
const dispatch = useStatusDispatch();
const [name, setName] = useState<string>();
@ -110,6 +112,7 @@ export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
const id = getAccountId(host, viewOnly, admin, cert);
dispatch({ type: 'disconnected' });
dispatchChat({ type: 'disconnected' });
changeAccount(id);
push(appendBasePath('/'));
@ -138,6 +141,7 @@ export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
const id = getAccountId(host, viewOnly, admin, cert);
dispatch({ type: 'disconnected' });
dispatchChat({ type: 'disconnected' });
changeAccount(id);
push(appendBasePath('/'));

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Checkbox } from '../../checkbox/Checkbox';
import { CheckboxText, StyledContainer, FixedWidth } from '../Auth.styled';
import { AlertCircle } from '../../generic/Icons';
import { AlertCircle } from 'react-feather';
import { fontColors } from '../../../styles/Themes';
import { ColorButton } from '../../buttons/colorButton/ColorButton';
import getConfig from 'next/config';
@ -50,7 +50,7 @@ export const WarningBox = () => {
return (
<StyledContainer>
<FixedWidth>
<AlertCircle color={fontColors.grey7} />
<AlertCircle size={18} color={fontColors.grey7} />
</FixedWidth>
<CheckboxText>
Macaroons are handled by the ThunderHub server to connect to your LND

View file

@ -12,7 +12,7 @@ import {
themeColors,
mediaWidths,
} from '../../../styles/Themes';
import { ChevronRight } from '../../generic/Icons';
import { ChevronRight } from 'react-feather';
import ScaleLoader from 'react-spinners/ScaleLoader';
interface GeneralProps {
@ -110,7 +110,7 @@ const DisabledButton = styled(GeneralButton)`
const renderArrow = () => (
<StyledArrow>
<ChevronRight size={'18px'} />
<ChevronRight size={18} />
</StyledArrow>
);

View file

@ -7,7 +7,7 @@ import {
SubTitle,
ResponsiveLine,
} from '../../generic/Styled';
import { Circle, ChevronRight } from '../../generic/Icons';
import { Circle, ChevronRight } from 'react-feather';
import styled from 'styled-components';
import { useAccount } from '../../../context/AccountContext';
import { saveSessionAuth } from '../../../utils/auth';
@ -71,7 +71,7 @@ export const LoginModal = ({
selected: boolean
) => (
<ColorButton color={color} onClick={onClick}>
<Circle size={'10px'} fillcolor={selected ? textColorMap[theme] : ''} />
<Circle size={10} fill={selected ? textColorMap[theme] : ''} />
<RadioText>{text}</RadioText>
</ColorButton>
);
@ -103,7 +103,7 @@ export const LoginModal = ({
withMargin={'16px 0 0'}
>
Unlock
<ChevronRight />
<ChevronRight size={18} />
</ColorButton>
</>
);

View file

@ -15,14 +15,14 @@ interface SecureButtonProps extends ColorButtonProps {
arrow?: boolean;
}
export const SecureButton = ({
export const SecureButton: React.FC<SecureButtonProps> = ({
callback,
color,
disabled,
children,
variables,
...props
}: SecureButtonProps) => {
}) => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const { host, cert, admin, sessionAdmin } = useAccount();

View file

@ -0,0 +1,49 @@
import React, { useState } from 'react';
import Modal from '../../modal/ReactModal';
import { LoginModal } from './LoginModal';
import { useAccount } from '../../../context/AccountContext';
interface SecureButtonProps {
callback: any;
children: any;
variables: {};
color?: string;
}
export const SecureWrapper: React.FC<SecureButtonProps> = ({
callback,
children,
variables,
color,
}) => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const { host, cert, admin, sessionAdmin } = useAccount();
if (!admin && !sessionAdmin) {
return null;
}
const auth = { host, macaroon: sessionAdmin, cert };
const handleClick = () => setModalOpen(true);
const onClick = sessionAdmin
? () => callback({ variables: { ...variables, auth } })
: handleClick;
return (
<>
<div onClick={onClick}>{children}</div>
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
<LoginModal
color={color}
macaroon={admin}
setModalOpen={setModalOpen}
callback={callback}
variables={variables}
/>
</Modal>
</>
);
};

View file

@ -0,0 +1,64 @@
import * as React from 'react';
import { useChatState, useChatDispatch } from '../../context/ChatContext';
import { useGetMessagesQuery } from '../../generated/graphql';
import { useAccount } from '../../context/AccountContext';
import { toast } from 'react-toastify';
import { getErrorContent } from '../../utils/error';
import { useRouter } from 'next/router';
export const ChatFetcher = () => {
const newChatToastId = 'newChatToastId';
const { auth } = useAccount();
const { pathname } = useRouter();
const { lastChat, chats, sentChats, initialized } = useChatState();
const dispatch = useChatDispatch();
const noChatsAvailable = chats.length <= 0 && sentChats.length <= 0;
const { data, loading, error } = useGetMessagesQuery({
skip: !auth || initialized || noChatsAvailable,
pollInterval: 1000,
fetchPolicy: 'network-only',
variables: { auth, initialize: !noChatsAvailable },
onError: error => toast.error(getErrorContent(error)),
});
React.useEffect(() => {
if (data?.getMessages?.messages) {
const messages = [...data.getMessages.messages];
let index = -1;
if (lastChat !== '') {
for (let i = 0; i < messages.length; i += 1) {
if (index < 0) {
const element = messages[i];
const { id } = element;
if (id === lastChat) {
index = i;
}
}
}
} else {
index = 100;
}
if (index < 1) {
return;
}
if (pathname !== '/chat') {
if (!toast.isActive(newChatToastId)) {
toast.success('You have a new message', { position: 'bottom-right' });
}
}
const newMessages = messages.slice(0, index);
const last = newMessages[0]?.id;
dispatch({ type: 'additional', chats: newMessages, lastChat: last });
}
}, [data, loading, error]);
return null;
};

View file

@ -0,0 +1,70 @@
import * as React from 'react';
import { useChatDispatch } from '../../context/ChatContext';
import { useGetMessagesLazyQuery } from '../../generated/graphql';
import { useAccount } from '../../context/AccountContext';
import { toast } from 'react-toastify';
import { getErrorContent } from '../../utils/error';
export const ChatInit = () => {
const { auth, id } = useAccount();
const dispatch = useChatDispatch();
const [
getMessages,
{ data: initData, loading: initLoading, error: initError },
] = useGetMessagesLazyQuery({
variables: { auth, initialize: true },
onError: error => toast.error(getErrorContent(error)),
});
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 {
const savedChats = JSON.parse(storageChats);
if (savedChats.length > 0) {
const sender = savedChats[0].sender;
dispatch({
type: 'initialized',
sentChats: savedChats,
sender,
hideFee,
hideNonVerified,
maxFee,
});
}
} catch (error) {
localStorage.removeItem('sentChats');
}
}
getMessages();
}, []);
React.useEffect(() => {
if (!initLoading && !initError && initData?.getMessages) {
const { messages } = initData.getMessages;
if (messages.length <= 0) {
dispatch({ type: 'initialized' });
return;
}
const lastChat = messages[0].id || '';
const sender = messages[0].sender || '';
dispatch({
type: 'initialized',
chats: messages,
lastChat,
sender,
});
}
}, [initLoading, initError, initData]);
return null;
};

View file

@ -1,124 +0,0 @@
import { FunctionComponent } from 'react';
import styled, { css } from 'styled-components';
import UpIcon from '../../assets/icons/arrow-up.svg';
import DownIcon from '../../assets/icons/arrow-down.svg';
import ZapIcon from '../../assets/icons/zap.svg';
import ZapOffIcon from '../../assets/icons/zap-off.svg';
import HelpIcon from '../../assets/icons/help-circle.svg';
import SunIcon from '../../assets/icons/sun.svg';
import MoonIcon from '../../assets/icons/moon.svg';
import EyeIcon from '../../assets/icons/eye.svg';
import EyeOffIcon from '../../assets/icons/eye-off.svg';
import ChevronsUpIcon from '../../assets/icons/chevrons-up.svg';
import ChevronsDownIcon from '../../assets/icons/chevrons-down.svg';
import ChevronLeftIcon from '../../assets/icons/chevron-left.svg';
import ChevronRightIcon from '../../assets/icons/chevron-right.svg';
import ChevronUpIcon from '../../assets/icons/chevron-up.svg';
import ChevronDownIcon from '../../assets/icons/chevron-down.svg';
import HomeIcon from '../../assets/icons/home.svg';
import CpuIcon from '../../assets/icons/cpu.svg';
import SendIcon from '../../assets/icons/send.svg';
import ServerIcon from '../../assets/icons/server.svg';
import SettingsIcon from '../../assets/icons/settings.svg';
import EditIcon from '../../assets/icons/edit.svg';
import MoreVerticalIcon from '../../assets/icons/more-vertical.svg';
import AnchorIcon from '../../assets/icons/anchor.svg';
import PocketIcon from '../../assets/icons/pocket.svg';
import GlobeIcon from '../../assets/icons/globe.svg';
import XIcon from '../../assets/icons/x.svg';
import LayersIcon from '../../assets/icons/layers.svg';
import LoaderIcon from '../../assets/icons/loader.svg';
import CircleIcon from '../../assets/icons/circle.svg';
import AlertTriangleIcon from '../../assets/icons/alert-triangle.svg';
import AlertCircleIcon from '../../assets/icons/alert-circle.svg';
import GitCommitIcon from '../../assets/icons/git-commit.svg';
import GitBranchIcon from '../../assets/icons/git-branch.svg';
import RadioIcon from '../../assets/icons/radio.svg';
import CopyIcon from '../../assets/icons/copy.svg';
import ShieldIcon from '../../assets/icons/shield.svg';
import CrosshairIcon from '../../assets/icons/crosshair.svg';
import KeyIcon from '../../assets/icons/key.svg';
import SlidersIcon from '../../assets/icons/sliders.svg';
import UsersIcon from '../../assets/icons/users.svg';
import GitPullRequestIcon from '../../assets/icons/git-pull-request.svg';
import Link from '../../assets/icons/link.svg';
import Menu from '../../assets/icons/menu.svg';
import Mail from '../../assets/icons/mail.svg';
import Github from '../../assets/icons/github.svg';
import Repeat from '../../assets/icons/repeat.svg';
import CheckIcon from '../../assets/icons/check.svg';
import StarIcon from '../../assets/icons/star.svg';
import HalfStarIcon from '../../assets/icons/half-star.svg';
import CreditCardIcon from '../../assets/icons/credit-card.svg';
export interface IconProps {
color?: string;
size?: string;
fillcolor?: string;
strokeWidth?: string;
}
const GenericStyles = css`
height: ${({ size }: IconProps) => (size ? size : '18px')};
width: ${({ size }: IconProps) => (size ? size : '18px')};
color: ${({ color }: IconProps) => (color ? color : '')};
fill: ${({ fillcolor }: IconProps) => (fillcolor ? fillcolor : '')};
stroke-width: ${({ strokeWidth }: IconProps) =>
strokeWidth ? strokeWidth : '2px'};
`;
const styleIcon = (icon: FunctionComponent) =>
styled(icon)`
${GenericStyles}
`;
export const QuestionIcon = styleIcon(HelpIcon);
export const Zap = styleIcon(ZapIcon);
export const ZapOff = styleIcon(ZapOffIcon);
export const Anchor = styleIcon(AnchorIcon);
export const Pocket = styleIcon(PocketIcon);
export const Globe = styleIcon(GlobeIcon);
export const UpArrow = styleIcon(UpIcon);
export const DownArrow = styleIcon(DownIcon);
export const Sun = styleIcon(SunIcon);
export const Moon = styleIcon(MoonIcon);
export const Eye = styleIcon(EyeIcon);
export const EyeOff = styleIcon(EyeOffIcon);
export const ChevronsDown = styleIcon(ChevronsDownIcon);
export const ChevronsUp = styleIcon(ChevronsUpIcon);
export const ChevronLeft = styleIcon(ChevronLeftIcon);
export const ChevronRight = styleIcon(ChevronRightIcon);
export const ChevronUp = styleIcon(ChevronUpIcon);
export const ChevronDown = styleIcon(ChevronDownIcon);
export const Home = styleIcon(HomeIcon);
export const Cpu = styleIcon(CpuIcon);
export const Send = styleIcon(SendIcon);
export const Server = styleIcon(ServerIcon);
export const Settings = styleIcon(SettingsIcon);
export const Edit = styleIcon(EditIcon);
export const MoreVertical = styleIcon(MoreVerticalIcon);
export const XSvg = styleIcon(XIcon);
export const Layers = styleIcon(LayersIcon);
export const Loader = styleIcon(LoaderIcon);
export const Circle = styleIcon(CircleIcon);
export const AlertTriangle = styleIcon(AlertTriangleIcon);
export const AlertCircle = styleIcon(AlertCircleIcon);
export const GitCommit = styleIcon(GitCommitIcon);
export const GitBranch = styleIcon(GitBranchIcon);
export const Radio = styleIcon(RadioIcon);
export const Copy = styleIcon(CopyIcon);
export const Shield = styleIcon(ShieldIcon);
export const Crosshair = styleIcon(CrosshairIcon);
export const Key = styleIcon(KeyIcon);
export const Sliders = styleIcon(SlidersIcon);
export const Users = styleIcon(UsersIcon);
export const GitPullRequest = styleIcon(GitPullRequestIcon);
export const LinkIcon = styleIcon(Link);
export const MenuIcon = styleIcon(Menu);
export const MailIcon = styleIcon(Mail);
export const GithubIcon = styleIcon(Github);
export const RepeatIcon = styleIcon(Repeat);
export const Check = styleIcon(CheckIcon);
export const Star = styleIcon(StarIcon);
export const HalfStar = styleIcon(HalfStarIcon);
export const CreditCard = styleIcon(CreditCardIcon);

View file

@ -31,13 +31,13 @@ export interface CardProps {
mobileCardPadding?: string;
}
export const Card = styled.div`
padding: ${({ cardPadding }: CardProps) => cardPadding ?? '16px'};
export const Card = styled.div<CardProps>`
padding: ${({ cardPadding }) => cardPadding ?? '16px'};
background: ${cardColor};
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
border-radius: 4px;
border: 1px solid ${cardBorderColor};
margin-bottom: ${({ bottom }: CardProps) => (bottom ? bottom : '25px')};
margin-bottom: ${({ bottom }) => (bottom ? bottom : '25px')};
width: 100%;
@media (${mediaWidths.mobile}) {
@ -198,12 +198,13 @@ export const ColorButton = styled(SimpleButton)`
export const OverflowText = styled.div`
margin-left: 16px;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
-webkit-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
@media (${mediaWidths.mobile}) {
margin-left: 8px;

View file

@ -1,8 +1,13 @@
import React from 'react';
import { SmallLink, DarkSubTitle, OverflowText, SingleLine } from './Styled';
import { StatusDot, DetailLine } from './CardGeneric';
import { format, formatDistanceStrict } from 'date-fns';
import { XSvg } from './Icons';
import {
format,
formatDistanceToNowStrict,
differenceInCalendarDays,
isToday,
} from 'date-fns';
import { X } from 'react-feather';
export const getTransactionLink = (transaction: string) => {
const link = `https://www.blockchain.com/btc/tx/${transaction}`;
@ -23,11 +28,42 @@ export const getNodeLink = (publicKey: string) => {
};
export const getDateDif = (date: string) => {
return formatDistanceStrict(new Date(date), new Date());
return formatDistanceToNowStrict(new Date(date));
};
export const getFormatDate = (date: string) => {
return format(new Date(date), 'dd-MM-yyyy - HH:mm:ss');
return format(new Date(date), 'dd/MM/yyyy - HH:mm:ss');
};
export const getMessageDate = (date: string, formatType?: string): string => {
let distance = formatDistanceToNowStrict(new Date(date));
if (distance.indexOf('minute') >= 0 || distance.indexOf('second') >= 0) {
distance = distance.replace('minutes', 'min');
distance = distance.replace('minute', 'min');
distance = distance.replace('seconds', 'sec');
distance = distance.replace('second', 'sec');
distance = distance.replace('0 sec', 'now');
return distance;
}
return format(new Date(date), formatType || 'HH:mm');
};
export const getDayChange = (date: string): string => {
if (isToday(new Date(date))) {
return 'Today';
}
return format(new Date(date), 'dd/MM/yy');
};
export const getIsDifferentDay = (current: string, next: string): boolean => {
const today = new Date(current);
const tomorrow = new Date(next);
const difference = differenceInCalendarDays(today, tomorrow);
return difference > 0 ? true : false;
};
export const getTooltipType = (theme: string) => {
@ -62,7 +98,7 @@ export const renderLine = (
<OverflowText>{content}</OverflowText>
{deleteCallback && (
<div style={{ margin: '0 0 -4px 4px' }} onClick={deleteCallback}>
<XSvg />
<X size={18} />
</div>
)}
</SingleLine>

View file

@ -1,5 +1,5 @@
import React from 'react';
import styled, { css } from 'styled-components';
import styled, { css, ThemeSet } from 'styled-components';
import {
textColor,
colorButtonBorder,
@ -10,6 +10,7 @@ import {
interface InputProps {
color?: string;
backgroundColor?: ThemeSet | string;
withMargin?: string;
mobileMargin?: string;
fullWidth?: boolean;
@ -17,7 +18,7 @@ interface InputProps {
maxWidth?: string;
}
export const StyledInput = styled.input`
export const StyledInput = styled.input<InputProps>`
padding: 5px;
height: 30px;
margin: 8px 0;
@ -25,14 +26,14 @@ export const StyledInput = styled.input`
background: none;
border-radius: 5px;
color: ${textColor};
transition: all 0.5s ease;
background-color: ${inputBackgroundColor};
${({ maxWidth }: InputProps) =>
background-color: ${({ backgroundColor }) =>
backgroundColor || inputBackgroundColor};
${({ maxWidth }) =>
maxWidth &&
css`
max-width: ${maxWidth};
`}
width: ${({ fullWidth }: InputProps) => (fullWidth ? '100%' : 'auto')};
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
margin: ${({ withMargin }) => (withMargin ? withMargin : '0')};
@media (${mediaWidths.mobile}) {
@ -60,13 +61,13 @@ export const StyledInput = styled.input`
&:hover {
border: 1px solid
${({ color }: InputProps) => (color ? color : colorButtonBorder)};
${({ color }) => (color ? color : colorButtonBorder)};
}
&:focus {
outline: none;
border: 1px solid
${({ color }: InputProps) => (color ? color : colorButtonBorder)};
${({ color }) => (color ? color : colorButtonBorder)};
}
`;
@ -75,12 +76,14 @@ interface InputCompProps {
value?: number | string;
placeholder?: string;
color?: string;
backgroundColor?: ThemeSet | string;
withMargin?: string;
mobileMargin?: string;
fullWidth?: boolean;
mobileFullWidth?: boolean;
maxWidth?: string;
onChange: (e: any) => void;
onKeyDown?: (e: any) => void;
}
export const Input = ({
@ -88,12 +91,14 @@ export const Input = ({
value,
placeholder,
color,
backgroundColor,
withMargin,
mobileMargin,
mobileFullWidth,
fullWidth = true,
maxWidth,
onChange,
onKeyDown,
}: InputCompProps) => {
return (
<StyledInput
@ -101,12 +106,14 @@ export const Input = ({
placeholder={placeholder}
value={value}
color={color}
backgroundColor={backgroundColor}
withMargin={withMargin}
mobileMargin={mobileMargin}
onChange={e => onChange(e)}
fullWidth={fullWidth}
mobileFullWidth={mobileFullWidth}
maxWidth={maxWidth}
onKeyDown={onKeyDown}
/>
);
};

View file

@ -5,7 +5,7 @@ import {
SubTitle,
Sub4Title,
} from '../../generic/Styled';
import { AlertTriangle } from '../../generic/Icons';
import { AlertTriangle } from 'react-feather';
import styled from 'styled-components';
import { toast } from 'react-toastify';
import { getErrorContent } from '../../../utils/error';
@ -77,7 +77,7 @@ export const CloseChannel = ({
const renderWarning = () => (
<WarningCard>
<AlertTriangle size={'32px'} color={'red'} />
<AlertTriangle size={32} color={'red'} />
<SubTitle>Are you sure you want to close the channel?</SubTitle>
<SecureButton
callback={closeChannel}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { SubTitle } from '../../generic/Styled';
import { AlertTriangle } from '../../generic/Icons';
import { AlertTriangle } from 'react-feather';
import styled from 'styled-components';
import { toast } from 'react-toastify';
import { getErrorContent } from '../../../utils/error';
@ -40,7 +40,7 @@ export const RemovePeerModal = ({
return (
<WarningCard>
<AlertTriangle size={'32px'} color={'red'} />
<AlertTriangle size={32} color={'red'} />
<SubTitle>Are you sure you want to remove this peer?</SubTitle>
<SecureButton
callback={removePeer}
@ -51,7 +51,7 @@ export const RemovePeerModal = ({
disabled={loading}
withMargin={'4px'}
>
{`Remove Peer [${peerAlias ?? 'Unknown'}]`}
{`Remove Peer [${peerAlias || publicKey?.substring(0, 6)}]`}
</SecureButton>
<ColorButton withMargin={'4px'} onClick={handleOnlyClose}>
Cancel

View file

@ -8,13 +8,13 @@ import {
StyledNodeBar,
NodeBarContainer,
} from './NodeInfo.styled';
import { QuestionIcon } from '../generic/Icons';
import { HelpCircle } from 'react-feather';
import styled from 'styled-components';
import ReactTooltip from 'react-tooltip';
import { useSettings } from '../../context/SettingsContext';
import { getTooltipType } from '../generic/helpers';
const StyledQuestion = styled(QuestionIcon)`
const StyledQuestion = styled(HelpCircle)`
margin-left: 8px;
`;
@ -47,7 +47,7 @@ export const NodeBar = () => {
<SubTitle>
Your Nodes
<span data-tip data-for="node_info_question">
<StyledQuestion size={'14px'} />
<StyledQuestion size={14} />
</span>
</SubTitle>
<NodeBarContainer>
@ -56,14 +56,14 @@ export const NodeBar = () => {
handleScroll(true);
}}
>
<ArrowLeft />
<ArrowLeft size={18} />
</div>
<div
onClick={() => {
handleScroll();
}}
>
<ArrowRight />
<ArrowRight size={18} />
</div>
<StyledNodeBar ref={slider}>
{viewOnlyAccounts.map((account, index) => (

View file

@ -1,6 +1,6 @@
import styled, { css } from 'styled-components';
import { Card } from '../generic/Styled';
import { ChevronLeft, ChevronRight } from '../generic/Icons';
import { ChevronLeft, ChevronRight } from 'react-feather';
import {
inverseTextColor,
buttonBorderColor,

View file

@ -41,6 +41,11 @@ export const Price = ({
return <>{getValue({ amount, ...priceProps, breakNumber })}</>;
};
interface GetPriceProps {
amount: number | string;
breakNumber?: boolean;
}
export const getPrice = (
currency: string,
priceContext: {
@ -48,13 +53,7 @@ export const getPrice = (
loading: boolean;
prices?: { [key: string]: { last: number; symbol: string } };
}
) => ({
amount,
breakNumber = false,
}: {
amount: number | string;
breakNumber?: boolean;
}) => {
) => ({ amount, breakNumber = false }: GetPriceProps): string => {
const { prices, loading, error } = priceContext;
let priceProps: PriceProps = {

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Star, HalfStar } from '../../components/generic/Icons';
import { Star } from 'react-feather';
import { HalfStar } from '../../assets/half-star.svg';
import { themeColors } from '../../styles/Themes';
import styled from 'styled-components';
@ -8,6 +9,9 @@ const StyledStar = styled(Star)`
`;
const StyledHalfStar = styled(HalfStar)`
height: 18px;
width: 18px;
stroke-width: 2px;
margin-bottom: -1px;
`;
@ -45,7 +49,7 @@ export const Rating = ({
for (let i = 0; i < 5; i += 1) {
if (i < amount) {
stars.push(
<StyledStar key={i} {...starConfig} fillcolor={themeColors.blue3} />
<StyledStar key={i} {...starConfig} fill={themeColors.blue3} />
);
} else if (hasHalf && i === amount) {
stars.push(<StyledHalfStar key={i} {...starConfig} />);

View file

@ -13,6 +13,7 @@ export const StatusCheck = () => {
const { name, auth } = useAccount();
const { data, loading, error } = useGetNodeInfoQuery({
skip: !auth,
fetchPolicy: 'network-only',
variables: { auth },
pollInterval: 10000,

155
src/context/ChatContext.tsx Normal file
View file

@ -0,0 +1,155 @@
import React, { createContext, useContext, useReducer } from 'react';
type ChatProps = {
date?: string;
contentType?: string;
alias?: string;
message?: string;
id?: string;
sender?: string;
tokens?: number;
};
type SentChatProps = {
date?: string;
contentType?: string;
alias?: string;
message?: string;
id?: string;
sender?: string;
isSent?: boolean;
feePaid?: number;
tokens?: number;
};
type State = {
initialized: boolean;
chats: ChatProps[];
sentChats: SentChatProps[];
lastChat: string;
sender: string;
hideFee: boolean;
hideNonVerified: boolean;
maxFee: number;
};
type ActionType = {
type:
| 'initialized'
| 'additional'
| 'changeActive'
| 'newChat'
| 'hideNonVerified'
| 'hideFee'
| 'changeFee'
| 'disconnected';
chats?: ChatProps[];
sentChats?: SentChatProps[];
newChat?: SentChatProps;
lastChat?: string;
sender?: string;
userId?: string;
hideFee?: boolean;
hideNonVerified?: boolean;
maxFee?: number;
};
type Dispatch = (action: ActionType) => void;
const StateContext = createContext<State | undefined>(undefined);
const DispatchContext = createContext<Dispatch | undefined>(undefined);
const initialState = {
initialized: false,
chats: [],
lastChat: '',
sender: '',
sentChats: [],
hideFee: false,
hideNonVerified: false,
maxFee: 20,
};
const stateReducer = (state: State, action: ActionType): State => {
switch (action.type) {
case 'initialized':
return {
...state,
initialized: true,
...action,
};
case 'additional':
return {
...state,
initialized: true,
chats: [...state.chats, ...action.chats],
lastChat: action.lastChat,
};
case 'changeActive':
return {
...state,
sender: action.sender,
};
case 'newChat':
localStorage.setItem(
`${action.userId}-sentChats`,
JSON.stringify([...state.sentChats, action.newChat])
);
return {
...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;
}
};
const ChatProvider = ({ children }: any) => {
const [state, dispatch] = useReducer(stateReducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
const useChatState = () => {
const context = useContext(StateContext);
if (context === undefined) {
throw new Error('useStatusState must be used within a StatusProvider');
}
return context;
};
const useChatDispatch = () => {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error('useStatusDispatch must be used within a StatusProvider');
}
return context;
};
export { ChatProvider, useChatState, useChatDispatch };

View file

@ -4,13 +4,16 @@ import { SettingsProvider } from './SettingsContext';
import { BitcoinInfoProvider } from './BitcoinContext';
import { StatusProvider } from './StatusContext';
import { PriceProvider } from './PriceContext';
import { ChatProvider } from './ChatContext';
export const ContextProvider: React.FC = ({ children }) => (
<AccountProvider>
<SettingsProvider>
<BitcoinInfoProvider>
<PriceProvider>
<StatusProvider>{children}</StatusProvider>
<ChatProvider>
<StatusProvider>{children}</StatusProvider>
</ChatProvider>
</PriceProvider>
</BitcoinInfoProvider>
</SettingsProvider>

View file

@ -47,6 +47,7 @@ export type Query = {
getOffers?: Maybe<Array<Maybe<HodlOfferType>>>;
getCountries?: Maybe<Array<Maybe<HodlCountryType>>>;
getCurrencies?: Maybe<Array<Maybe<HodlCurrencyType>>>;
getMessages?: Maybe<GetMessagesType>;
};
export type QueryGetChannelBalanceArgs = {
@ -218,6 +219,14 @@ export type QueryGetOffersArgs = {
filter?: Maybe<Scalars['String']>;
};
export type QueryGetMessagesArgs = {
auth: AuthType;
logger?: Maybe<Scalars['Boolean']>;
token?: Maybe<Scalars['String']>;
initialize?: Maybe<Scalars['Boolean']>;
lastMessage?: Maybe<Scalars['String']>;
};
export type ChannelBalanceType = {
__typename?: 'channelBalanceType';
confirmedBalance?: Maybe<Scalars['Int']>;
@ -312,6 +321,7 @@ export type ChannelFeeType = {
feeRate?: Maybe<Scalars['Int']>;
transactionId?: Maybe<Scalars['String']>;
transactionVout?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
};
export type ChannelReportType = {
@ -528,6 +538,24 @@ export type HodlCurrencyType = {
type?: Maybe<Scalars['String']>;
};
export type GetMessagesType = {
__typename?: 'getMessagesType';
token?: Maybe<Scalars['String']>;
messages?: Maybe<Array<Maybe<MessagesType>>>;
};
export type MessagesType = {
__typename?: 'messagesType';
date?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['String']>;
verified?: Maybe<Scalars['Boolean']>;
contentType?: Maybe<Scalars['String']>;
sender?: Maybe<Scalars['String']>;
alias?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
};
export type Mutation = {
__typename?: 'Mutation';
closeChannel?: Maybe<CloseChannelType>;
@ -541,6 +569,7 @@ export type Mutation = {
sendToAddress?: Maybe<SendToType>;
addPeer?: Maybe<Scalars['Boolean']>;
removePeer?: Maybe<Scalars['Boolean']>;
sendMessage?: Maybe<Scalars['Int']>;
};
export type MutationCloseChannelArgs = {
@ -625,6 +654,16 @@ export type MutationRemovePeerArgs = {
publicKey: Scalars['String'];
};
export type MutationSendMessageArgs = {
auth: AuthType;
logger?: Maybe<Scalars['Boolean']>;
publicKey: Scalars['String'];
message: Scalars['String'];
messageType?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
maxFee?: Maybe<Scalars['Int']>;
};
export type CloseChannelType = {
__typename?: 'closeChannelType';
transactionId?: Maybe<Scalars['String']>;
@ -936,6 +975,20 @@ export type AddPeerMutation = { __typename?: 'Mutation' } & Pick<
'addPeer'
>;
export type SendMessageMutationVariables = {
auth: AuthType;
publicKey: Scalars['String'];
message: Scalars['String'];
messageType?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
maxFee?: Maybe<Scalars['Int']>;
};
export type SendMessageMutation = { __typename?: 'Mutation' } & Pick<
Mutation,
'sendMessage'
>;
export type GetNetworkInfoQueryVariables = {
auth: AuthType;
};
@ -1412,6 +1465,7 @@ export type ChannelFeesQuery = { __typename?: 'Query' } & {
| 'feeRate'
| 'transactionId'
| 'transactionVout'
| 'public_key'
>
>
>
@ -1486,6 +1540,36 @@ export type GetUtxosQuery = { __typename?: 'Query' } & {
>;
};
export type GetMessagesQueryVariables = {
auth: AuthType;
initialize?: Maybe<Scalars['Boolean']>;
lastMessage?: Maybe<Scalars['String']>;
};
export type GetMessagesQuery = { __typename?: 'Query' } & {
getMessages?: Maybe<
{ __typename?: 'getMessagesType' } & Pick<GetMessagesType, 'token'> & {
messages?: Maybe<
Array<
Maybe<
{ __typename?: 'messagesType' } & Pick<
MessagesType,
| 'date'
| 'contentType'
| 'alias'
| 'message'
| 'id'
| 'sender'
| 'verified'
| 'tokens'
>
>
>
>;
}
>;
};
export const GetCountriesDocument = gql`
query GetCountries {
getCountries {
@ -2276,6 +2360,73 @@ export type AddPeerMutationOptions = ApolloReactCommon.BaseMutationOptions<
AddPeerMutation,
AddPeerMutationVariables
>;
export const SendMessageDocument = gql`
mutation SendMessage(
$auth: authType!
$publicKey: String!
$message: String!
$messageType: String
$tokens: Int
$maxFee: Int
) {
sendMessage(
auth: $auth
publicKey: $publicKey
message: $message
messageType: $messageType
tokens: $tokens
maxFee: $maxFee
)
}
`;
export type SendMessageMutationFn = ApolloReactCommon.MutationFunction<
SendMessageMutation,
SendMessageMutationVariables
>;
/**
* __useSendMessageMutation__
*
* To run a mutation, you first call `useSendMessageMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSendMessageMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [sendMessageMutation, { data, loading, error }] = useSendMessageMutation({
* variables: {
* auth: // value for 'auth'
* publicKey: // value for 'publicKey'
* message: // value for 'message'
* messageType: // value for 'messageType'
* tokens: // value for 'tokens'
* maxFee: // value for 'maxFee'
* },
* });
*/
export function useSendMessageMutation(
baseOptions?: ApolloReactHooks.MutationHookOptions<
SendMessageMutation,
SendMessageMutationVariables
>
) {
return ApolloReactHooks.useMutation<
SendMessageMutation,
SendMessageMutationVariables
>(SendMessageDocument, baseOptions);
}
export type SendMessageMutationHookResult = ReturnType<
typeof useSendMessageMutation
>;
export type SendMessageMutationResult = ApolloReactCommon.MutationResult<
SendMessageMutation
>;
export type SendMessageMutationOptions = ApolloReactCommon.BaseMutationOptions<
SendMessageMutation,
SendMessageMutationVariables
>;
export const GetNetworkInfoDocument = gql`
query GetNetworkInfo($auth: authType!) {
getNetworkInfo(auth: $auth) {
@ -3830,6 +3981,7 @@ export const ChannelFeesDocument = gql`
feeRate
transactionId
transactionVout
public_key
}
}
`;
@ -4077,3 +4229,77 @@ export type GetUtxosQueryResult = ApolloReactCommon.QueryResult<
GetUtxosQuery,
GetUtxosQueryVariables
>;
export const GetMessagesDocument = gql`
query GetMessages(
$auth: authType!
$initialize: Boolean
$lastMessage: String
) {
getMessages(
auth: $auth
initialize: $initialize
lastMessage: $lastMessage
) {
token
messages {
date
contentType
alias
message
id
sender
verified
tokens
}
}
}
`;
/**
* __useGetMessagesQuery__
*
* To run a query within a React component, call `useGetMessagesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetMessagesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetMessagesQuery({
* variables: {
* auth: // value for 'auth'
* initialize: // value for 'initialize'
* lastMessage: // value for 'lastMessage'
* },
* });
*/
export function useGetMessagesQuery(
baseOptions?: ApolloReactHooks.QueryHookOptions<
GetMessagesQuery,
GetMessagesQueryVariables
>
) {
return ApolloReactHooks.useQuery<GetMessagesQuery, GetMessagesQueryVariables>(
GetMessagesDocument,
baseOptions
);
}
export function useGetMessagesLazyQuery(
baseOptions?: ApolloReactHooks.LazyQueryHookOptions<
GetMessagesQuery,
GetMessagesQueryVariables
>
) {
return ApolloReactHooks.useLazyQuery<
GetMessagesQuery,
GetMessagesQueryVariables
>(GetMessagesDocument, baseOptions);
}
export type GetMessagesQueryHookResult = ReturnType<typeof useGetMessagesQuery>;
export type GetMessagesLazyQueryHookResult = ReturnType<
typeof useGetMessagesLazyQuery
>;
export type GetMessagesQueryResult = ApolloReactCommon.QueryResult<
GetMessagesQuery,
GetMessagesQueryVariables
>;

View file

@ -135,3 +135,23 @@ export const ADD_PEER = gql`
)
}
`;
export const SEND_MESSAGE = gql`
mutation SendMessage(
$auth: authType!
$publicKey: String!
$message: String!
$messageType: String
$tokens: Int
$maxFee: Int
) {
sendMessage(
auth: $auth
publicKey: $publicKey
message: $message
messageType: $messageType
tokens: $tokens
maxFee: $maxFee
)
}
`;

Some files were not shown because too many files have changed in this diff Show more