mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-02-22 06:21:37 +01:00
chore: transaction filters
This commit is contained in:
parent
33223183e1
commit
7e721e8439
10 changed files with 249 additions and 39 deletions
|
@ -48,22 +48,18 @@ function createApolloClient(context?: ResolverContext) {
|
|||
fields: {
|
||||
getResume: {
|
||||
keyArgs: [],
|
||||
merge(existing, incoming, { args }) {
|
||||
if (!existing) {
|
||||
return incoming;
|
||||
}
|
||||
merge(existing, incoming) {
|
||||
if (!existing) return incoming;
|
||||
|
||||
const { offset } = args || {};
|
||||
|
||||
const merged = existing?.resume ? existing.resume.slice(0) : [];
|
||||
for (let i = 0; i < incoming.resume.length; ++i) {
|
||||
merged[offset + i] = incoming.resume[i];
|
||||
}
|
||||
const current = existing?.resume ? [...existing.resume] : [];
|
||||
const newIncoming = incoming?.resume
|
||||
? [...incoming.resume]
|
||||
: [];
|
||||
|
||||
return {
|
||||
...existing,
|
||||
offset: incoming.offset,
|
||||
resume: merged,
|
||||
resume: [...current, ...newIncoming],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,29 +1,42 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { InvoiceCard } from 'src/views/transactions/InvoiceCard';
|
||||
import { useGetResumeQuery } from 'src/graphql/queries/__generated__/getResume.generated';
|
||||
import {
|
||||
GetResumeQuery,
|
||||
useGetResumeQuery,
|
||||
} from 'src/graphql/queries/__generated__/getResume.generated';
|
||||
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
|
||||
|
||||
import { NextPageContext } from 'next';
|
||||
import { getProps } from 'src/utils/ssr';
|
||||
import { RefreshCw } from 'react-feather';
|
||||
import { RefreshCw, Settings } from 'react-feather';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
Card,
|
||||
CardWithTitle,
|
||||
SubTitle,
|
||||
SingleLine,
|
||||
DarkSubTitle,
|
||||
} from '../src/components/generic/Styled';
|
||||
import { getErrorContent } from '../src/utils/error';
|
||||
import { PaymentsCard } from '../src/views/transactions/PaymentsCards';
|
||||
import { LoadingCard } from '../src/components/loading/LoadingCard';
|
||||
import { ColorButton } from '../src/components/buttons/colorButton/ColorButton';
|
||||
import { FlowBox } from '../src/views/home/reports/flow';
|
||||
import { useLocalStorage } from 'src/hooks/UseLocalStorage';
|
||||
import { useNodeInfo } from 'src/hooks/UseNodeInfo';
|
||||
import {
|
||||
defaultSettings,
|
||||
TransactionSettings,
|
||||
} from 'src/views/transactions/Settings';
|
||||
import { subDays, format } from 'date-fns';
|
||||
|
||||
type RotationProps = {
|
||||
withRotation: boolean;
|
||||
};
|
||||
|
||||
type ResumeTransactions = GetResumeQuery['getResume']['resume'];
|
||||
|
||||
const Rotation = styled.div<RotationProps>`
|
||||
${({ withRotation }) =>
|
||||
withRotation &&
|
||||
|
@ -45,8 +58,14 @@ const TransactionsView = () => {
|
|||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [indexOpen, setIndexOpen] = useState(0);
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const { publicKey } = useNodeInfo();
|
||||
|
||||
const [settings] = useLocalStorage('transactionSettings', defaultSettings);
|
||||
|
||||
const {
|
||||
data,
|
||||
fetchMore,
|
||||
|
@ -79,7 +98,42 @@ const TransactionsView = () => {
|
|||
return <LoadingCard title={'Transactions'} />;
|
||||
}
|
||||
|
||||
const resumeList = data.getResume.resume;
|
||||
const beforeDate = subDays(new Date(), offset);
|
||||
|
||||
const selfInvoices: string[] = data.getResume.resume.reduce((p, c) => {
|
||||
if (!c) return p;
|
||||
if (c.__typename === 'PaymentType') {
|
||||
if (c.destination === publicKey) {
|
||||
return [...p, c.id];
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}, [] as string[]);
|
||||
|
||||
const resumeList = data.getResume.resume?.reduce((p, c) => {
|
||||
const { rebalance, confirmed } = settings;
|
||||
|
||||
if (!c) return p;
|
||||
|
||||
if (rebalance) {
|
||||
if (c.__typename === 'PaymentType') {
|
||||
if (c.destination === publicKey) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
if (selfInvoices.includes(c.id)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmed) {
|
||||
if (!c.is_confirmed) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
return [...p, c];
|
||||
}, [] as ResumeTransactions);
|
||||
|
||||
const handleClick = (limit: number) =>
|
||||
fetchMore({ variables: { offset, limit } });
|
||||
|
@ -89,24 +143,44 @@ const TransactionsView = () => {
|
|||
<FlowBox />
|
||||
<CardWithTitle>
|
||||
<SingleLine>
|
||||
<SubTitle>Transactions</SubTitle>
|
||||
<ColorButton
|
||||
withMargin={'0 0 8px'}
|
||||
onClick={() => {
|
||||
if (isPolling) {
|
||||
setIsPolling(false);
|
||||
stopPolling();
|
||||
} else {
|
||||
setIsPolling(true);
|
||||
startPolling(1000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Rotation withRotation={isPolling}>
|
||||
<RefreshCw size={18} />
|
||||
</Rotation>
|
||||
</ColorButton>
|
||||
<SubTitle>
|
||||
Transactions
|
||||
<DarkSubTitle fontSize={'12px'}>
|
||||
{`${format(beforeDate, 'dd/MM/yy')} - Today`}
|
||||
</DarkSubTitle>
|
||||
</SubTitle>
|
||||
<SingleLine>
|
||||
<ColorButton
|
||||
withMargin={'0 0 8px 8px'}
|
||||
onClick={() => {
|
||||
setOpen(p => !p);
|
||||
}}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</ColorButton>
|
||||
<ColorButton
|
||||
withMargin={'0 0 8px'}
|
||||
onClick={() => {
|
||||
if (isPolling) {
|
||||
setIsPolling(false);
|
||||
stopPolling();
|
||||
} else {
|
||||
setIsPolling(true);
|
||||
startPolling(1000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Rotation withRotation={isPolling}>
|
||||
<RefreshCw size={18} />
|
||||
</Rotation>
|
||||
</ColorButton>
|
||||
</SingleLine>
|
||||
</SingleLine>
|
||||
{open && (
|
||||
<Card>
|
||||
<TransactionSettings />
|
||||
</Card>
|
||||
)}
|
||||
<Card bottom={'8px'} mobileCardPadding={'0'} mobileNoBackground={true}>
|
||||
{resumeList?.map((entry, index: number) => {
|
||||
if (!entry) {
|
||||
|
|
|
@ -78,6 +78,6 @@ export const transactionTypes = gql`
|
|||
|
||||
type getResumeType {
|
||||
offset: Int
|
||||
resume: [Transaction]
|
||||
resume: [Transaction]!
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -62,7 +62,7 @@ export const queryTypes = gql`
|
|||
getNode(publicKey: String!, withoutChannels: Boolean): Node!
|
||||
decodeRequest(request: String!): decodeType
|
||||
getWalletInfo: walletInfoType
|
||||
getResume(offset: Int, limit: Int): getResumeType
|
||||
getResume(offset: Int, limit: Int): getResumeType!
|
||||
getForwards(days: Int!): [Forward]!
|
||||
getBitcoinPrice(logger: Boolean, currency: String): String
|
||||
getBitcoinFees(logger: Boolean): bitcoinFeeType
|
||||
|
|
|
@ -47,6 +47,7 @@ type InputWithDecoProps = {
|
|||
placeholder?: string;
|
||||
inputType?: string;
|
||||
inputCallback?: (value: string) => void;
|
||||
blurCallback?: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEnter?: () => void;
|
||||
};
|
||||
|
@ -64,6 +65,7 @@ export const InputWithDeco: React.FC<InputWithDecoProps> = ({
|
|||
inputMaxWidth,
|
||||
inputType = 'text',
|
||||
inputCallback,
|
||||
blurCallback,
|
||||
onKeyDown,
|
||||
onEnter,
|
||||
}) => {
|
||||
|
@ -100,6 +102,7 @@ export const InputWithDeco: React.FC<InputWithDecoProps> = ({
|
|||
mobileMargin={'0'}
|
||||
type={inputType}
|
||||
onChange={e => inputCallback && inputCallback(e.target.value)}
|
||||
onBlur={e => blurCallback && blurCallback(e.target.value)}
|
||||
{...onKeyDownProp}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -83,6 +83,7 @@ interface InputCompProps {
|
|||
mobileFullWidth?: boolean;
|
||||
maxWidth?: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onBlur?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEnter?: () => void;
|
||||
}
|
||||
|
@ -99,6 +100,7 @@ export const Input = ({
|
|||
fullWidth = true,
|
||||
maxWidth,
|
||||
onChange,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
onEnter,
|
||||
}: InputCompProps) => {
|
||||
|
@ -112,6 +114,7 @@ export const Input = ({
|
|||
withMargin={withMargin}
|
||||
mobileMargin={mobileMargin}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
fullWidth={fullWidth}
|
||||
mobileFullWidth={mobileFullWidth}
|
||||
maxWidth={maxWidth}
|
||||
|
|
|
@ -12,10 +12,10 @@ export type GetResumeQueryVariables = Types.Exact<{
|
|||
|
||||
export type GetResumeQuery = (
|
||||
{ __typename?: 'Query' }
|
||||
& { getResume?: Types.Maybe<(
|
||||
& { getResume: (
|
||||
{ __typename?: 'getResumeType' }
|
||||
& Pick<Types.GetResumeType, 'offset'>
|
||||
& { resume?: Types.Maybe<Array<Types.Maybe<(
|
||||
& { resume: Array<Types.Maybe<(
|
||||
{ __typename?: 'InvoiceType' }
|
||||
& Pick<Types.InvoiceType, 'chain_address' | 'confirmed_at' | 'created_at' | 'description' | 'description_hash' | 'expires_at' | 'id' | 'is_canceled' | 'is_confirmed' | 'is_held' | 'is_private' | 'is_push' | 'received' | 'received_mtokens' | 'request' | 'secret' | 'tokens' | 'type' | 'date'>
|
||||
& { payments: Array<Types.Maybe<(
|
||||
|
@ -42,8 +42,8 @@ export type GetResumeQuery = (
|
|||
& Pick<Types.NodeType, 'alias' | 'public_key'>
|
||||
) }
|
||||
)> }
|
||||
)>>> }
|
||||
)> }
|
||||
)>> }
|
||||
) }
|
||||
);
|
||||
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ export type Query = {
|
|||
getNode: Node;
|
||||
decodeRequest?: Maybe<DecodeType>;
|
||||
getWalletInfo?: Maybe<WalletInfoType>;
|
||||
getResume?: Maybe<GetResumeType>;
|
||||
getResume: GetResumeType;
|
||||
getForwards: Array<Maybe<Forward>>;
|
||||
getBitcoinPrice?: Maybe<Scalars['String']>;
|
||||
getBitcoinFees?: Maybe<BitcoinFeeType>;
|
||||
|
@ -914,7 +914,7 @@ export type Transaction = InvoiceType | PaymentType;
|
|||
export type GetResumeType = {
|
||||
__typename?: 'getResumeType';
|
||||
offset?: Maybe<Scalars['Int']>;
|
||||
resume?: Maybe<Array<Maybe<Transaction>>>;
|
||||
resume: Array<Maybe<Transaction>>;
|
||||
};
|
||||
|
||||
export type ChannelHealth = {
|
||||
|
|
67
src/hooks/UseLocalStorage.tsx
Normal file
67
src/hooks/UseLocalStorage.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useLocalStorage = <T,>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T) => void] => {
|
||||
const readValue = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return initialValue;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
console.warn(`Error reading localStorage key “${key}”:`, error);
|
||||
return initialValue;
|
||||
}
|
||||
};
|
||||
|
||||
const [storedValue, setStoredValue] = useState<T>(readValue);
|
||||
|
||||
const setValue = (value: T) => {
|
||||
if (typeof window == 'undefined') {
|
||||
console.warn(
|
||||
`Tried setting localStorage key “${key}” even though environment is not a client`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const newValue = value instanceof Function ? value(storedValue) : value;
|
||||
window.localStorage.setItem(key, JSON.stringify(newValue));
|
||||
|
||||
setStoredValue(newValue);
|
||||
|
||||
// We dispatch a custom event so every useLocalStorage hook are notified
|
||||
window.dispatchEvent(new Event('local-storage'));
|
||||
} catch (error) {
|
||||
console.warn(`Error setting localStorage key “${key}”:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setStoredValue(readValue());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorageChange = () => {
|
||||
setStoredValue(readValue());
|
||||
};
|
||||
|
||||
// this only works for other documents, not the current one
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
// this is a custom event, triggered in writeValueToLocalStorage
|
||||
window.addEventListener('local-storage', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
window.removeEventListener('local-storage', handleStorageChange);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return [storedValue, setValue];
|
||||
};
|
67
src/views/transactions/Settings.tsx
Normal file
67
src/views/transactions/Settings.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
MultiButton,
|
||||
SingleButton,
|
||||
} from 'src/components/buttons/multiButton/MultiButton';
|
||||
import { SingleLine } from 'src/components/generic/Styled';
|
||||
import { useLocalStorage } from 'src/hooks/UseLocalStorage';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const NoWrapText = styled.div`
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const InputTitle = styled(NoWrapText)``;
|
||||
|
||||
export const defaultSettings = {
|
||||
rebalance: false,
|
||||
confirmed: true,
|
||||
};
|
||||
|
||||
export const TransactionSettings = () => {
|
||||
const [settings, setSettings] = useLocalStorage(
|
||||
'transactionSettings',
|
||||
defaultSettings
|
||||
);
|
||||
|
||||
const { rebalance, confirmed } = settings;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SingleLine>
|
||||
<InputTitle>Confirmed</InputTitle>
|
||||
<MultiButton>
|
||||
<SingleButton
|
||||
selected={confirmed}
|
||||
onClick={() => setSettings({ ...settings, confirmed: true })}
|
||||
>
|
||||
Yes
|
||||
</SingleButton>
|
||||
<SingleButton
|
||||
selected={!confirmed}
|
||||
onClick={() => setSettings({ ...settings, confirmed: false })}
|
||||
>
|
||||
No
|
||||
</SingleButton>
|
||||
</MultiButton>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<InputTitle>Circular Payment</InputTitle>
|
||||
<MultiButton margin={'8px 0'}>
|
||||
<SingleButton
|
||||
selected={rebalance}
|
||||
onClick={() => setSettings({ ...settings, rebalance: true })}
|
||||
>
|
||||
Yes
|
||||
</SingleButton>
|
||||
<SingleButton
|
||||
selected={!rebalance}
|
||||
onClick={() => setSettings({ ...settings, rebalance: false })}
|
||||
>
|
||||
No
|
||||
</SingleButton>
|
||||
</MultiButton>
|
||||
</SingleLine>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Add table
Reference in a new issue