Ui 2 updates subscribe (#6027)
* Add account & vault APIs * Additional status methods * Move permission modals into Dapp * Adjust display position * Don't publish invalid events * Cleanup Wallet display * Update package-lock * Align icon buttons center * Adjust account selectors * Adjust wallet white * Allow display of boolean/false values * Pass value through correctly for disabled inputs * Split requests into sections * onClict -> onClick * Update label * Update skip step * Connect provider interfaces
This commit is contained in:
parent
76f0247f5d
commit
cbcda140ec
39
js/package-lock.json
generated
39
js/package-lock.json
generated
@ -1559,9 +1559,9 @@
|
|||||||
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk="
|
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk="
|
||||||
},
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.10.0",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||||
"integrity": "sha512-q/r9trjmuikWDRJNTBHAVnWhuU6w+z80KgBq7j9YDclik5E7X4xi0KnlZBNFA1zOQ+SH/vHMWd2mC9QTOz7GpA=="
|
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ=="
|
||||||
},
|
},
|
||||||
"commondir": {
|
"commondir": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -4120,7 +4120,8 @@
|
|||||||
"graceful-readlink": {
|
"graceful-readlink": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
|
||||||
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU="
|
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"growl": {
|
"growl": {
|
||||||
"version": "1.9.2",
|
"version": "1.9.2",
|
||||||
@ -4729,9 +4730,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"irregular-plurals": {
|
"irregular-plurals": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.3.0.tgz",
|
||||||
"integrity": "sha1-OPKZg0uowAwwvpxVThNyaXUv86w=",
|
"integrity": "sha512-njf5A+Mxb3kojuHd1DzISjjIl+XhyzovXEOyPPSzdQozq/Lf2tN27mOrAAsxEPZxpn6I4MGzs1oo9TxXxPFpaA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"is-absolute": {
|
"is-absolute": {
|
||||||
@ -6793,9 +6794,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.5.tgz",
|
||||||
"integrity": "sha1-VzrN33P0LsskqmGNQO49WnwEplQ=",
|
"integrity": "sha1-76NPdFyiW9Ozy9FapOAxErgjfzw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
@ -6831,9 +6832,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.5.tgz",
|
||||||
"integrity": "sha1-VzrN33P0LsskqmGNQO49WnwEplQ=",
|
"integrity": "sha1-76NPdFyiW9Ozy9FapOAxErgjfzw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
@ -6869,9 +6870,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.5.tgz",
|
||||||
"integrity": "sha1-VzrN33P0LsskqmGNQO49WnwEplQ=",
|
"integrity": "sha1-76NPdFyiW9Ozy9FapOAxErgjfzw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
@ -6907,9 +6908,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.5.tgz",
|
||||||
"integrity": "sha1-VzrN33P0LsskqmGNQO49WnwEplQ=",
|
"integrity": "sha1-76NPdFyiW9Ozy9FapOAxErgjfzw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
|
@ -43,11 +43,6 @@ export default class Eth {
|
|||||||
}, timeout);
|
}, timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this._api.transport.isConnected) {
|
|
||||||
nextTimeout(500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._api.eth
|
return this._api.eth
|
||||||
.blockNumber()
|
.blockNumber()
|
||||||
.then((blockNumber) => {
|
.then((blockNumber) => {
|
||||||
|
@ -53,11 +53,6 @@ export default class Personal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this._api.transport.isConnected) {
|
|
||||||
nextTimeout(500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._api.parity
|
return this._api.parity
|
||||||
.defaultAccount()
|
.defaultAccount()
|
||||||
.then((defaultAccount) => {
|
.then((defaultAccount) => {
|
||||||
|
@ -15,31 +15,55 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'parity_accountsInfo': {},
|
accountsView: {
|
||||||
'parity_allAccountsInfo': {},
|
methods: [
|
||||||
'parity_changeVault': {},
|
'parity_accountsInfo',
|
||||||
'parity_changeVaultPassword': {},
|
'parity_allAccountsInfo'
|
||||||
'parity_consensusCapability': {},
|
]
|
||||||
'parity_checkRequest': {},
|
},
|
||||||
'parity_closeVault': {},
|
accountsCreate: {
|
||||||
'parity_executeUpgrade': {},
|
methods: [
|
||||||
'parity_generateSecretPhrase': {},
|
'parity_generateSecretPhrase',
|
||||||
'parity_getVaultMeta': {},
|
'parity_importGethAccounts',
|
||||||
'parity_hashContent': {},
|
'parity_listGethAccounts',
|
||||||
'parity_importGethAccounts': {},
|
'parity_newAccountFromPhrase',
|
||||||
'parity_localTransactions': {},
|
'parity_newAccountFromSecret',
|
||||||
'parity_listGethAccounts': {},
|
'parity_newAccountFromWallet',
|
||||||
'parity_listVaults': {},
|
'parity_phraseToAddress'
|
||||||
'parity_listOpenedVaults': {},
|
]
|
||||||
'parity_newAccountFromPhrase': {},
|
},
|
||||||
'parity_newAccountFromSecret': {},
|
accountsEdit: {
|
||||||
'parity_newAccountFromWallet': {},
|
methods: [
|
||||||
'parity_newVault': {},
|
'parity_setAccountName',
|
||||||
'parity_openVault': {},
|
'parity_setAccountMeta'
|
||||||
'parity_phraseToAddress': {},
|
]
|
||||||
'parity_setAccountMeta': {},
|
},
|
||||||
'parity_setAccountName': {},
|
upgrade: {
|
||||||
'parity_setVaultMeta': {},
|
methods: [
|
||||||
'parity_upgradeReady': {},
|
'parity_consensusCapability',
|
||||||
'parity_versionInfo': {}
|
'parity_executeUpgrade',
|
||||||
|
'parity_upgradeReady',
|
||||||
|
'parity_versionInfo'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
vaults: {
|
||||||
|
methods: [
|
||||||
|
'parity_changeVault',
|
||||||
|
'parity_changeVaultPassword',
|
||||||
|
'parity_closeVault',
|
||||||
|
'parity_getVaultMeta',
|
||||||
|
'parity_listVaults',
|
||||||
|
'parity_listOpenedVaults',
|
||||||
|
'parity_newVault',
|
||||||
|
'parity_openVault',
|
||||||
|
'parity_setVaultMeta'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
methods: [
|
||||||
|
'parity_checkRequest',
|
||||||
|
'parity_hashContent',
|
||||||
|
'parity_localTransactions'
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -39,7 +39,8 @@ export default class Store {
|
|||||||
const duplicates = {};
|
const duplicates = {};
|
||||||
|
|
||||||
return this.requests.filter(({ request: { data: { method, token } } }) => {
|
return this.requests.filter(({ request: { data: { method, token } } }) => {
|
||||||
const id = `${token}:${method}`;
|
const section = this.getFilteredSectionName(method);
|
||||||
|
const id = `${token}:${section}`;
|
||||||
|
|
||||||
if (!duplicates[id]) {
|
if (!duplicates[id]) {
|
||||||
duplicates[id] = true;
|
duplicates[id] = true;
|
||||||
@ -71,10 +72,11 @@ export default class Store {
|
|||||||
|
|
||||||
if (approveAll) {
|
if (approveAll) {
|
||||||
const { request: { data: { method, token } } } = queued;
|
const { request: { data: { method, token } } } = queued;
|
||||||
const requests = this.findMatchingRequests(method, token);
|
|
||||||
|
|
||||||
this.methodsStore.addTokenPermission(method, token);
|
this.getFilteredSection(method).methods.forEach((m) => {
|
||||||
requests.forEach(this.approveSingleRequest);
|
this.methodsStore.addTokenPermission(m, token);
|
||||||
|
this.findMatchingRequests(m, token).forEach(this.approveSingleRequest);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.approveSingleRequest(queued);
|
this.approveSingleRequest(queued);
|
||||||
}
|
}
|
||||||
@ -115,6 +117,16 @@ export default class Store {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFilteredSectionName = (method) => {
|
||||||
|
return Object.keys(filteredRequests).find((key) => {
|
||||||
|
return filteredRequests[key].methods.includes(method);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredSection = (method) => {
|
||||||
|
return filteredRequests[this.getFilteredSectionName(method)];
|
||||||
|
}
|
||||||
|
|
||||||
receiveMessage = ({ data, origin, source }) => {
|
receiveMessage = ({ data, origin, source }) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
@ -126,7 +138,7 @@ export default class Store {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredRequests[method] && !this.methodsStore.hasTokenPermission(method, token)) {
|
if (this.getFilteredSection(method) && !this.methodsStore.hasTokenPermission(method, token)) {
|
||||||
this.queueRequest({ data, origin, source });
|
this.queueRequest({ data, origin, source });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { flatten } from 'lodash';
|
||||||
import { action, observable } from 'mobx';
|
import { action, observable } from 'mobx';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
@ -24,7 +25,9 @@ import filteredRequests from '../../DappRequests/filteredRequests';
|
|||||||
const LS_PERMISSIONS = '_parity::dapps::methods';
|
const LS_PERMISSIONS = '_parity::dapps::methods';
|
||||||
|
|
||||||
export default class Store {
|
export default class Store {
|
||||||
@observable filteredRequests = Object.keys(filteredRequests);
|
@observable filteredRequests = flatten(
|
||||||
|
Object.keys(filteredRequests).map((key) => filteredRequests[key].methods)
|
||||||
|
);
|
||||||
@observable modalOpen = false;
|
@observable modalOpen = false;
|
||||||
@observable permissions = {};
|
@observable permissions = {};
|
||||||
@observable tokens = {};
|
@observable tokens = {};
|
||||||
|
@ -166,7 +166,7 @@ export default function TnC ({ hasAccepted, onAccept }) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
checked={ hasAccepted }
|
checked={ hasAccepted }
|
||||||
onClict={ onAccept }
|
onClick={ onAccept }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -314,7 +314,7 @@ class FirstRun extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipAccountCreation = () => {
|
skipAccountCreation = () => {
|
||||||
this.setState({ stage: this.state.stage + 2 });
|
this.setState({ stage: this.state.stage + 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
printPhrase = () => {
|
printPhrase = () => {
|
||||||
|
@ -19,7 +19,7 @@ import { Checkbox as SemanticCheckbox } from 'semantic-ui-react';
|
|||||||
|
|
||||||
import { nodeOrStringProptype } from '@parity/shared/util/proptypes';
|
import { nodeOrStringProptype } from '@parity/shared/util/proptypes';
|
||||||
|
|
||||||
import LabelWrapper from '../LabelWrapper';
|
import Label from '../Label';
|
||||||
|
|
||||||
export default function Checkbox ({ checked = false, className, label, onClick, style }) {
|
export default function Checkbox ({ checked = false, className, label, onClick, style }) {
|
||||||
return (
|
return (
|
||||||
@ -27,7 +27,7 @@ export default function Checkbox ({ checked = false, className, label, onClick,
|
|||||||
checked={ checked }
|
checked={ checked }
|
||||||
className={ className }
|
className={ className }
|
||||||
label={
|
label={
|
||||||
<LabelWrapper label={ label } />
|
<Label label={ label } />
|
||||||
}
|
}
|
||||||
onClick={ onClick }
|
onClick={ onClick }
|
||||||
style={ style }
|
style={ style }
|
||||||
|
@ -113,6 +113,11 @@ export default class Input extends Component {
|
|||||||
render () {
|
render () {
|
||||||
const { children, className, defaultValue, disabled, error, hint, label, max, min, onClick, readOnly, step, style, tabIndex, type } = this.props;
|
const { children, className, defaultValue, disabled, error, hint, label, max, min, onClick, readOnly, step, style, tabIndex, type } = this.props;
|
||||||
const { value } = this.state;
|
const { value } = this.state;
|
||||||
|
const displayValue = typeof value !== 'undefined'
|
||||||
|
? value
|
||||||
|
: defaultValue;
|
||||||
|
|
||||||
|
console.log('Input', displayValue, parseI18NString(this.context, `${displayValue}`));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LabelWrapper
|
<LabelWrapper
|
||||||
@ -141,11 +146,11 @@ export default class Input extends Component {
|
|||||||
step={ step }
|
step={ step }
|
||||||
style={ style }
|
style={ style }
|
||||||
tabIndex={ tabIndex }
|
tabIndex={ tabIndex }
|
||||||
value={ parseI18NString(this.context, value || defaultValue) }
|
|
||||||
>
|
>
|
||||||
{ this.renderCopyButton() }
|
{ this.renderCopyButton() }
|
||||||
<input
|
<input
|
||||||
type={ type }
|
type={ type }
|
||||||
|
value={ parseI18NString(this.context, `${displayValue}`) }
|
||||||
/>
|
/>
|
||||||
{ children }
|
{ children }
|
||||||
</SemanticInput>
|
</SemanticInput>
|
||||||
|
@ -66,7 +66,16 @@ class InputAddress extends Component {
|
|||||||
classes.push(!icon ? styles.inputEmpty : styles.input);
|
classes.push(!icon ? styles.inputEmpty : styles.input);
|
||||||
|
|
||||||
const containerClasses = [ styles.container ];
|
const containerClasses = [ styles.container ];
|
||||||
const nullName = (disabled || readOnly) && isNullAddress(value) ? 'null' : null;
|
const nullName = (disabled || readOnly) && isNullAddress(value)
|
||||||
|
? 'null'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// FIXME: The is not advisable, fixes the display issue, however the name should come from
|
||||||
|
// a common component.
|
||||||
|
// account.name || (value ? 'UNNAMED' : value)
|
||||||
|
const displayValue = text && account
|
||||||
|
? (account.name || (value ? 'UNNAMED' : value))
|
||||||
|
: (nullName || value);
|
||||||
|
|
||||||
if (small) {
|
if (small) {
|
||||||
containerClasses.push(styles.small);
|
containerClasses.push(styles.small);
|
||||||
@ -78,9 +87,6 @@ class InputAddress extends Component {
|
|||||||
props.focused = focused;
|
props.focused = focused;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: The is not advisable, fixes the display issue, however the name should come from
|
|
||||||
// a common component.
|
|
||||||
// account.name || (value ? 'UNNAMED' : value)
|
|
||||||
return (
|
return (
|
||||||
<div className={ containerClasses.join(' ') }>
|
<div className={ containerClasses.join(' ') }>
|
||||||
<Input
|
<Input
|
||||||
@ -98,11 +104,7 @@ class InputAddress extends Component {
|
|||||||
onSubmit={ this.onSubmit }
|
onSubmit={ this.onSubmit }
|
||||||
readOnly={ readOnly }
|
readOnly={ readOnly }
|
||||||
tabIndex={ tabIndex }
|
tabIndex={ tabIndex }
|
||||||
value={
|
value={ displayValue }
|
||||||
text && account
|
|
||||||
? (account.name || (value ? 'UNNAMED' : value))
|
|
||||||
: (nullName || value)
|
|
||||||
}
|
|
||||||
{ ...props }
|
{ ...props }
|
||||||
/>
|
/>
|
||||||
{ icon }
|
{ icon }
|
||||||
|
@ -80,23 +80,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.identities {
|
.identities {
|
||||||
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.identity {
|
.identity {
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: 0 1 12.5%;
|
flex: 0 1 12.5%;
|
||||||
padding: 0.75em 0;
|
padding: 0.75em 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 12.5% !important;
|
width: 12.5% !important;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
box-sizing: content-box;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
|
> img {
|
||||||
|
background: white;
|
||||||
border-color: #2185D0;
|
border-color: #2185D0;
|
||||||
border-radius: 8px;
|
}
|
||||||
background: rgba(200, 200, 225, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unselected {
|
&.unselected {
|
||||||
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
|
|
||||||
.detail {
|
.detail {
|
||||||
font-size: 1.125em;
|
font-size: 1.125em;
|
||||||
color: white;
|
|
||||||
margin: 0 0.375em;
|
margin: 0 0.375em;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
|
|
||||||
|
@ -39,29 +39,6 @@ import styles from './wallet.css';
|
|||||||
|
|
||||||
const accountsHistory = HistoryStore.get('accounts');
|
const accountsHistory = HistoryStore.get('accounts');
|
||||||
|
|
||||||
class WalletContainer extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
netVersion: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { netVersion, ...others } = this.props;
|
|
||||||
|
|
||||||
if (netVersion === '0') {
|
|
||||||
return (
|
|
||||||
<Loading size='large' />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wallet
|
|
||||||
netVersion={ netVersion }
|
|
||||||
{ ...others }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Wallet extends Component {
|
class Wallet extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
@ -422,4 +399,4 @@ function mapDispatchToProps (dispatch) {
|
|||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(WalletContainer);
|
)(Wallet);
|
||||||
|
Loading…
Reference in New Issue
Block a user