mirror of
https://github.com/filecoin-project/slate.git
synced 2024-12-24 17:44:50 +03:00
settings component and experience minus some polish
This commit is contained in:
parent
3d64268058
commit
b73256c35b
@ -226,6 +226,11 @@ export default class SystemPage extends React.Component {
|
||||
href="/experiences/list-filecoin-deals"
|
||||
title="FilecoinDealsList"
|
||||
/>
|
||||
<SidebarLink
|
||||
url={url}
|
||||
href="/experiences/filecoin-settings"
|
||||
title="FilecoinSettings"
|
||||
/>
|
||||
|
||||
<span css={STYLES_LABEL}>
|
||||
<br />
|
||||
|
@ -33,6 +33,12 @@ const STYLES_INPUT_CONTAINER = css`
|
||||
min-width: 188px;
|
||||
`;
|
||||
|
||||
const STYLES_INPUT_CONTAINER_FULL = css`
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
min-width: 188px;
|
||||
`;
|
||||
|
||||
const STYLES_INPUT = css`
|
||||
${INPUT_STYLES}
|
||||
padding: 0 24px 0 24px;
|
||||
@ -128,7 +134,12 @@ export class Input extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div css={STYLES_INPUT_CONTAINER} style={this.props.containerStyle}>
|
||||
<div
|
||||
css={
|
||||
this.props.full ? STYLES_INPUT_CONTAINER_FULL : STYLES_INPUT_CONTAINER
|
||||
}
|
||||
style={this.props.containerStyle}
|
||||
>
|
||||
<DescriptionGroup
|
||||
tooltip={this.props.tooltip}
|
||||
label={this.props.label}
|
||||
|
@ -31,15 +31,15 @@ const STYLES_SELECT_MENU = css`
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const STYLES_SELECT_MENU_FULL = css`
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
const STYLES_CONTAINER = css`
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
`;
|
||||
|
||||
const STYLES_CONTAINER_FULL = css`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@ -86,7 +86,7 @@ export const SelectMenu = (props) => {
|
||||
let presentationValue = map[props.value] ? map[props.value] : "Unselected";
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div css={props.full ? STYLES_CONTAINER_FULL : STYLES_CONTAINER}>
|
||||
<DescriptionGroup
|
||||
label={props.label}
|
||||
description={props.description}
|
||||
@ -94,15 +94,7 @@ export const SelectMenu = (props) => {
|
||||
style={props.containerStyle}
|
||||
/>
|
||||
|
||||
<div
|
||||
css={
|
||||
props.className
|
||||
? props.className
|
||||
: props.full
|
||||
? STYLES_SELECT_MENU_FULL
|
||||
: STYLES_SELECT_MENU
|
||||
}
|
||||
>
|
||||
<div css={props.className ? props.className : STYLES_SELECT_MENU}>
|
||||
<label css={STYLES_SELECT_MENU_LABEL} htmlFor={`id-${props.name}`}>
|
||||
{map[props.value]}{" "}
|
||||
{props.category ? (
|
||||
@ -126,7 +118,7 @@ export const SelectMenu = (props) => {
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
FilecoinStorageDealsList,
|
||||
FilecoinRetrievalDealsList,
|
||||
} from "~/components/system/modules/FilecoinDealsList";
|
||||
import { FilecoinSettings } from "~/components/system/modules/FilecoinSettings";
|
||||
|
||||
// NOTE(jim): Global components
|
||||
import { GlobalModal } from "~/components/system/components/GlobalModal";
|
||||
@ -92,6 +93,7 @@ export {
|
||||
FilecoinBalancesList,
|
||||
FilecoinRetrievalDealsList,
|
||||
FilecoinStorageDealsList,
|
||||
FilecoinSettings,
|
||||
// NOTE(jim): Components
|
||||
ButtonPrimary,
|
||||
ButtonPrimaryFull,
|
||||
|
335
components/system/modules/FilecoinSettings.js
Normal file
335
components/system/modules/FilecoinSettings.js
Normal file
@ -0,0 +1,335 @@
|
||||
import * as React from "react";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Strings from "~/common/strings";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
import { DescriptionGroup } from "~/components/system/components/fragments/DescriptionGroup";
|
||||
import { SelectMenu } from "~/components/system/components/SelectMenus";
|
||||
import { Toggle } from "~/components/system/components/Toggle";
|
||||
import { Input } from "~/components/system/components/Input";
|
||||
import { CheckBox } from "~/components/system/components/CheckBox";
|
||||
import { ButtonPrimary } from "~/components/system/components/Buttons";
|
||||
import { CardTabGroup } from "~/components/system/components/CardTabGroup";
|
||||
|
||||
const STYLES_GROUP = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
max-width: 768px;
|
||||
`;
|
||||
|
||||
const STYLES_SUBGROUP = css`
|
||||
padding-left: 24px;
|
||||
width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
const STYLES_LEFT = css`
|
||||
padding: 12px 0 0 0;
|
||||
min-width: 10%;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
const STYLES_RIGHT = css`
|
||||
padding-left: 48px;
|
||||
padding-top: 24px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const TAB_GROUP = [
|
||||
{ value: "general", label: "General" },
|
||||
{ value: "cold", label: "Cold Storage" },
|
||||
{ value: "hot", label: "Hot Storage" },
|
||||
];
|
||||
|
||||
export class FilecoinSettings extends React.Component {
|
||||
static defaultProps = {
|
||||
addrs: [],
|
||||
settings_deals_auto_approve: false,
|
||||
settings_hot_enabled: true,
|
||||
//settings_hot_allow_unfreeze: this.props.allowUnfreeze,
|
||||
settings_hot_ipfs_add_timeout: 60,
|
||||
settings_cold_enabled: true,
|
||||
//settings_cold_default_address: this.props.defaultAddr,
|
||||
settings_cold_default_duration: 1000,
|
||||
settings_cold_default_replication_factor: 1,
|
||||
settings_cold_default_excluded_miners: [],
|
||||
settings_cold_default_trusted_miners: [],
|
||||
//settings_cold_default_max_price: this.props.maxPrice,
|
||||
settings_cold_default_auto_renew: true,
|
||||
//settings_cold_default_auto_renew_max_price: this.props.autoRenewMaxPrice,
|
||||
//settings_repairable:
|
||||
};
|
||||
|
||||
state = {
|
||||
tabGroup: "general",
|
||||
addrsList: this.props.addrsList.map((each) => {
|
||||
return {
|
||||
value: each.addr,
|
||||
name: each.name,
|
||||
};
|
||||
}),
|
||||
settings_deals_auto_approve: this.props.autoApprove, //left off changing these to match teh shape of this.props.defaultStorageConfig
|
||||
settings_hot_enabled: this.props.hotEnabled, //and incorporate info from aaron
|
||||
settings_hot_allow_unfreeze: this.props.allowUnfreeze, //we can use miner api for the list editor component (and incorp reputation)
|
||||
settings_hot_ipfs_add_timeout: this.props.addTimeout,
|
||||
settings_cold_enabled: this.props.coldEnabled,
|
||||
settings_cold_default_address: this.props.defaultAddr,
|
||||
settings_cold_default_duration: this.props.dealMinDuration,
|
||||
settings_cold_default_replication_factor: this.props.repFactor,
|
||||
settings_cold_default_excluded_miners: this.props.excludedMinersList,
|
||||
settings_cold_default_trusted_miners: this.props.trustedMinersList,
|
||||
settings_cold_default_max_price: this.props.maxPrice,
|
||||
settings_cold_default_auto_renew: this.props.autoRenew,
|
||||
settings_cold_default_auto_renew_max_price: this.props.autoRenewMaxPrice,
|
||||
settings_repairable: this.props.repairable,
|
||||
};
|
||||
|
||||
_handleSave = async () => {
|
||||
this.props.onSave({
|
||||
data: {
|
||||
settings_deals_auto_approve: this.state.settings_deals_auto_approve,
|
||||
},
|
||||
config: {
|
||||
hot: {
|
||||
enabled: this.state.settings_hot_enabled,
|
||||
allowUnfreeze: this.state.settings_hot_allow_unfreeze,
|
||||
ipfs: {
|
||||
addTimeout: this.state.settings_hot_ipfs_add_timeout,
|
||||
},
|
||||
},
|
||||
cold: {
|
||||
enabled: this.state.settings_cold_enabled,
|
||||
filecoin: {
|
||||
addr: this.state.settings_cold_default_address,
|
||||
dealMinDuration: this.state.settings_cold_default_duration,
|
||||
repFactor: this.state.settings_cold_default_replication_factor,
|
||||
excludedMinersList: this.state
|
||||
.settings_cold_default_excluded_miners,
|
||||
trustedMinersList: this.state.settings_cold_default_trusted_miners,
|
||||
maxPrice: this.state.settings_cold_default_max_price,
|
||||
renew: {
|
||||
enabled: this.state.settings_cold_default_auto_renew,
|
||||
threshold: this.state.settings_cold_default_auto_renew_max_price,
|
||||
},
|
||||
},
|
||||
},
|
||||
repairable: this.state.settings_repairable,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
_handleChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<CardTabGroup
|
||||
name="tabGroup"
|
||||
options={TAB_GROUP}
|
||||
value={this.state.tabGroup}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
<div style={{ padding: "16px" }}>
|
||||
{this.state.tabGroup === "general" ? (
|
||||
<div>
|
||||
<div css={STYLES_GROUP}>
|
||||
<div css={STYLES_LEFT}>
|
||||
<DescriptionGroup
|
||||
label="Automatically approve deals"
|
||||
tooltip="If you do not have enough Filecoin you will receive a warning, regardless of whether this is enabled."
|
||||
description="When enabled, every storage deal will be automatically approved without asking for confirmation."
|
||||
/>
|
||||
</div>
|
||||
<div css={STYLES_RIGHT}>
|
||||
<Toggle
|
||||
name="settings_deals_auto_approve"
|
||||
onChange={this._handleChange}
|
||||
active={this.state.settings_deals_auto_approve}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DescriptionGroup
|
||||
style={{ marginTop: 24 }}
|
||||
label="Repairable"
|
||||
description="Placeholder."
|
||||
tooltip="Placeholder."
|
||||
/>
|
||||
<CheckBox
|
||||
name="settings_repairable"
|
||||
value={this.state.settings_repairable}
|
||||
onChange={this._handleChange}
|
||||
>
|
||||
Repairable
|
||||
</CheckBox>
|
||||
</div>
|
||||
</div>
|
||||
) : this.state.tabGroup === "cold" ? (
|
||||
<div>
|
||||
<div css={STYLES_GROUP}>
|
||||
<div css={STYLES_LEFT}>
|
||||
<DescriptionGroup
|
||||
label="Enable cold storage"
|
||||
tooltip="Placeholder"
|
||||
description="By enabling cold storage, every time you make a deal your data will be stored on the Filecoin Network."
|
||||
/>
|
||||
</div>
|
||||
<div css={STYLES_RIGHT}>
|
||||
<Toggle
|
||||
name="settings_cold_enabled"
|
||||
onChange={this._handleChange}
|
||||
active={this.state.settings_cold_enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.settings_cold_enabled ? (
|
||||
<div>
|
||||
<SelectMenu
|
||||
full
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
label="Default Filecoin address"
|
||||
description="Deal payments will default to this address."
|
||||
tooltip="You will always have the option to choose a different wallet before you make a deal."
|
||||
name="settings_cold_default_address"
|
||||
value={this.state.settings_cold_default_address}
|
||||
category="address"
|
||||
onChange={this._handleChange}
|
||||
options={this.state.addrsList}
|
||||
/>
|
||||
|
||||
<Input
|
||||
full
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
label="Default deal duration"
|
||||
description="How long your files will be stored for."
|
||||
tooltip="Placeholder."
|
||||
name="settings_cold_default_duration"
|
||||
type="number"
|
||||
value={this.state.settings_cold_default_duration}
|
||||
placeholder="Duration in epochs (~25 seconds)"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
|
||||
<Input
|
||||
full
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
label="Default replication factor"
|
||||
description="The number of miners that each file will be stored with."
|
||||
tooltip="A higher replication factor means your files are more secure against loss, but also costs more."
|
||||
name="settings_cold_default_replication_factor"
|
||||
value={this.state.settings_cold_default_replication_factor}
|
||||
placeholder="Number of miners"
|
||||
type="number"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
|
||||
<Input
|
||||
full
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
label="Max Filecoin price"
|
||||
description="The maximum price you're willing to pay to store your file for the length of one deal duration (as specified above)."
|
||||
tooltip="Slate will always try to find you the best price, regardless of how high you set this."
|
||||
name="settings_cold_default_max_price"
|
||||
type="number"
|
||||
value={this.state.settings_cold_default_max_price}
|
||||
placeholder="Price in Filecoin"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
|
||||
<DescriptionGroup
|
||||
style={{ marginTop: 24 }}
|
||||
label="Auto renew deals"
|
||||
description="If auto renew is enabled, your wallet will automatically be charged once the deal duration is up. This guarantees your files will remain stored even if you do not manually renew."
|
||||
tooltip="This does not protect your files in the event that your wallet lacks sufficient funds or if there are no miners willing to store it for less than your max auto renew price."
|
||||
/>
|
||||
<CheckBox
|
||||
name="settings_cold_default_auto_renew"
|
||||
value={this.state.settings_cold_default_auto_renew}
|
||||
onChange={this._handleChange}
|
||||
>
|
||||
Enable auto renew
|
||||
</CheckBox>
|
||||
|
||||
<Input
|
||||
full
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
label="Max auto renew price."
|
||||
description="Set the maximum Filecoin price you're willing to pay to auto renew a storage deal."
|
||||
tooltip="Placeholder."
|
||||
name="settings_cold_default_auto_renew_max_price"
|
||||
type="number"
|
||||
value={
|
||||
this.state.settings_cold_default_auto_renew_max_price
|
||||
}
|
||||
placeholder="Price in Filecoin"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div css={STYLES_GROUP}>
|
||||
<div css={STYLES_LEFT}>
|
||||
<DescriptionGroup
|
||||
label="Enable hot storage"
|
||||
tooltip="Placeholder"
|
||||
description="By enabling hot storage, every time you make a deal your data will be stored on IPFS."
|
||||
/>
|
||||
</div>
|
||||
<div css={STYLES_RIGHT}>
|
||||
<Toggle
|
||||
name="settings_hot_enabled"
|
||||
onChange={this._handleChange}
|
||||
active={this.state.settings_hot_enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.settings_hot_enabled ? (
|
||||
<div>
|
||||
<DescriptionGroup
|
||||
style={{ marginTop: 24 }}
|
||||
label="Allow Unfreeze"
|
||||
description="Placeholder."
|
||||
tooltip="Placeholder."
|
||||
/>
|
||||
<CheckBox
|
||||
name="settings_hot_allow_unfreeze"
|
||||
value={this.state.settings_hot_allow_unfreeze}
|
||||
onChange={this._handleChange}
|
||||
>
|
||||
IPFS allow unfreeze setting description.
|
||||
</CheckBox>
|
||||
|
||||
<Input
|
||||
full
|
||||
containerStyle={{ marginTop: 24 }}
|
||||
label="Add timeout"
|
||||
description="Add IPFS timeout setting description."
|
||||
tooltip="Placeholder."
|
||||
name="settings_hot_ipfs_add_timeout"
|
||||
value={this.state.settings_hot_ipfs_add_timeout}
|
||||
placeholder="Type in seconds"
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<ButtonPrimary onClick={this._handleSave}>Save</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
128
pages/experiences/filecoin-settings.js
Normal file
128
pages/experiences/filecoin-settings.js
Normal file
@ -0,0 +1,128 @@
|
||||
import * as React from "react";
|
||||
import * as System from "~/components/system";
|
||||
|
||||
import SystemPage from "~/components/system/SystemPage";
|
||||
import ViewSourceLink from "~/components/system/ViewSourceLink";
|
||||
import CodeBlock from "~/components/system/CodeBlock";
|
||||
|
||||
const EXAMPLE_CODE = `import * as React from 'react';
|
||||
import { FilecoinSettings } from 'slate-react-system';
|
||||
import { createPow } from "@textile/powergate-client";
|
||||
|
||||
const PowerGate = createPow({ host: "http://pow.slate.textile.io:6002" });
|
||||
|
||||
class Example extends React.Component {
|
||||
componentDidMount = async () => {
|
||||
const FFS = await PowerGate.ffs.create();
|
||||
const token = FFS.token ? FFS.token : null;
|
||||
PowerGate.setToken(token);
|
||||
const { addrs } = await Powergate.ffs.addrs();
|
||||
const { defaultStorageConfig } = await PowerGate.ffs.defaultStorageConfig();
|
||||
this.setState({ token, defaultStorageConfig, addrsList });
|
||||
}
|
||||
|
||||
_handleSave = async ({ data, config }) => {
|
||||
const response = await Powergate.ffs.setDefaultStorageConfig(config);
|
||||
this.setState({ data });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FilecoinSettings
|
||||
defaultStorageConfig={this.state.defaultStorageConfig}
|
||||
addrsList={this.state.addrsList}
|
||||
onSave={this._handleSave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default class SystemPageFilecoinWalletBalances extends React.Component {
|
||||
_handleSave = (value) => {
|
||||
console.log(value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const addrsList = [
|
||||
{
|
||||
addr:
|
||||
"t3qwsglg755cwfaehqmsuzj2efebyyrqzlnhjogj2uwj44ce3anpowsdmaxdfnndukihmzrohqnpzakoq3tujq",
|
||||
name: "Initial Address",
|
||||
type: "bls",
|
||||
},
|
||||
{
|
||||
addr:
|
||||
"t3ual5q5qo5wolfxsui4ciujfucqwf6gqso4lettcjwl2tyismgol7c4tngvoono5rmytuqotye7oosfjv6g7a",
|
||||
name: "Secondary Address",
|
||||
type: "bls",
|
||||
},
|
||||
];
|
||||
const defaultStorageConfig = {
|
||||
defaultStorageConfig: {
|
||||
cold: {
|
||||
enabled: true,
|
||||
filecoin: {
|
||||
addr:
|
||||
"t3whycszj43jeesnnagr3wbkyxoh6qv5ri6kqfmvk3u5x2nxcgiu4266dstutkdvi4pqbkz75odv7bxa6fjjkq",
|
||||
countryCodesList: [],
|
||||
dealMinDuration: 1000,
|
||||
excludedMinersList: [],
|
||||
maxPrice: 0,
|
||||
renew: {
|
||||
enabled: false,
|
||||
threshold: 0,
|
||||
},
|
||||
repFactor: 1,
|
||||
trustedMinersList: [],
|
||||
},
|
||||
},
|
||||
hot: {
|
||||
allowUnfreeze: false,
|
||||
enabled: true,
|
||||
ipfs: {
|
||||
addTimeout: 30,
|
||||
},
|
||||
},
|
||||
repairable: false,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<SystemPage
|
||||
title="SDS: Filecoin Settings"
|
||||
description="..."
|
||||
url="https://slate.host/experiences/filecoin-settings"
|
||||
>
|
||||
<System.H1>
|
||||
Filecoin Settings{" "}
|
||||
<ViewSourceLink file="experiences/filecoin-settings.js" />
|
||||
</System.H1>
|
||||
<br />
|
||||
<br />
|
||||
<System.P>
|
||||
Here is an example of an experience for getting and setting Filecoin
|
||||
Settings from{" "}
|
||||
<a target="_blank" href="https://github.com/textileio/powergate/">
|
||||
Textile's Powergate
|
||||
</a>
|
||||
.
|
||||
</System.P>
|
||||
<br />
|
||||
<br />
|
||||
<System.FilecoinSettings
|
||||
addrsList={addrsList}
|
||||
defaultStorageConfig={defaultStorageConfig}
|
||||
onSave={this._handleSave}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<System.H2>Code</System.H2>
|
||||
<hr />
|
||||
<br />
|
||||
<CodeBlock>{EXAMPLE_CODE}</CodeBlock>
|
||||
</SystemPage>
|
||||
);
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ class Example extends React.Component {
|
||||
token: null
|
||||
}
|
||||
|
||||
_handleCreateToken = () => {
|
||||
_handleCreateToken = async () => {
|
||||
const PowerGate = createPow({ host: "http://pow.slate.textile.io:6002" });
|
||||
const FFS = await PowerGate.ffs.create();
|
||||
const token = FFS.token ? FFS.token : null;
|
||||
|
@ -139,6 +139,7 @@ class ExampleTwo extends React.Component {
|
||||
value={this.state.eight}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
<br />
|
||||
<System.TabGroup
|
||||
name="nine"
|
||||
options={TAB_GROUP_THREE}
|
||||
|
@ -51,7 +51,7 @@ export default class SceneSettings extends React.Component {
|
||||
},
|
||||
config: {
|
||||
hot: {
|
||||
enabled: this.state.settings_cold_enabled,
|
||||
enabled: this.state.settings_hot_enabled,
|
||||
allowUnfreeze: this.state.settings_hot_allow_unfreeze,
|
||||
ipfs: {
|
||||
addTimeout: this.state.settings_hot_ipfs_add_timeout,
|
||||
|
Loading…
Reference in New Issue
Block a user