mirror of
https://github.com/ascribe/onion.git
synced 2024-12-23 01:39:36 +01:00
Merged in AD-598-register-action-switches (pull request #14)
Ad 598 register action switches
This commit is contained in:
commit
78049b448e
@ -13,7 +13,7 @@ class UserActions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchCurrentUser() {
|
fetchCurrentUser() {
|
||||||
UserFetcher.fetchOne()
|
return UserFetcher.fetchOne()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.actions.updateCurrentUser(res.users[0]);
|
this.actions.updateCurrentUser(res.users[0]);
|
||||||
})
|
})
|
||||||
|
@ -25,7 +25,8 @@ let LoginForm = React.createClass({
|
|||||||
headerMessage: React.PropTypes.string,
|
headerMessage: React.PropTypes.string,
|
||||||
submitMessage: React.PropTypes.string,
|
submitMessage: React.PropTypes.string,
|
||||||
redirectOnLoggedIn: React.PropTypes.bool,
|
redirectOnLoggedIn: React.PropTypes.bool,
|
||||||
redirectOnLoginSuccess: React.PropTypes.bool
|
redirectOnLoginSuccess: React.PropTypes.bool,
|
||||||
|
onLogin: React.PropTypes.func
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [Router.Navigation],
|
mixins: [Router.Navigation],
|
||||||
@ -70,8 +71,9 @@ let LoginForm = React.createClass({
|
|||||||
// The easiest way to check if the user was successfully logged in is to fetch the user
|
// The easiest way to check if the user was successfully logged in is to fetch the user
|
||||||
// in the user store (which is obviously only possible if the user is logged in), since
|
// in the user store (which is obviously only possible if the user is logged in), since
|
||||||
// register_piece is listening to the changes of the user_store.
|
// register_piece is listening to the changes of the user_store.
|
||||||
UserActions.fetchCurrentUser();
|
UserActions.fetchCurrentUser()
|
||||||
|
.then(() => {
|
||||||
|
if(this.props.redirectOnLoginSuccess) {
|
||||||
/* Taken from http://stackoverflow.com/a/14916411 */
|
/* Taken from http://stackoverflow.com/a/14916411 */
|
||||||
/*
|
/*
|
||||||
We actually have to trick the Browser into showing the "save password" dialog
|
We actually have to trick the Browser into showing the "save password" dialog
|
||||||
@ -79,9 +81,19 @@ let LoginForm = React.createClass({
|
|||||||
Users on Stack Overflow claim this is a bug in chrome and should be fixed in the future.
|
Users on Stack Overflow claim this is a bug in chrome and should be fixed in the future.
|
||||||
Until then, we redirect the HARD way, but reloading the whole page using window.location
|
Until then, we redirect the HARD way, but reloading the whole page using window.location
|
||||||
*/
|
*/
|
||||||
if(this.props.redirectOnLoginSuccess) {
|
|
||||||
window.location = AppConstants.baseUrl + 'collection';
|
window.location = AppConstants.baseUrl + 'collection';
|
||||||
|
} else if(this.props.onLogin) {
|
||||||
|
// In some instances we want to give a callback to an outer container,
|
||||||
|
// to show that the one login action the user triggered actually went through.
|
||||||
|
// We can not do this by listening on a store's state as it wouldn't really tell us
|
||||||
|
// if the user did log in or was just fetching the user's data again
|
||||||
|
this.props.onLogin();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.logGlobal(err);
|
||||||
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -21,8 +21,9 @@ let RegisterPieceForm = React.createClass({
|
|||||||
headerMessage: React.PropTypes.string,
|
headerMessage: React.PropTypes.string,
|
||||||
submitMessage: React.PropTypes.string,
|
submitMessage: React.PropTypes.string,
|
||||||
handleSuccess: React.PropTypes.func,
|
handleSuccess: React.PropTypes.func,
|
||||||
isFineUploaderEditable: React.PropTypes.bool,
|
isFineUploaderActive: React.PropTypes.bool,
|
||||||
children: React.PropTypes.element
|
children: React.PropTypes.element,
|
||||||
|
onLoggedOut: React.PropTypes.func
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
@ -94,7 +95,8 @@ let RegisterPieceForm = React.createClass({
|
|||||||
submitKey={this.submitKey}
|
submitKey={this.submitKey}
|
||||||
setIsUploadReady={this.setIsUploadReady}
|
setIsUploadReady={this.setIsUploadReady}
|
||||||
isReadyForFormSubmission={this.isReadyForFormSubmission}
|
isReadyForFormSubmission={this.isReadyForFormSubmission}
|
||||||
editable={this.props.isFineUploaderEditable}/>
|
isFineUploaderActive={this.props.isFineUploaderActive}
|
||||||
|
onLoggedOut={this.props.onLoggedOut}/>
|
||||||
</Property>
|
</Property>
|
||||||
<Property
|
<Property
|
||||||
name='artist_name'
|
name='artist_name'
|
||||||
@ -133,10 +135,11 @@ let FileUploader = React.createClass({
|
|||||||
submitKey: React.PropTypes.func,
|
submitKey: React.PropTypes.func,
|
||||||
isReadyForFormSubmission: React.PropTypes.func,
|
isReadyForFormSubmission: React.PropTypes.func,
|
||||||
onClick: React.PropTypes.func,
|
onClick: React.PropTypes.func,
|
||||||
// editable is used to lock react fine uploader in case
|
// isFineUploaderActive is used to lock react fine uploader in case
|
||||||
// a user is actually not logged in already to prevent him from droping files
|
// a user is actually not logged in already to prevent him from droping files
|
||||||
// before login in
|
// before login in
|
||||||
editable: React.PropTypes.bool
|
isFineUploaderActive: React.PropTypes.bool,
|
||||||
|
onLoggedOut: React.PropTypes.func
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -158,7 +161,7 @@ let FileUploader = React.createClass({
|
|||||||
setIsUploadReady={this.props.setIsUploadReady}
|
setIsUploadReady={this.props.setIsUploadReady}
|
||||||
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
|
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
|
||||||
areAssetsDownloadable={false}
|
areAssetsDownloadable={false}
|
||||||
areAssetsEditable={this.props.editable}
|
areAssetsEditable={this.props.isFineUploaderActive}
|
||||||
signature={{
|
signature={{
|
||||||
endpoint: AppConstants.serverUrl + 's3/signature/',
|
endpoint: AppConstants.serverUrl + 's3/signature/',
|
||||||
customHeaders: {
|
customHeaders: {
|
||||||
@ -172,7 +175,8 @@ let FileUploader = React.createClass({
|
|||||||
customHeaders: {
|
customHeaders: {
|
||||||
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
||||||
}
|
}
|
||||||
}}/>
|
}}
|
||||||
|
onInactive={this.props.onLoggedOut}/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,7 @@ let SlidesContainer = React.createClass({
|
|||||||
getInitialState() {
|
getInitialState() {
|
||||||
// handle queryParameters
|
// handle queryParameters
|
||||||
let queryParams = this.getQuery();
|
let queryParams = this.getQuery();
|
||||||
let slideNum = 0;
|
let slideNum = -1;
|
||||||
|
|
||||||
if(queryParams && 'slide_num' in queryParams) {
|
if(queryParams && 'slide_num' in queryParams) {
|
||||||
slideNum = parseInt(queryParams.slide_num, 10);
|
slideNum = parseInt(queryParams.slide_num, 10);
|
||||||
@ -26,17 +26,12 @@ let SlidesContainer = React.createClass({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
containerWidth: 0,
|
containerWidth: 0,
|
||||||
slideNum: slideNum
|
slideNum: slideNum,
|
||||||
|
historyLength: window.history.length
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// check if slide_num was defined, and if not then default to 0
|
|
||||||
let queryParams = this.getQuery();
|
|
||||||
if(!('slide_num' in queryParams)) {
|
|
||||||
this.replaceWith(this.getPathname(), null, {slide_num: 0});
|
|
||||||
}
|
|
||||||
|
|
||||||
// init container width
|
// init container width
|
||||||
this.handleContainerResize();
|
this.handleContainerResize();
|
||||||
|
|
||||||
@ -45,6 +40,12 @@ let SlidesContainer = React.createClass({
|
|||||||
window.addEventListener('resize', this.handleContainerResize);
|
window.addEventListener('resize', this.handleContainerResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
// check if slide_num was defined, and if not then default to 0
|
||||||
|
let queryParams = this.getQuery();
|
||||||
|
this.setSlideNum(queryParams.slide_num);
|
||||||
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.handleContainerResize);
|
window.removeEventListener('resize', this.handleContainerResize);
|
||||||
},
|
},
|
||||||
@ -58,9 +59,45 @@ let SlidesContainer = React.createClass({
|
|||||||
// We let every one from the outsite set the page number of the slider,
|
// We let every one from the outsite set the page number of the slider,
|
||||||
// though only if the slideNum is actually in the range of our children-list.
|
// though only if the slideNum is actually in the range of our children-list.
|
||||||
setSlideNum(slideNum) {
|
setSlideNum(slideNum) {
|
||||||
if(slideNum < 0 || slideNum < React.Children.count(this.props.children)) {
|
|
||||||
|
// slideNum can in some instances be not a number,
|
||||||
|
// therefore we have to parse it to one and make sure that its not NaN
|
||||||
|
slideNum = parseInt(slideNum, 10);
|
||||||
|
|
||||||
|
// if slideNum is not a number (even after we parsed it to one) and there has
|
||||||
|
// never been a transition to another slide (this.state.slideNum ==== -1 indicates that)
|
||||||
|
// then we want to "replace" (in this case append) the current url with ?slide_num=0
|
||||||
|
if(isNaN(slideNum) && this.state.slideNum === -1) {
|
||||||
|
slideNum = 0;
|
||||||
|
|
||||||
this.replaceWith(this.getPathname(), null, {slide_num: slideNum});
|
this.replaceWith(this.getPathname(), null, {slide_num: slideNum});
|
||||||
|
this.setState({slideNum: slideNum});
|
||||||
|
return;
|
||||||
|
|
||||||
|
// slideNum always represents the future state. So if slideNum and
|
||||||
|
// this.state.slideNum are equal, there is no sense in redirecting
|
||||||
|
} else if(slideNum === this.state.slideNum) {
|
||||||
|
return;
|
||||||
|
|
||||||
|
// if slideNum is within the range of slides and none of the previous cases
|
||||||
|
// where matched, we can actually do transitions
|
||||||
|
} else if(slideNum >= 0 || slideNum < React.Children.count(this.props.children)) {
|
||||||
|
|
||||||
|
if(slideNum !== this.state.slideNum - 1) {
|
||||||
|
// Bootstrapping the component, getInitialState is called once to save
|
||||||
|
// the tabs history length.
|
||||||
|
// In order to know if we already pushed a new state on the history stack or not,
|
||||||
|
// we're comparing the old history length with the new one and if it didn't change then
|
||||||
|
// we push a new state on it ONCE (ever).
|
||||||
|
// Otherwise, we're able to use the browsers history.forward() method
|
||||||
|
// to keep the stack clean
|
||||||
|
if(this.state.historyLength === window.history.length) {
|
||||||
|
this.transitionTo(this.getPathname(), null, {slide_num: slideNum});
|
||||||
|
} else {
|
||||||
|
window.history.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
slideNum: slideNum
|
slideNum: slideNum
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ let FileDragAndDrop = React.createClass({
|
|||||||
onDragLeave: React.PropTypes.func,
|
onDragLeave: React.PropTypes.func,
|
||||||
onDragOver: React.PropTypes.func,
|
onDragOver: React.PropTypes.func,
|
||||||
onDragEnd: React.PropTypes.func,
|
onDragEnd: React.PropTypes.func,
|
||||||
|
onInactive: React.PropTypes.func,
|
||||||
filesToUpload: React.PropTypes.array,
|
filesToUpload: React.PropTypes.array,
|
||||||
handleDeleteFile: React.PropTypes.func,
|
handleDeleteFile: React.PropTypes.func,
|
||||||
handleCancelFile: React.PropTypes.func,
|
handleCancelFile: React.PropTypes.func,
|
||||||
@ -72,6 +73,15 @@ let FileDragAndDrop = React.createClass({
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let files;
|
let files;
|
||||||
|
|
||||||
|
if(this.props.dropzoneInactive) {
|
||||||
|
// if there is a handle function for doing stuff
|
||||||
|
// when the dropzone is inactive, then call it
|
||||||
|
if(this.props.onInactive) {
|
||||||
|
this.props.onInactive();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// handle Drag and Drop
|
// handle Drag and Drop
|
||||||
if(event.dataTransfer && event.dataTransfer.files.length > 0) {
|
if(event.dataTransfer && event.dataTransfer.files.length > 0) {
|
||||||
files = event.dataTransfer.files;
|
files = event.dataTransfer.files;
|
||||||
@ -113,10 +123,15 @@ let FileDragAndDrop = React.createClass({
|
|||||||
this.props.handleResumeFile(fileId);
|
this.props.handleResumeFile(fileId);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleOnClick(event) {
|
handleOnClick() {
|
||||||
// when multiple is set to false and the user already uploaded a piece,
|
// when multiple is set to false and the user already uploaded a piece,
|
||||||
// do not propagate event
|
// do not propagate event
|
||||||
if(this.props.dropzoneInactive) {
|
if(this.props.dropzoneInactive) {
|
||||||
|
// if there is a handle function for doing stuff
|
||||||
|
// when the dropzone is inactive, then call it
|
||||||
|
if(this.props.onInactive) {
|
||||||
|
this.props.onInactive();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,8 @@ var ReactS3FineUploader = React.createClass({
|
|||||||
isReadyForFormSubmission: React.PropTypes.func,
|
isReadyForFormSubmission: React.PropTypes.func,
|
||||||
areAssetsDownloadable: React.PropTypes.bool,
|
areAssetsDownloadable: React.PropTypes.bool,
|
||||||
areAssetsEditable: React.PropTypes.bool,
|
areAssetsEditable: React.PropTypes.bool,
|
||||||
defaultErrorMessage: React.PropTypes.string
|
defaultErrorMessage: React.PropTypes.string,
|
||||||
|
onInactive: React.PropTypes.func
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
@ -560,7 +561,8 @@ var ReactS3FineUploader = React.createClass({
|
|||||||
multiple={this.props.multiple}
|
multiple={this.props.multiple}
|
||||||
areAssetsDownloadable={this.props.areAssetsDownloadable}
|
areAssetsDownloadable={this.props.areAssetsDownloadable}
|
||||||
areAssetsEditable={this.props.areAssetsEditable}
|
areAssetsEditable={this.props.areAssetsEditable}
|
||||||
dropzoneInactive={!this.props.areAssetsEditable || !this.props.multiple && this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0} />
|
dropzoneInactive={!this.props.areAssetsEditable || !this.props.multiple && this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0}
|
||||||
|
onInactive={this.props.onInactive}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ let LoginContainer = React.createClass({
|
|||||||
propTypes: {
|
propTypes: {
|
||||||
message: React.PropTypes.string,
|
message: React.PropTypes.string,
|
||||||
redirectOnLoggedIn: React.PropTypes.bool,
|
redirectOnLoggedIn: React.PropTypes.bool,
|
||||||
redirectOnLoginSuccess: React.PropTypes.bool
|
redirectOnLoginSuccess: React.PropTypes.bool,
|
||||||
|
onLogin: React.PropTypes.func
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
@ -31,7 +32,8 @@ let LoginContainer = React.createClass({
|
|||||||
<LoginForm
|
<LoginForm
|
||||||
redirectOnLoggedIn={this.props.redirectOnLoggedIn}
|
redirectOnLoggedIn={this.props.redirectOnLoggedIn}
|
||||||
redirectOnLoginSuccess={this.props.redirectOnLoginSuccess}
|
redirectOnLoginSuccess={this.props.redirectOnLoginSuccess}
|
||||||
message={this.props.message} />
|
message={this.props.message}
|
||||||
|
onLogin={this.props.onLogin}/>
|
||||||
<div className="ascribe-login-text">
|
<div className="ascribe-login-text">
|
||||||
{getLangText('Not an ascribe user')}? <Link to="signup">{getLangText('Sign up')}...</Link><br/>
|
{getLangText('Not an ascribe user')}? <Link to="signup">{getLangText('Sign up')}...</Link><br/>
|
||||||
{getLangText('Forgot my password')}? <Link to="password_reset">{getLangText('Rescue me')}...</Link>
|
{getLangText('Forgot my password')}? <Link to="password_reset">{getLangText('Rescue me')}...</Link>
|
||||||
|
@ -59,7 +59,7 @@ let RegisterPiece = React.createClass( {
|
|||||||
PieceListStore.getState(),
|
PieceListStore.getState(),
|
||||||
{
|
{
|
||||||
selectedLicense: 0,
|
selectedLicense: 0,
|
||||||
isFineUploaderEditable: false
|
isFineUploaderActive: false
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -82,14 +82,10 @@ let RegisterPiece = React.createClass( {
|
|||||||
onChange(state) {
|
onChange(state) {
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
|
|
||||||
// once the currentUser object from UserStore is defined (eventually the user was transitioned
|
if(this.state.currentUser && this.state.currentUser.email) {
|
||||||
// to the login form via the slider and successfully logged in), we can direct him back to the
|
|
||||||
// register_piece slide
|
|
||||||
if(state.currentUser && state.currentUser.email || this.state.currentUser && this.state.currentUser.email) {
|
|
||||||
this.refs.slidesContainer.setSlideNum(0);
|
|
||||||
// we should also make the fineuploader component editable again
|
// we should also make the fineuploader component editable again
|
||||||
this.setState({
|
this.setState({
|
||||||
isFineUploaderEditable: true
|
isFineUploaderActive: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -105,7 +101,8 @@ let RegisterPiece = React.createClass( {
|
|||||||
this.state.pageSize,
|
this.state.pageSize,
|
||||||
this.state.searchTerm,
|
this.state.searchTerm,
|
||||||
this.state.orderBy,
|
this.state.orderBy,
|
||||||
this.state.orderAsc);
|
this.state.orderAsc
|
||||||
|
);
|
||||||
|
|
||||||
this.transitionTo('piece', {pieceId: response.piece.id});
|
this.transitionTo('piece', {pieceId: response.piece.id});
|
||||||
},
|
},
|
||||||
@ -160,11 +157,25 @@ let RegisterPiece = React.createClass( {
|
|||||||
changeSlide() {
|
changeSlide() {
|
||||||
// only transition to the login store, if user is not logged in
|
// only transition to the login store, if user is not logged in
|
||||||
// ergo the currentUser object is not properly defined
|
// ergo the currentUser object is not properly defined
|
||||||
if(!this.state.currentUser.email) {
|
if(this.state.currentUser && !this.state.currentUser.email) {
|
||||||
this.refs.slidesContainer.setSlideNum(1);
|
this.refs.slidesContainer.setSlideNum(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// basically redirects to the second slide (index: 1), when the user is not logged in
|
||||||
|
onLoggedOut() {
|
||||||
|
this.refs.slidesContainer.setSlideNum(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
onLogin() {
|
||||||
|
// once the currentUser object from UserStore is defined (eventually the user was transitioned
|
||||||
|
// to the login form via the slider and successfully logged in), we can direct him back to the
|
||||||
|
// register_piece slide
|
||||||
|
if(this.state.currentUser && this.state.currentUser.email) {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SlidesContainer ref="slidesContainer">
|
<SlidesContainer ref="slidesContainer">
|
||||||
@ -175,8 +186,9 @@ let RegisterPiece = React.createClass( {
|
|||||||
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
|
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
|
||||||
<RegisterPieceForm
|
<RegisterPieceForm
|
||||||
{...this.props}
|
{...this.props}
|
||||||
isFineUploaderEditable={this.state.isFineUploaderEditable}
|
isFineUploaderActive={this.state.isFineUploaderActive}
|
||||||
handleSuccess={this.handleSuccess}>
|
handleSuccess={this.handleSuccess}
|
||||||
|
onLoggedOut={this.onLoggedOut}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
{this.getLicenses()}
|
{this.getLicenses()}
|
||||||
{this.getSpecifyEditions()}
|
{this.getSpecifyEditions()}
|
||||||
@ -188,7 +200,8 @@ let RegisterPiece = React.createClass( {
|
|||||||
<LoginContainer
|
<LoginContainer
|
||||||
message={getLangText('Please login before ascribing your work%s', '...')}
|
message={getLangText('Please login before ascribing your work%s', '...')}
|
||||||
redirectOnLoggedIn={false}
|
redirectOnLoggedIn={false}
|
||||||
redirectOnLoginSuccess={false}/>
|
redirectOnLoginSuccess={false}
|
||||||
|
onLogin={this.onLogin}/>
|
||||||
</div>
|
</div>
|
||||||
</SlidesContainer>
|
</SlidesContainer>
|
||||||
);
|
);
|
||||||
|
@ -43,6 +43,7 @@ html {
|
|||||||
.ascribe-default-app {
|
.ascribe-default-app {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
padding-top: 70px;
|
padding-top: 70px;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
Loading…
Reference in New Issue
Block a user