Fix newError noops when not bound to dispacher (#4013)
* AddContract properly binds newError * EditMeta properly binds newError * PasswordManager properly binds newError * pass null instead of empty mapStateToProps * Add openSnackbar test & binded prop
This commit is contained in:
parent
9db3f383e3
commit
04ed53e0f2
@ -17,6 +17,8 @@
|
|||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import { newError } from '~/redux/actions';
|
import { newError } from '~/redux/actions';
|
||||||
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
|
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
|
||||||
@ -25,13 +27,14 @@ import { AddIcon, CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
|
|||||||
import Store from './store';
|
import Store from './store';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class AddContract extends Component {
|
class AddContract extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
contracts: PropTypes.object.isRequired,
|
contracts: PropTypes.object.isRequired,
|
||||||
|
newError: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func
|
onClose: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,7 +247,7 @@ export default class AddContract extends Component {
|
|||||||
this.onClose();
|
this.onClose();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
newError(error);
|
this.props.newError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,3 +255,14 @@ export default class AddContract extends Component {
|
|||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps (dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
newError
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AddContract);
|
||||||
|
@ -20,24 +20,27 @@ import sinon from 'sinon';
|
|||||||
|
|
||||||
import AddContract from './';
|
import AddContract from './';
|
||||||
|
|
||||||
import { CONTRACTS, createApi } from './addContract.test.js';
|
import { CONTRACTS, createApi, createRedux } from './addContract.test.js';
|
||||||
|
|
||||||
|
let api;
|
||||||
let component;
|
let component;
|
||||||
|
let instance;
|
||||||
let onClose;
|
let onClose;
|
||||||
|
let reduxStore;
|
||||||
|
|
||||||
function renderShallow (props) {
|
function render (props = {}) {
|
||||||
|
api = createApi();
|
||||||
onClose = sinon.stub();
|
onClose = sinon.stub();
|
||||||
|
reduxStore = createRedux();
|
||||||
|
|
||||||
component = shallow(
|
component = shallow(
|
||||||
<AddContract
|
<AddContract
|
||||||
{ ...props }
|
{ ...props }
|
||||||
contracts={ CONTRACTS }
|
contracts={ CONTRACTS }
|
||||||
onClose={ onClose } />,
|
onClose={ onClose } />,
|
||||||
{
|
{ context: { store: reduxStore } }
|
||||||
context: {
|
).find('AddContract').shallow({ context: { api } });
|
||||||
api: createApi()
|
instance = component.instance();
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
@ -45,11 +48,37 @@ function renderShallow (props) {
|
|||||||
describe('modals/AddContract', () => {
|
describe('modals/AddContract', () => {
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
renderShallow();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the defauls', () => {
|
it('renders the defauls', () => {
|
||||||
expect(component).to.be.ok;
|
expect(component).to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onAdd', () => {
|
||||||
|
it('calls store addContract', () => {
|
||||||
|
sinon.stub(instance.store, 'addContract').resolves(true);
|
||||||
|
return instance.onAdd().then(() => {
|
||||||
|
expect(instance.store.addContract).to.have.been.called;
|
||||||
|
instance.store.addContract.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls closes dialog on success', () => {
|
||||||
|
sinon.stub(instance.store, 'addContract').resolves(true);
|
||||||
|
return instance.onAdd().then(() => {
|
||||||
|
expect(onClose).to.have.been.called;
|
||||||
|
instance.store.addContract.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds newError on failure', () => {
|
||||||
|
sinon.stub(instance.store, 'addContract').rejects('test');
|
||||||
|
return instance.onAdd().then(() => {
|
||||||
|
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
|
||||||
|
instance.store.addContract.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -31,8 +31,19 @@ function createApi () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRedux () {
|
||||||
|
return {
|
||||||
|
dispatch: sinon.stub(),
|
||||||
|
subscribe: sinon.stub(),
|
||||||
|
getState: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ABI,
|
ABI,
|
||||||
CONTRACTS,
|
CONTRACTS,
|
||||||
createApi
|
createApi,
|
||||||
|
createRedux
|
||||||
};
|
};
|
||||||
|
@ -17,20 +17,24 @@
|
|||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
|
import { newError } from '~/redux/actions';
|
||||||
import { Button, Form, Input, InputChip, Modal } from '~/ui';
|
import { Button, Form, Input, InputChip, Modal } from '~/ui';
|
||||||
import { CancelIcon, SaveIcon } from '~/ui/Icons';
|
import { CancelIcon, SaveIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
import Store from './store';
|
import Store from './store';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class EditMeta extends Component {
|
class EditMeta extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: PropTypes.object.isRequired,
|
account: PropTypes.object.isRequired,
|
||||||
|
newError: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired
|
onClose: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +142,20 @@ export default class EditMeta extends Component {
|
|||||||
|
|
||||||
return this.store
|
return this.store
|
||||||
.save()
|
.save()
|
||||||
.then(() => this.props.onClose());
|
.then(() => this.props.onClose())
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.newError(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps (dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
newError
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(EditMeta);
|
||||||
|
@ -20,25 +20,27 @@ import sinon from 'sinon';
|
|||||||
|
|
||||||
import EditMeta from './';
|
import EditMeta from './';
|
||||||
|
|
||||||
import { ACCOUNT, createApi } from './editMeta.test.js';
|
import { ACCOUNT, createApi, createRedux } from './editMeta.test.js';
|
||||||
|
|
||||||
|
let api;
|
||||||
let component;
|
let component;
|
||||||
|
let instance;
|
||||||
let onClose;
|
let onClose;
|
||||||
|
let reduxStore;
|
||||||
|
|
||||||
function render (props) {
|
function render (props) {
|
||||||
|
api = createApi();
|
||||||
onClose = sinon.stub();
|
onClose = sinon.stub();
|
||||||
|
reduxStore = createRedux();
|
||||||
|
|
||||||
component = shallow(
|
component = shallow(
|
||||||
<EditMeta
|
<EditMeta
|
||||||
{ ...props }
|
{ ...props }
|
||||||
account={ ACCOUNT }
|
account={ ACCOUNT }
|
||||||
onClose={ onClose } />,
|
onClose={ onClose } />,
|
||||||
{
|
{ context: { store: reduxStore } }
|
||||||
context: {
|
).find('EditMeta').shallow({ context: { api } });
|
||||||
api: createApi()
|
instance = component.instance();
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
@ -56,15 +58,29 @@ describe('modals/EditMeta', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('onSave', () => {
|
describe('onSave', () => {
|
||||||
it('calls store.save() & props.onClose', () => {
|
it('calls store.save', () => {
|
||||||
const instance = component.instance();
|
|
||||||
sinon.spy(instance.store, 'save');
|
sinon.spy(instance.store, 'save');
|
||||||
|
|
||||||
instance.onSave().then(() => {
|
return instance.onSave().then(() => {
|
||||||
expect(instance.store.save).to.have.been.called;
|
expect(instance.store.save).to.have.been.called;
|
||||||
|
instance.store.save.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the dialog on success', () => {
|
||||||
|
return instance.onSave().then(() => {
|
||||||
expect(onClose).to.have.been.called;
|
expect(onClose).to.have.been.called;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds newError on failure', () => {
|
||||||
|
sinon.stub(instance.store, 'save').rejects('test');
|
||||||
|
|
||||||
|
return instance.onSave().then(() => {
|
||||||
|
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
|
||||||
|
instance.store.save.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -50,8 +50,19 @@ function createApi () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRedux () {
|
||||||
|
return {
|
||||||
|
dispatch: sinon.stub(),
|
||||||
|
subscribe: sinon.stub(),
|
||||||
|
getState: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ACCOUNT,
|
ACCOUNT,
|
||||||
ADDRESS,
|
ADDRESS,
|
||||||
createApi
|
createApi,
|
||||||
|
createRedux
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import { action, computed, observable, transaction } from 'mobx';
|
import { action, computed, observable, transaction } from 'mobx';
|
||||||
|
|
||||||
import { newError } from '~/redux/actions';
|
|
||||||
import { validateName } from '~/util/validation';
|
import { validateName } from '~/util/validation';
|
||||||
|
|
||||||
export default class Store {
|
export default class Store {
|
||||||
@ -92,8 +91,7 @@ export default class Store {
|
|||||||
])
|
])
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('onSave', error);
|
console.error('onSave', error);
|
||||||
|
throw error;
|
||||||
newError(error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,10 @@ import { Tabs, Tab } from 'material-ui/Tabs';
|
|||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import { newError, showSnackbar } from '~/redux/actions';
|
import { newError, openSnackbar } from '~/redux/actions';
|
||||||
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
|
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
|
||||||
import Form, { Input } from '~/ui/Form';
|
import Form, { Input } from '~/ui/Form';
|
||||||
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
|
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
|
||||||
@ -42,13 +44,15 @@ const TABS_ITEM_STYLE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class PasswordManager extends Component {
|
class PasswordManager extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: PropTypes.object.isRequired,
|
account: PropTypes.object.isRequired,
|
||||||
|
openSnackbar: PropTypes.func.isRequired,
|
||||||
|
newError: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func
|
onClose: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +340,7 @@ export default class PasswordManager extends Component {
|
|||||||
.changePassword()
|
.changePassword()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
showSnackbar(
|
this.props.openSnackbar(
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='passwordChange.success'
|
id='passwordChange.success'
|
||||||
@ -347,7 +351,7 @@ export default class PasswordManager extends Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
newError(error);
|
this.props.newError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,7 +359,19 @@ export default class PasswordManager extends Component {
|
|||||||
return this.store
|
return this.store
|
||||||
.testPassword()
|
.testPassword()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
newError(error);
|
this.props.newError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps (dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
openSnackbar,
|
||||||
|
newError
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(PasswordManager);
|
||||||
|
@ -20,25 +20,25 @@ import sinon from 'sinon';
|
|||||||
|
|
||||||
import PasswordManager from './';
|
import PasswordManager from './';
|
||||||
|
|
||||||
import { ACCOUNT, createApi } from './passwordManager.test.js';
|
import { ACCOUNT, createApi, createRedux } from './passwordManager.test.js';
|
||||||
|
|
||||||
let component;
|
let component;
|
||||||
|
let instance;
|
||||||
let onClose;
|
let onClose;
|
||||||
|
let reduxStore;
|
||||||
|
|
||||||
function render (props) {
|
function render (props) {
|
||||||
onClose = sinon.stub();
|
onClose = sinon.stub();
|
||||||
|
reduxStore = createRedux();
|
||||||
|
|
||||||
component = shallow(
|
component = shallow(
|
||||||
<PasswordManager
|
<PasswordManager
|
||||||
{ ...props }
|
{ ...props }
|
||||||
account={ ACCOUNT }
|
account={ ACCOUNT }
|
||||||
onClose={ onClose } />,
|
onClose={ onClose } />,
|
||||||
{
|
{ context: { store: reduxStore } }
|
||||||
context: {
|
).find('PasswordManager').shallow({ context: { api: createApi() } });
|
||||||
api: createApi()
|
instance = component.instance();
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
@ -56,24 +56,53 @@ describe('modals/PasswordManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('changePassword', () => {
|
describe('changePassword', () => {
|
||||||
it('calls store.changePassword & props.onClose', () => {
|
it('calls store.changePassword', () => {
|
||||||
const instance = component.instance();
|
|
||||||
sinon.spy(instance.store, 'changePassword');
|
sinon.spy(instance.store, 'changePassword');
|
||||||
|
|
||||||
instance.changePassword().then(() => {
|
return instance.changePassword().then(() => {
|
||||||
expect(instance.store.changePassword).to.have.been.called;
|
expect(instance.store.changePassword).to.have.been.called;
|
||||||
|
instance.store.changePassword.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the dialog on success', () => {
|
||||||
|
return instance.changePassword().then(() => {
|
||||||
expect(onClose).to.have.been.called;
|
expect(onClose).to.have.been.called;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows snackbar on success', () => {
|
||||||
|
return instance.changePassword().then(() => {
|
||||||
|
expect(reduxStore.dispatch).to.have.been.calledWithMatch({ type: 'openSnackbar' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds newError on failure', () => {
|
||||||
|
sinon.stub(instance.store, 'changePassword').rejects('test');
|
||||||
|
|
||||||
|
return instance.changePassword().then(() => {
|
||||||
|
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
|
||||||
|
instance.store.changePassword.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('testPassword', () => {
|
describe('testPassword', () => {
|
||||||
it('calls store.testPassword', () => {
|
it('calls store.testPassword', () => {
|
||||||
const instance = component.instance();
|
|
||||||
sinon.spy(instance.store, 'testPassword');
|
sinon.spy(instance.store, 'testPassword');
|
||||||
|
|
||||||
instance.testPassword().then(() => {
|
return instance.testPassword().then(() => {
|
||||||
expect(instance.store.testPassword).to.have.been.called;
|
expect(instance.store.testPassword).to.have.been.called;
|
||||||
|
instance.store.testPassword.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds newError on failure', () => {
|
||||||
|
sinon.stub(instance.store, 'testPassword').rejects('test');
|
||||||
|
|
||||||
|
return instance.testPassword().then(() => {
|
||||||
|
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
|
||||||
|
instance.store.testPassword.restore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -37,7 +37,18 @@ function createApi (result = true) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRedux () {
|
||||||
|
return {
|
||||||
|
dispatch: sinon.stub(),
|
||||||
|
subscribe: sinon.stub(),
|
||||||
|
getState: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ACCOUNT,
|
ACCOUNT,
|
||||||
createApi
|
createApi,
|
||||||
|
createRedux
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import { newError } from '~/ui/Errors/actions';
|
import { newError } from '~/ui/Errors/actions';
|
||||||
import { setAddressImage } from './providers/imagesActions';
|
import { setAddressImage } from './providers/imagesActions';
|
||||||
import { showSnackbar } from './providers/snackbarActions';
|
import { openSnackbar, showSnackbar } from './providers/snackbarActions';
|
||||||
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
|
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
|
||||||
import { toggleView } from '~/views/Settings/actions';
|
import { toggleView } from '~/views/Settings/actions';
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ export {
|
|||||||
newError,
|
newError,
|
||||||
clearStatusLogs,
|
clearStatusLogs,
|
||||||
setAddressImage,
|
setAddressImage,
|
||||||
|
openSnackbar,
|
||||||
showSnackbar,
|
showSnackbar,
|
||||||
toggleStatusLogs,
|
toggleStatusLogs,
|
||||||
toggleStatusRefresh,
|
toggleStatusRefresh,
|
||||||
|
@ -20,7 +20,7 @@ export function showSnackbar (message, cooldown) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSnackbar (message, cooldown) {
|
export function openSnackbar (message, cooldown) {
|
||||||
return {
|
return {
|
||||||
type: 'openSnackbar',
|
type: 'openSnackbar',
|
||||||
message, cooldown
|
message, cooldown
|
||||||
|
Loading…
Reference in New Issue
Block a user