chore: transaction filters

This commit is contained in:
apotdevin 2021-04-24 12:16:41 +02:00
parent 33223183e1
commit 7e721e8439
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
10 changed files with 249 additions and 39 deletions

View file

@ -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],
};
},
},

View file

@ -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) {

View file

@ -78,6 +78,6 @@ export const transactionTypes = gql`
type getResumeType {
offset: Int
resume: [Transaction]
resume: [Transaction]!
}
`;

View file

@ -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

View file

@ -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}
/>

View file

@ -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}

View file

@ -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'>
) }
)> }
)>>> }
)> }
)>> }
) }
);

View file

@ -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 = {

View 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];
};

View 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>
</>
);
};