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
|
@ -10,6 +10,4 @@
|
|||
.env
|
||||
.vscode
|
||||
.storybook
|
||||
CHANGELOG.md
|
||||
.elasticbeanstalk/*
|
||||
.ebextensions/*
|
||||
CHANGELOG.md
|
8
.gitignore
vendored
|
@ -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
|
32
Dockerfile
|
@ -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
|
@ -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',
|
||||
},
|
||||
};
|
15
package.json
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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;
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom/extend-expect';
|
93
src/api/helpers/customRecords.ts
Normal 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 {};
|
||||
}
|
||||
};
|
|
@ -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';
|
||||
};
|
||||
|
|
5
src/api/schemas/mutations/chat/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { sendMessage } from '../chat/sendMessage';
|
||||
|
||||
export const chat = {
|
||||
sendMessage,
|
||||
};
|
109
src/api/schemas/mutations/chat/sendMessage.ts
Normal 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;
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -71,6 +71,7 @@ export const getChannelFees = {
|
|||
feeRate: fee_rate,
|
||||
transactionId: transaction_id,
|
||||
transactionVout: transaction_vout,
|
||||
public_key: channel.partner_public_key,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
102
src/api/schemas/query/chat/getMessages.ts
Normal 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 };
|
||||
},
|
||||
};
|
5
src/api/schemas/query/chat/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { getMessages } from './getMessages';
|
||||
|
||||
export const chatQueries = {
|
||||
getMessages,
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -34,7 +34,7 @@ export const getRoutes = {
|
|||
});
|
||||
|
||||
if (!route) {
|
||||
throw new Error('No route found.');
|
||||
throw new Error('NoRouteFound');
|
||||
}
|
||||
|
||||
return JSON.stringify(route);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 },
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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' },
|
||||
};
|
||||
|
|
Before Width: | Height: | Size: 438 B After Width: | Height: | Size: 438 B |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 (
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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('/'));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
49
src/components/buttons/secureButton/SecureWrapper.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
64
src/components/chat/ChatFetcher.tsx
Normal 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;
|
||||
};
|
70
src/components/chat/ChatInit.tsx
Normal 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;
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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} />);
|
||||
|
|
|
@ -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
|
@ -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 };
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
>;
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
|