Fix more types
This commit is contained in:
parent
accfe1f4c2
commit
9019f0432c
|
@ -44,7 +44,9 @@ export function pollOfflineStatus(): interfaces.PollOfflineStatus {
|
|||
}
|
||||
|
||||
export type TChangeNodeIntent = typeof changeNodeIntent;
|
||||
export function changeNodeIntent(payload: string): interfaces.ChangeNodeIntentAction {
|
||||
export function changeNodeIntent(
|
||||
payload: interfaces.ChangeNodeIntentAction['payload']
|
||||
): interfaces.ChangeNodeIntentAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_NODE_CHANGE_INTENT,
|
||||
payload
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { TypeKeys } from './constants';
|
||||
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config';
|
||||
import {
|
||||
NodeConfig,
|
||||
CustomNodeConfig,
|
||||
NetworkConfig,
|
||||
CustomNetworkConfig,
|
||||
NodeConfigs
|
||||
} from 'config';
|
||||
|
||||
/*** Toggle Offline ***/
|
||||
export interface ToggleOfflineAction {
|
||||
|
@ -35,7 +41,7 @@ export interface PollOfflineStatus {
|
|||
/*** Change Node ***/
|
||||
export interface ChangeNodeIntentAction {
|
||||
type: TypeKeys.CONFIG_NODE_CHANGE_INTENT;
|
||||
payload: string;
|
||||
payload: keyof NodeConfigs | string;
|
||||
}
|
||||
|
||||
/*** Add Custom Node ***/
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { AmountFieldFactory } from './AmountFieldFactory';
|
||||
import { UnitDropDown } from 'components';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
interface Props {
|
||||
hasUnitDropdown?: boolean;
|
||||
|
@ -36,5 +37,10 @@ export const AmountField: React.SFC<Props> = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const isAmountValid = (raw, customValidator, isValid) =>
|
||||
customValidator ? customValidator(raw) : isValid;
|
||||
const isAmountValid = (
|
||||
raw: (
|
||||
| AppState['transaction']['fields']['value']
|
||||
| AppState['transaction']['meta']['tokenValue'])['raw'],
|
||||
customValidator: ((rawAmount: string) => boolean) | undefined,
|
||||
isValid: boolean
|
||||
) => (customValidator ? customValidator(raw) : isValid);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { rateSymbols, TFetchCCRates } from 'actions/rates';
|
|||
import { TokenBalance } from 'selectors/wallet';
|
||||
import { Balance } from 'libs/wallet';
|
||||
import { NetworkConfig } from 'config';
|
||||
import { ETH_DECIMAL, convertTokenBase } from 'libs/units';
|
||||
import { ETH_DECIMAL, convertTokenBase, Wei, TokenValue } from 'libs/units';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import UnitDisplay from 'components/ui/UnitDisplay';
|
||||
import './EquivalentValues.scss';
|
||||
|
|
|
@ -15,10 +15,15 @@ interface Props {
|
|||
onRemoveCustomToken(symbol: string): any;
|
||||
}
|
||||
|
||||
interface TrackedTokens {
|
||||
[symbol: string]: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
trackedTokens: { [symbol: string]: boolean };
|
||||
trackedTokens: TrackedTokens;
|
||||
showCustomTokenForm: boolean;
|
||||
}
|
||||
|
||||
export default class TokenBalances extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
trackedTokens: {},
|
||||
|
@ -27,7 +32,7 @@ export default class TokenBalances extends React.Component<Props, State> {
|
|||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (nextProps.tokenBalances !== this.props.tokenBalances) {
|
||||
const trackedTokens = nextProps.tokenBalances.reduce((prev, t) => {
|
||||
const trackedTokens = nextProps.tokenBalances.reduce<TrackedTokens>((prev, t) => {
|
||||
prev[t.symbol] = !t.balance.isZero();
|
||||
return prev;
|
||||
}, {});
|
||||
|
|
|
@ -13,7 +13,7 @@ interface StateProps {
|
|||
}
|
||||
|
||||
interface OwnProps {
|
||||
withProps(props: CallBackProps);
|
||||
withProps(props: CallBackProps): React.ReactElement<any> | null;
|
||||
onChange(value: React.FormEvent<HTMLInputElement>): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ import {
|
|||
VERSION,
|
||||
NodeConfig,
|
||||
CustomNodeConfig,
|
||||
CustomNetworkConfig
|
||||
CustomNetworkConfig,
|
||||
NodeConfigs
|
||||
} from 'config';
|
||||
import GasPriceDropdown from './components/GasPriceDropdown';
|
||||
import Navigation from './components/Navigation';
|
||||
|
@ -73,9 +74,11 @@ export default class Header extends Component<Props, State> {
|
|||
const selectedNetwork = getNetworkConfigFromId(node.network, customNetworks);
|
||||
const LanguageDropDown = Dropdown as new () => Dropdown<typeof selectedLanguage>;
|
||||
|
||||
const nodeOptions = Object.keys(NODES)
|
||||
.map(key => {
|
||||
const defaultNodeOptions = Object.keys(NODES).map((key: keyof NodeConfigs) => {
|
||||
const n = NODES[key];
|
||||
if (!n) {
|
||||
throw Error(`Node ${key} not found`);
|
||||
}
|
||||
const network = getNetworkConfigFromId(n.network, customNetworks);
|
||||
return {
|
||||
value: key,
|
||||
|
@ -87,9 +90,8 @@ export default class Header extends Component<Props, State> {
|
|||
color: network && network.color,
|
||||
hidden: n.hidden
|
||||
};
|
||||
})
|
||||
.concat(
|
||||
customNodes.map(cn => {
|
||||
});
|
||||
const customNodeOptions = customNodes.map(cn => {
|
||||
const network = getNetworkConfigFromId(cn.network, customNetworks);
|
||||
return {
|
||||
value: makeCustomNodeId(cn),
|
||||
|
@ -102,8 +104,15 @@ export default class Header extends Component<Props, State> {
|
|||
hidden: false,
|
||||
onRemove: () => this.props.removeCustomNode(cn)
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
type DefaultNodeOption = typeof defaultNodeOptions[number];
|
||||
type CustomNodeOption = typeof customNodeOptions[number];
|
||||
|
||||
const nodeOptions: (DefaultNodeOption | CustomNodeOption)[] = [
|
||||
...defaultNodeOptions,
|
||||
...customNodeOptions
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="Header">
|
||||
|
|
|
@ -18,7 +18,7 @@ interface State {
|
|||
}
|
||||
|
||||
class LogOutPromptClass extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
nextLocation: null,
|
||||
|
|
|
@ -113,6 +113,7 @@ const WEB3_TYPES = {
|
|||
}
|
||||
};
|
||||
|
||||
type Web3Keys = keyof typeof WEB3_TYPES;
|
||||
type SecureWallets = { [key in SecureWalletName]: SecureWalletInfo };
|
||||
type InsecureWallets = { [key in InsecureWalletName]: InsecureWalletInfo };
|
||||
type MiscWallet = { [key in MiscWalletName]: MiscWalletInfo };
|
||||
|
@ -126,8 +127,8 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
// index signature should become [key: Wallets] (from config) once typescript bug is fixed
|
||||
public WALLETS: Wallets = {
|
||||
[SecureWalletName.WEB3]: {
|
||||
lid: WEB3_TYPE ? WEB3_TYPES[WEB3_TYPE].lid : 'x_Web3',
|
||||
icon: WEB3_TYPE && WEB3_TYPES[WEB3_TYPE].icon,
|
||||
lid: WEB3_TYPE ? WEB3_TYPES[WEB3_TYPE as Web3Keys].lid : 'x_Web3',
|
||||
icon: WEB3_TYPE && WEB3_TYPES[WEB3_TYPE as Web3Keys].icon,
|
||||
description: 'ADD_Web3Desc',
|
||||
component: Web3Decrypt,
|
||||
initialParams: {},
|
||||
|
|
|
@ -215,7 +215,7 @@ export const NETWORKS = {
|
|||
|
||||
export type NetworkKeys = keyof typeof NETWORKS;
|
||||
|
||||
enum NodeName {
|
||||
export enum NodeName {
|
||||
ETH_MEW = 'eth_mew',
|
||||
ETH_ETHSCAN = 'eth_ethscan',
|
||||
ETH_INFURA = 'eth_infura',
|
||||
|
@ -235,7 +235,7 @@ interface Web3NodeConfig {
|
|||
web3?: NodeConfig;
|
||||
}
|
||||
|
||||
type NodeConfigs = NonWeb3NodeConfigs & Web3NodeConfig;
|
||||
export type NodeConfigs = NonWeb3NodeConfigs & Web3NodeConfig;
|
||||
|
||||
export const NODES: NodeConfigs = {
|
||||
eth_mew: {
|
||||
|
|
|
@ -9,7 +9,10 @@ import classnames from 'classnames';
|
|||
|
||||
interface Props {
|
||||
contracts: NetworkContract[];
|
||||
accessContract(abiJson: string, address: string): (ev) => void;
|
||||
accessContract(
|
||||
abiJson: string,
|
||||
address: string
|
||||
): (ev: React.FormEvent<HTMLButtonElement>) => void;
|
||||
resetState(): void;
|
||||
}
|
||||
|
||||
|
@ -127,9 +130,11 @@ e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]';
|
|||
);
|
||||
}
|
||||
|
||||
private handleInput = name => (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
private handleInput = (name: keyof State) => (
|
||||
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
this.props.resetState();
|
||||
this.setState({ [name]: ev.currentTarget.value });
|
||||
this.setState({ [name as any]: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private handleSelectContract = (ev: React.FormEvent<HTMLSelectElement>) => {
|
||||
|
|
|
@ -21,6 +21,7 @@ import { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values';
|
|||
import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config';
|
||||
import './RequestPayment.scss';
|
||||
import { reset, TReset, setCurrentTo, TSetCurrentTo } from 'actions/transaction';
|
||||
import { isEtherUnit } from 'libs/units';
|
||||
|
||||
interface OwnProps {
|
||||
wallet: AppState['wallet']['inst'];
|
||||
|
@ -43,7 +44,8 @@ interface ActionProps {
|
|||
|
||||
type Props = OwnProps & StateProps & ActionProps;
|
||||
|
||||
const isValidAmount = decimal => amount => validNumber(+amount) && validDecimal(amount, decimal);
|
||||
const isValidAmount = (decimal: number) => (amount: string) =>
|
||||
validNumber(+amount) && validDecimal(amount, decimal);
|
||||
|
||||
class RequestPayment extends React.Component<Props, {}> {
|
||||
public state = {
|
||||
|
@ -139,7 +141,9 @@ class RequestPayment extends React.Component<Props, {}> {
|
|||
private generateEIP681String(
|
||||
currentTo: string,
|
||||
tokenContractAddress: string,
|
||||
currentValue,
|
||||
currentValue:
|
||||
| AppState['transaction']['fields']['value']
|
||||
| AppState['transaction']['meta']['tokenTo'],
|
||||
gasLimit: { raw: string; value: BN | null },
|
||||
unit: string,
|
||||
decimal: number,
|
||||
|
@ -155,8 +159,12 @@ class RequestPayment extends React.Component<Props, {}> {
|
|||
) {
|
||||
return '';
|
||||
}
|
||||
const currentValueIsEther = (
|
||||
u: string,
|
||||
_: AppState['transaction']['fields']['value'] | AppState['transaction']['meta']['tokenTo']
|
||||
): _ is AppState['transaction']['fields']['value'] => isEtherUnit(u);
|
||||
|
||||
if (unit === 'ether') {
|
||||
if (currentValueIsEther(unit, currentValue)) {
|
||||
return buildEIP681EtherRequest(currentTo, chainId, currentValue);
|
||||
} else {
|
||||
return buildEIP681TokenRequest(
|
||||
|
|
|
@ -37,7 +37,7 @@ export function decodeCryptojsSalt(input: string): any {
|
|||
export function evp_kdf(data: Buffer, salt: Buffer, opts: any) {
|
||||
// A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)`
|
||||
|
||||
function iter(block) {
|
||||
function iter(block: Buffer) {
|
||||
let hash = createHash(opts.digest || 'md5');
|
||||
hash.update(block);
|
||||
hash.update(data);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import uts46 from 'idna-uts46';
|
||||
import ethUtil from 'ethereumjs-util';
|
||||
|
||||
export function normalise(name: string) {
|
||||
export function normalise(name: string): string {
|
||||
try {
|
||||
return uts46.toUnicode(name, { useStd3ASCII: true, transitional: false });
|
||||
} catch (e) {
|
||||
|
@ -65,7 +65,7 @@ export enum NameState {
|
|||
NotYetAvailable = '5'
|
||||
}
|
||||
|
||||
export const modeStrMap = name => [
|
||||
export const modeStrMap = (name: string) => [
|
||||
`${name} is available and the auction hasn’t started`,
|
||||
`${name} is available and the auction has been started`,
|
||||
`${name} is taken and currently owned by someone`,
|
||||
|
|
|
@ -6,7 +6,7 @@ export interface TxObj {
|
|||
to: string;
|
||||
data: string;
|
||||
}
|
||||
interface TokenBalanceResult {
|
||||
export interface TokenBalanceResult {
|
||||
balance: TokenValue;
|
||||
error: string | null;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Wei, toTokenBase } from 'libs/units';
|
|||
import { addHexPrefix } from 'ethereumjs-util';
|
||||
import BN from 'bn.js';
|
||||
import { NetworkKeys } from 'config';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
export function stripHexPrefix(value: string) {
|
||||
return value.replace('0x', '');
|
||||
|
@ -42,16 +43,16 @@ export function networkIdToName(networkId: string | number): NetworkKeys {
|
|||
export const buildEIP681EtherRequest = (
|
||||
recipientAddr: string,
|
||||
chainId: number,
|
||||
etherValue: { raw: string; value: Wei | '' }
|
||||
etherValue: AppState['transaction']['fields']['value']
|
||||
) => `ethereum:${recipientAddr}${chainId !== 1 ? `@${chainId}` : ''}?value=${etherValue.raw}e18`;
|
||||
|
||||
export const buildEIP681TokenRequest = (
|
||||
recipientAddr: string,
|
||||
contractAddr: string,
|
||||
chainId: number,
|
||||
tokenValue: { raw: string; value: Wei | '' },
|
||||
tokenValue: AppState['transaction']['meta']['tokenTo'],
|
||||
decimal: number,
|
||||
gasLimit: { raw: string; value: BN | null }
|
||||
gasLimit: AppState['transaction']['fields']['gasLimit']
|
||||
) =>
|
||||
`ethereum:${contractAddr}${
|
||||
chainId !== 1 ? `@${chainId}` : ''
|
||||
|
|
|
@ -16,7 +16,8 @@ import {
|
|||
NodeConfig,
|
||||
CustomNodeConfig,
|
||||
CustomNetworkConfig,
|
||||
Web3Service
|
||||
Web3Service,
|
||||
NodeConfigs
|
||||
} from 'config';
|
||||
import {
|
||||
makeCustomNodeId,
|
||||
|
@ -24,6 +25,7 @@ import {
|
|||
makeNodeConfigFromCustomConfig
|
||||
} from 'utils/node';
|
||||
import { makeCustomNetworkId, getNetworkConfigFromId } from 'utils/network';
|
||||
import { Omit } from 'react-redux';
|
||||
import {
|
||||
getNode,
|
||||
getNodeConfig,
|
||||
|
@ -133,6 +135,7 @@ export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIte
|
|||
}
|
||||
|
||||
let actionConfig = NODES[action.payload];
|
||||
|
||||
if (!actionConfig) {
|
||||
const customConfigs: CustomNodeConfig[] = yield select(getCustomNodeConfigs);
|
||||
const config = getCustomNodeConfigFromId(action.payload, customConfigs);
|
||||
|
@ -246,10 +249,10 @@ export function* unsetWeb3Node(): SagaIterator {
|
|||
yield put(changeNodeIntent(newNode));
|
||||
}
|
||||
|
||||
export const equivalentNodeOrDefault = (nodeConfig: NodeConfig) => {
|
||||
const node = Object.keys(NODES)
|
||||
export const equivalentNodeOrDefault = (nodeConfig: NodeConfig): keyof NodeConfigs => {
|
||||
const node: keyof Omit<NodeConfigs, 'web3'> | '' = Object.keys(NODES)
|
||||
.filter(key => key !== 'web3')
|
||||
.reduce((found, key) => {
|
||||
.reduce<keyof Omit<NodeConfigs, 'web3'> | ''>((found, key: keyof Omit<NodeConfigs, 'web3'>) => {
|
||||
const config = NODES[key];
|
||||
if (found.length) {
|
||||
return found;
|
||||
|
@ -261,7 +264,7 @@ export const equivalentNodeOrDefault = (nodeConfig: NodeConfig) => {
|
|||
}, '');
|
||||
|
||||
// if no equivalent node was found, use the app default
|
||||
return node.length ? node : configInitialState.nodeSelection;
|
||||
return node ? node : (configInitialState.nodeSelection as keyof NodeConfigs);
|
||||
};
|
||||
|
||||
export default function* configSaga(): SagaIterator {
|
||||
|
|
|
@ -8,14 +8,13 @@ import { showNotification } from 'actions/notifications';
|
|||
import { Token } from 'config';
|
||||
import { publicToAddress, toChecksumAddress } from 'ethereumjs-util';
|
||||
import HDKey from 'hdkey';
|
||||
import { INode } from 'libs/nodes/INode';
|
||||
import { INode, TokenBalanceResult } from 'libs/nodes/INode';
|
||||
import { SagaIterator } from 'redux-saga';
|
||||
import { all, apply, fork, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { getNodeLib } from 'selectors/config';
|
||||
import { getDesiredToken, getWallets } from 'selectors/deterministicWallets';
|
||||
import { getTokens } from 'selectors/wallet';
|
||||
import translate from 'translations';
|
||||
import { TokenValue } from 'libs/units';
|
||||
|
||||
export function* getDeterministicWallets(action: GetDeterministicWalletsAction): SagaIterator {
|
||||
const { seed, dPath, publicKey, chainCode, limit, offset } = action.payload;
|
||||
|
@ -96,7 +95,7 @@ export function* updateWalletTokenValues(): SagaIterator {
|
|||
const calls = wallets.map(w => {
|
||||
return apply(node, node.getTokenBalance, [w.address, token]);
|
||||
});
|
||||
const tokenBalances: { balance: TokenValue; error: string | null } = yield all(calls);
|
||||
const tokenBalances: TokenBalanceResult[] = yield all(calls);
|
||||
|
||||
for (let i = 0; i < wallets.length; i++) {
|
||||
if (!tokenBalances[i].error) {
|
||||
|
|
|
@ -2,13 +2,18 @@ import { AppState } from 'reducers';
|
|||
import { ICurrentTo, ICurrentValue } from 'selectors/transaction';
|
||||
import { isEtherUnit } from 'libs/units';
|
||||
|
||||
export const reduceToValues = (transactionFields: AppState['transaction']['fields']) =>
|
||||
Object.keys(transactionFields).reduce(
|
||||
(obj, currFieldName) => {
|
||||
type TransactionFields = AppState['transaction']['fields'];
|
||||
type TransactionFieldValues = {
|
||||
[field in keyof TransactionFields]: TransactionFields[field]['value']
|
||||
};
|
||||
|
||||
export const reduceToValues = (transactionFields: TransactionFields) =>
|
||||
Object.keys(transactionFields).reduce<TransactionFieldValues>(
|
||||
(obj, currFieldName: keyof TransactionFields) => {
|
||||
const currField = transactionFields[currFieldName];
|
||||
return { ...obj, [currFieldName]: currField.value };
|
||||
},
|
||||
{} as any //TODO: Fix types
|
||||
{} as TransactionFieldValues
|
||||
);
|
||||
|
||||
export const isFullTx = (
|
||||
|
|
|
@ -134,7 +134,7 @@ const configureStore = () => {
|
|||
languageSelection: state.config.languageSelection,
|
||||
customNodes: state.config.customNodes,
|
||||
customNetworks: state.config.customNetworks,
|
||||
setGasLimit: state.config.setGasLimit
|
||||
autoGasLimit: state.config.autoGasLimit
|
||||
},
|
||||
transaction: {
|
||||
fields: {
|
||||
|
|
|
@ -41,7 +41,7 @@ export function makeNetworkConfigFromCustomConfig(config: CustomNetworkConfig):
|
|||
}
|
||||
|
||||
export function getNetworkConfigFromId(
|
||||
id: string,
|
||||
id: keyof typeof NETWORKS,
|
||||
configs: CustomNetworkConfig[]
|
||||
): NetworkConfig | undefined {
|
||||
if (NETWORKS[id]) {
|
||||
|
@ -111,7 +111,9 @@ export function isWalletFormatSupportedOnNetwork(
|
|||
}
|
||||
|
||||
export function allWalletFormatsSupportedOnNetwork(network: NetworkConfig): WalletName[] {
|
||||
return walletNames.filter(walletName => isWalletFormatSupportedOnNetwork(walletName, network));
|
||||
return walletNames.filter((walletName: WalletName) =>
|
||||
isWalletFormatSupportedOnNetwork(walletName, network)
|
||||
);
|
||||
}
|
||||
|
||||
export function unSupportedWalletFormatsOnNetwork(network: NetworkConfig): WalletName[] {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CustomNode } from 'libs/nodes';
|
||||
import { NODES, NodeConfig, CustomNodeConfig } from 'config';
|
||||
import { NODES, NodeConfig, CustomNodeConfig, NodeConfigs } from 'config';
|
||||
|
||||
export function makeCustomNodeId(config: CustomNodeConfig): string {
|
||||
return `${config.url}:${config.port}`;
|
||||
|
@ -13,7 +13,7 @@ export function getCustomNodeConfigFromId(
|
|||
}
|
||||
|
||||
export function getNodeConfigFromId(
|
||||
id: string,
|
||||
id: keyof NodeConfigs,
|
||||
configs: CustomNodeConfig[]
|
||||
): NodeConfig | undefined {
|
||||
if (NODES[id]) {
|
||||
|
|
|
@ -6,7 +6,7 @@ export function dedupeCustomTokens(networkTokens: Token[], customTokens: Token[]
|
|||
}
|
||||
|
||||
// If any tokens have the same symbol or contract address, remove them
|
||||
const tokenCollisionMap = networkTokens.reduce((prev, token) => {
|
||||
const tokenCollisionMap = networkTokens.reduce<{ [tokenKey: string]: boolean }>((prev, token) => {
|
||||
prev[token.symbol] = true;
|
||||
prev[token.address] = true;
|
||||
return prev;
|
||||
|
|
Loading…
Reference in New Issue