New Homepage (#7266)
* Purify dappCard style * Add support for pinning apps * Add a section to show pinned apps * Cleaner code * Bump dependency versions * Small tweaks * Avoid double scrollbars * Small style updates * Bump parity/shared version
This commit is contained in:
parent
c731b5ef62
commit
1851453f00
32
js/package-lock.json
generated
32
js/package-lock.json
generated
@ -15,9 +15,9 @@
|
||||
}
|
||||
},
|
||||
"@parity/api": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@parity/api/-/api-2.1.5.tgz",
|
||||
"integrity": "sha512-HkvMIhIwDMEIyTmXqEjWn1C2qes0qJO270bQldRfCZf0XiOGXG726EzV3FUpUbVONCVQ9riDviAl3fw6D+N6nA==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@parity/api/-/api-2.1.6.tgz",
|
||||
"integrity": "sha512-4HuUJLGkZEAHSy5918ofepKXWvAE89VsoUN7q1Px9ASN8xLR3MWPMuNxOJZjKTqVDZWpXs8q3g7GIOVdi90BXA==",
|
||||
"requires": {
|
||||
"@parity/abi": "2.1.2",
|
||||
"@parity/jsonrpc": "2.1.4",
|
||||
@ -52,8 +52,8 @@
|
||||
"version": "github:js-dist-paritytech/dapp-dapp-methods#7245089a8e83274372cde3c9406ae155e2083f84",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parity/api": "2.1.5",
|
||||
"@parity/shared": "2.2.12",
|
||||
"@parity/api": "2.1.6",
|
||||
"@parity/shared": "2.2.14",
|
||||
"@parity/ui": "2.2.15",
|
||||
"lodash": "4.17.4",
|
||||
"mobx": "3.3.2",
|
||||
@ -74,9 +74,9 @@
|
||||
"integrity": "sha512-6bICFA1c1GBz4d7vratkoqovBezJNjc8VCwnZtpPTcyLeMshAhatPV4dGgJo/eHtlOCkKAeaAKatWZhEtXt/5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parity/api": "2.1.5",
|
||||
"@parity/api": "2.1.6",
|
||||
"@parity/etherscan": "2.1.3",
|
||||
"@parity/shared": "2.2.12",
|
||||
"@parity/shared": "2.2.14",
|
||||
"bignumber.js": "3.0.1",
|
||||
"brace": "0.9.0",
|
||||
"date-difference": "1.0.0",
|
||||
@ -556,7 +556,7 @@
|
||||
"version": "github:js-dist-paritytech/dapp-dapp-visible#84f40feb42f1707d7f463213a990600dc11978f7",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parity/api": "2.1.5",
|
||||
"@parity/api": "2.1.6",
|
||||
"@parity/ui": "2.2.15",
|
||||
"mobx": "3.3.2",
|
||||
"mobx-react": "4.3.5",
|
||||
@ -576,9 +576,9 @@
|
||||
"integrity": "sha512-6bICFA1c1GBz4d7vratkoqovBezJNjc8VCwnZtpPTcyLeMshAhatPV4dGgJo/eHtlOCkKAeaAKatWZhEtXt/5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parity/api": "2.1.5",
|
||||
"@parity/api": "2.1.6",
|
||||
"@parity/etherscan": "2.1.3",
|
||||
"@parity/shared": "2.2.12",
|
||||
"@parity/shared": "2.2.14",
|
||||
"bignumber.js": "3.0.1",
|
||||
"brace": "0.9.0",
|
||||
"date-difference": "1.0.0",
|
||||
@ -1103,7 +1103,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@parity/etherscan/-/etherscan-2.1.3.tgz",
|
||||
"integrity": "sha512-GtQMaE8t7PDOcz/K4Ud+Z6EELB47+qG5V6R7iTJ4DcueXVgiMAXK5OiNeKF3Qjd1/M4FIJdFm5NTSdC7bR38+Q==",
|
||||
"requires": {
|
||||
"@parity/api": "2.1.5",
|
||||
"@parity/api": "2.1.6",
|
||||
"bignumber.js": "3.0.1",
|
||||
"es6-promise": "4.1.1",
|
||||
"node-fetch": "1.7.3",
|
||||
@ -1140,9 +1140,9 @@
|
||||
"version": "github:paritytech/plugin-signer-qr#c16423de5b8a8f68ebd5f1e78e084fa959329a9f"
|
||||
},
|
||||
"@parity/shared": {
|
||||
"version": "2.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@parity/shared/-/shared-2.2.12.tgz",
|
||||
"integrity": "sha512-2ANbEOkWoqOf5ytE0K5pq7ZeqS7PVuiIwrhyxDgn8dQhpFiDLHro7pQirIEDZ8LzZK6g6V+HU38sagn6dYSRIQ==",
|
||||
"version": "2.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@parity/shared/-/shared-2.2.14.tgz",
|
||||
"integrity": "sha512-YM7ZqWNyquX0+j89NPPxFBCUPgaqIVrqQ4Iqml50kz9uC/Ev07A/OB/+yYmAJ+3io6LWvDc/U5K3CpaMltTLtg==",
|
||||
"requires": {
|
||||
"@parity/ledger": "2.1.2",
|
||||
"eventemitter3": "2.0.3",
|
||||
@ -1202,9 +1202,9 @@
|
||||
"resolved": "https://registry.npmjs.org/@parity/ui/-/ui-3.0.16.tgz",
|
||||
"integrity": "sha512-yGQ8k2/oNxu0GyJ6eQS1AUk1O1XR//oXfaHPEBa0VuJIB7gY9lPrkG7CSNpazOrTCfhdCHgpjGe3WR5lbv0HiQ==",
|
||||
"requires": {
|
||||
"@parity/api": "2.1.5",
|
||||
"@parity/api": "2.1.6",
|
||||
"@parity/etherscan": "2.1.3",
|
||||
"@parity/shared": "2.2.12",
|
||||
"@parity/shared": "2.2.14",
|
||||
"babel-runtime": "6.26.0",
|
||||
"bignumber.js": "4.1.0",
|
||||
"brace": "0.11.0",
|
||||
|
@ -140,12 +140,12 @@
|
||||
"yargs": "6.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@parity/api": "^2.1.1",
|
||||
"@parity/api": "^2.1.6",
|
||||
"@parity/plugin-signer-account": "paritytech/plugin-signer-account",
|
||||
"@parity/plugin-signer-default": "paritytech/plugin-signer-default",
|
||||
"@parity/plugin-signer-hardware": "paritytech/plugin-signer-hardware",
|
||||
"@parity/plugin-signer-qr": "paritytech/plugin-signer-qr",
|
||||
"@parity/shared": "^2.2.12",
|
||||
"@parity/shared": "2.2.14",
|
||||
"@parity/ui": "^3.0.16",
|
||||
"keythereum": "1.0.2",
|
||||
"lodash.flatten": "4.4.0",
|
||||
|
@ -20,37 +20,28 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.logo {
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
> div:not(.logo) {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
/* Show scrollbar for Homepage only, dapps' scrollbar are handled inside
|
||||
* their iframe.
|
||||
*/
|
||||
& > div {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,6 +75,7 @@ class Application extends Component {
|
||||
|
||||
return (
|
||||
<div className={ styles.application }>
|
||||
<img src={ parityLogo } className={ styles.logo } />
|
||||
{
|
||||
blockNumber
|
||||
? <Status upgradeStore={ this.upgradeStore } />
|
||||
@ -107,33 +108,32 @@ class Application extends Component {
|
||||
}
|
||||
|
||||
renderApp () {
|
||||
const { children } = this.props;
|
||||
|
||||
return [
|
||||
<Extension key='extension' />,
|
||||
<FirstRun key='firstrun' />,
|
||||
<Snackbar key='snackbar' />,
|
||||
<UpgradeParity key='upgrade' upgradeStore={ this.upgradeStore } />,
|
||||
<Errors key='errors' />,
|
||||
<div key='content' className={ styles.content }>
|
||||
{ children }
|
||||
</div>
|
||||
this.renderContent()
|
||||
];
|
||||
}
|
||||
|
||||
renderMinimized () {
|
||||
const { children } = this.props;
|
||||
|
||||
return [
|
||||
<Errors key='errors' />,
|
||||
<div key='content' className={ styles.content }>
|
||||
<div className={ styles.logo }>
|
||||
<img src={ parityLogo } />
|
||||
</div>
|
||||
{ children }
|
||||
</div>
|
||||
this.renderContent()
|
||||
];
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<div key='content' className={ styles.content }>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
|
@ -72,7 +72,7 @@
|
||||
|
||||
.icons {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.icon,
|
||||
|
@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import GradientBg from '@parity/ui/lib/GradientBg';
|
||||
import Input from '@parity/ui/lib/Form/Input';
|
||||
import { CompareIcon, ComputerIcon, DashboardIcon, VpnIcon, KeyIcon } from '@parity/ui/lib/Icons';
|
||||
import { CompareIcon, ComputerIcon, DashboardIcon, KeyIcon } from '@parity/ui/lib/Icons';
|
||||
|
||||
import styles from './connection.css';
|
||||
|
||||
|
@ -16,48 +16,44 @@
|
||||
*/
|
||||
|
||||
.card {
|
||||
padding: 0.25em;
|
||||
position: relative;
|
||||
margin-top: 3em;
|
||||
|
||||
.pin {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1.6em;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pin.pinned {
|
||||
display: block;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: white;
|
||||
box-sizing: content-box;
|
||||
min-height: 100px;
|
||||
display: block;
|
||||
padding-top: 1em;
|
||||
|
||||
.title {
|
||||
min-height: 1em;
|
||||
padding-left: 72px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #333;
|
||||
min-height: 1em;
|
||||
padding-left: 72px;
|
||||
padding-top: 0.25em;
|
||||
opacity: 0.66;
|
||||
margin-top: 0.8em;
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.vouching {
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 1em;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0.25em 0.25em 0.25em 0;
|
||||
}
|
||||
|
||||
div {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.15);
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,11 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import Container from '@parity/ui/lib/Container';
|
||||
import DappIcon from '@parity/ui/lib/DappIcon';
|
||||
import DappVouchFor from '@parity/ui/lib/DappVouchFor';
|
||||
import Header from 'semantic-ui-react/dist/commonjs/elements/Header';
|
||||
import Button from 'semantic-ui-react/dist/commonjs/elements/Button';
|
||||
|
||||
import styles from './dappCard.css';
|
||||
|
||||
@ -27,11 +28,15 @@ export default class DappCard extends Component {
|
||||
static propTypes = {
|
||||
app: PropTypes.object.isRequired,
|
||||
availability: PropTypes.string.isRequired,
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
onPin: PropTypes.func,
|
||||
pinned: PropTypes.bool
|
||||
};
|
||||
|
||||
handlePin = () => this.props.onPin(this.props.app.id)
|
||||
|
||||
render () {
|
||||
const { app, availability, className } = this.props;
|
||||
const { app, availability, className, pinned } = this.props;
|
||||
|
||||
if (app.onlyPersonal && availability !== 'personal') {
|
||||
return null;
|
||||
@ -39,26 +44,28 @@ export default class DappCard extends Component {
|
||||
|
||||
return (
|
||||
<div className={ [styles.card, className].join(' ') }>
|
||||
<Container
|
||||
className={ styles.content }
|
||||
link={ `/${app.id}` }
|
||||
>
|
||||
<DappIcon
|
||||
app={ app }
|
||||
className={ styles.image }
|
||||
/>
|
||||
<div className={ styles.title }>
|
||||
{ app.name }
|
||||
</div>
|
||||
<div className={ styles.description }>
|
||||
{ app.description }
|
||||
</div>
|
||||
<DappVouchFor
|
||||
app={ app }
|
||||
className={ styles.vouching }
|
||||
maxNumber={ 10 }
|
||||
/>
|
||||
</Container>
|
||||
<Button
|
||||
size='mini'
|
||||
icon='pin'
|
||||
circular
|
||||
className={ [styles.pin, pinned && styles.pinned].join(' ') }
|
||||
onClick={ this.handlePin }
|
||||
/>
|
||||
<div className={ styles.content }>
|
||||
<Link to={ app.url === 'web' ? '/web' : `/${app.id}` } >
|
||||
<DappIcon
|
||||
app={ app }
|
||||
className={ styles.image }
|
||||
/>
|
||||
<Header
|
||||
as='h5'
|
||||
textAlign='center'
|
||||
className={ styles.title }
|
||||
>
|
||||
{app.name}
|
||||
</Header>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -41,15 +41,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
padding: 2em 0 0 4em !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.dapps {
|
||||
padding: 0 1.5em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 1.5em 0;
|
||||
justify-content: center;
|
||||
|
||||
.dapp {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
min-width: 25%;
|
||||
width: 12em;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
}
|
||||
|
@ -45,48 +45,62 @@ class Dapps extends Component {
|
||||
this.store.loadAllApps();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { availability } = this.props;
|
||||
const applications = [].concat(this.store.visibleLocal, this.store.visibleViews, this.store.visibleBuiltin, this.store.visibleNetwork);
|
||||
handlePin = (appId) => {
|
||||
if (this.store.displayApps[appId].pinned) {
|
||||
this.store.unpinApp(appId);
|
||||
} else {
|
||||
this.store.pinApp(appId);
|
||||
}
|
||||
}
|
||||
|
||||
renderSection = (apps) => (
|
||||
apps && apps.length > 0 &&
|
||||
<div className={ styles.dapps }>
|
||||
{
|
||||
apps.map((app, index) => (
|
||||
<DappCard
|
||||
app={ app }
|
||||
pinned={ this.store.displayApps[app.id] && this.store.displayApps[app.id].pinned }
|
||||
availability={ this.props.availability }
|
||||
className={ styles.dapp }
|
||||
key={ `${index}_${app.id}` }
|
||||
onPin={ this.handlePin }
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Page className={ styles.dapps }>
|
||||
<Page className={ styles.layout }>
|
||||
{this.renderSection(this.store.pinnedApps)}
|
||||
{this.renderSection(this.store.visibleUnpinned)}
|
||||
{
|
||||
applications.map((app, index) => (
|
||||
<DappCard
|
||||
app={ app }
|
||||
availability={ availability }
|
||||
className={ styles.dapp }
|
||||
key={ `${index}_${app.id}` }
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
this.store.externalOverlayVisible
|
||||
? (
|
||||
<div className={ styles.overlay }>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='dapps.external.warning'
|
||||
defaultMessage='Applications made available on the network by 3rd-party authors are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each before interacting.'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox
|
||||
className={ styles.accept }
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='dapps.external.accept'
|
||||
defaultMessage='I understand that these applications are not affiliated with Parity'
|
||||
/>
|
||||
}
|
||||
checked={ false }
|
||||
onClick={ this.onClickAcceptExternal }
|
||||
/>
|
||||
</div>
|
||||
this.store.externalOverlayVisible &&
|
||||
(
|
||||
<div className={ styles.overlay }>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='dapps.external.warning'
|
||||
defaultMessage='Applications made available on the network by 3rd-party authors are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each before interacting.'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
<div>
|
||||
<Checkbox
|
||||
className={ styles.accept }
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='dapps.external.accept'
|
||||
defaultMessage='I understand that these applications are not affiliated with Parity'
|
||||
/>
|
||||
}
|
||||
checked={ false }
|
||||
onClick={ this.onClickAcceptExternal }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Page>
|
||||
);
|
||||
|
@ -17,46 +17,49 @@
|
||||
import * as mobx from 'mobx';
|
||||
import flatten from 'lodash.flatten';
|
||||
|
||||
import VisibleStore from '@parity/shared/lib/mobx/dappsStore';
|
||||
|
||||
import DappsStore from '@parity/shared/lib/mobx/dappsStore';
|
||||
import RequestStore from './DappRequests/store';
|
||||
import methodGroups from './DappRequests/methodGroups';
|
||||
|
||||
export default function execute (appId, method, params, callback) {
|
||||
const visibleStore = VisibleStore.get();
|
||||
const dappsStore = DappsStore.get();
|
||||
const requestStore = RequestStore.get();
|
||||
|
||||
switch (method) {
|
||||
case 'shell_getApps':
|
||||
case 'shell_getApps': {
|
||||
const [displayAll] = params;
|
||||
|
||||
callback(
|
||||
null,
|
||||
displayAll
|
||||
? visibleStore.allApps.slice().map(mobx.toJS)
|
||||
: visibleStore.visibleApps.slice().map(mobx.toJS)
|
||||
? dappsStore.allApps.slice().map(mobx.toJS)
|
||||
: dappsStore.visibleApps.slice().map(mobx.toJS)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_getFilteredMethods':
|
||||
case 'shell_getFilteredMethods': {
|
||||
callback(
|
||||
null,
|
||||
flatten(Object.keys(methodGroups).map(key => methodGroups[key].methods))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_getMethodGroups':
|
||||
case 'shell_getMethodGroups': {
|
||||
callback(
|
||||
null,
|
||||
methodGroups
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_getMethodPermissions':
|
||||
case 'shell_getMethodPermissions': {
|
||||
callback(null, mobx.toJS(requestStore.permissions));
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_loadApp':
|
||||
case 'shell_loadApp': {
|
||||
const [loadId, loadParams] = params;
|
||||
const loadUrl = `/${loadId}/${loadParams || ''}`;
|
||||
|
||||
@ -64,27 +67,43 @@ export default function execute (appId, method, params, callback) {
|
||||
|
||||
callback(null, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_requestNewToken':
|
||||
case 'shell_requestNewToken': {
|
||||
callback(null, requestStore.createToken(appId));
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_setAppVisibility':
|
||||
const [changeId, visibility] = params;
|
||||
case 'shell_setAppVisibility': {
|
||||
const [appId, visibility] = params;
|
||||
|
||||
callback(
|
||||
null,
|
||||
visibility
|
||||
? visibleStore.showApp(changeId)
|
||||
: visibleStore.hideApp(changeId)
|
||||
? dappsStore.showApp(appId)
|
||||
: dappsStore.hideApp(appId)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_setMethodPermissions':
|
||||
case 'shell_setAppPinned': {
|
||||
const [appId, pinned] = params;
|
||||
|
||||
callback(
|
||||
null,
|
||||
pinned
|
||||
? dappsStore.pinApp(appId)
|
||||
: dappsStore.unpinApp(appId)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'shell_setMethodPermissions': {
|
||||
const [permissions] = params;
|
||||
|
||||
callback(null, requestStore.setPermissions(permissions));
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
|
Loading…
Reference in New Issue
Block a user