refactor: 💡 React components to consume appConfig using Context (#852)
* refactor: 💡 React components to consume appConfig using Context React components to consume app configuration using React Context and React Hooks. Non React components to continue using global variable window.config. Related documentation also changed. Closes: #725 * refactor: 💡 Removing unecessary code On current React component there is no need to import useContext method from React, so, removing it. * refactor: 💡 Code review Code review. Minor changes based on review inputs and moving userManager to an init method BREAKING CHANGE: #725 Closes: #725 * docs: don't include implementation detail in docs * docs: don't include implementation detail in docs * docs: no need to specify implementation details in employment recipe * docs: no need to specify implementation details in deployment recipe
This commit is contained in:
parent
a187783e0d
commit
7c4ee734fa
@ -119,8 +119,7 @@ window.config = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
- Install the viewer:
|
- Install the viewer: `window.OHIFStandaloneViewer.installViewer(window.config);`
|
||||||
`window.OHIFStandaloneViewer.installViewer(window.config);`
|
|
||||||
|
|
||||||
This exact setup is demonstrated in this
|
This exact setup is demonstrated in this
|
||||||
[CodeSandbox](https://codesandbox.io/s/ohif-viewer-script-tag-usage-c4u4t) and
|
[CodeSandbox](https://codesandbox.io/s/ohif-viewer-script-tag-usage-c4u4t) and
|
||||||
|
|||||||
@ -121,11 +121,7 @@ likely want to update:
|
|||||||
|
|
||||||
#### OHIF Viewer
|
#### OHIF Viewer
|
||||||
|
|
||||||
The OHIF Viewer's configuration is imported from a static `.js` file and made
|
The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
|
||||||
available globally at `window.config`. The configuration we use is set to a
|
|
||||||
specific file when we build the viewer, and determined by the env variable:
|
|
||||||
`APP_CONFIG`. You can see where we set its value in the `dockerfile` for this
|
|
||||||
solution:
|
|
||||||
|
|
||||||
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
||||||
|
|
||||||
|
|||||||
@ -122,11 +122,7 @@ likely want to update:
|
|||||||
|
|
||||||
#### OHIF Viewer
|
#### OHIF Viewer
|
||||||
|
|
||||||
The OHIF Viewer's configuration is imported from a static `.js` file and made
|
The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
|
||||||
available globally at `window.config`. The configuration we use is set to a
|
|
||||||
specific file when we build the viewer, and determined by the env variable:
|
|
||||||
`APP_CONFIG`. You can see where we set its value in the `dockerfile` for this
|
|
||||||
solution:
|
|
||||||
|
|
||||||
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
|
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
|
||||||
|
|
||||||
|
|||||||
@ -25,12 +25,15 @@ import { OidcProvider } from 'redux-oidc';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
import WhiteLabellingContext from './WhiteLabellingContext';
|
|
||||||
import { getActiveContexts } from './store/layout/selectors.js';
|
import { getActiveContexts } from './store/layout/selectors.js';
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import setupTools from './setupTools.js';
|
import setupTools from './setupTools.js';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import UserManagerContext from './UserManagerContext';
|
|
||||||
|
// Contexts
|
||||||
|
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||||
|
import UserManagerContext from './context/UserManagerContext';
|
||||||
|
import AppContext from './context/AppContext';
|
||||||
|
|
||||||
// ~~~~ APP SETUP
|
// ~~~~ APP SETUP
|
||||||
initCornerstoneTools({
|
initCornerstoneTools({
|
||||||
@ -74,10 +77,66 @@ class App extends Component {
|
|||||||
extensions: [],
|
extensions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_appConfig;
|
||||||
|
_userManager;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
if (this.props.oidc.length) {
|
this.appConfig = props;
|
||||||
|
const { servers, extensions, hotkeys, oidc } = props;
|
||||||
|
|
||||||
|
this.initUserManager(oidc);
|
||||||
|
_initExtensions(extensions, hotkeys);
|
||||||
|
_initServers(servers);
|
||||||
|
initWebWorkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const userManager = this._userManager;
|
||||||
|
const config = {
|
||||||
|
appConfig: this._appConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userManager) {
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={config}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<OidcProvider store={store} userManager={userManager}>
|
||||||
|
<UserManagerContext.Provider value={userManager}>
|
||||||
|
<Router basename={this.props.routerBasename}>
|
||||||
|
<WhiteLabellingContext.Provider
|
||||||
|
value={this.props.whiteLabelling}
|
||||||
|
>
|
||||||
|
<OHIFStandaloneViewer userManager={userManager} />
|
||||||
|
</WhiteLabellingContext.Provider>
|
||||||
|
</Router>
|
||||||
|
</UserManagerContext.Provider>
|
||||||
|
</OidcProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={config}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<Router basename={this.props.routerBasename}>
|
||||||
|
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
||||||
|
<OHIFStandaloneViewer />
|
||||||
|
</WhiteLabellingContext.Provider>
|
||||||
|
</Router>
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
initUserManager(oidc) {
|
||||||
|
if (oidc && !!oidc.length) {
|
||||||
const firstOpenIdClient = this.props.oidc[0];
|
const firstOpenIdClient = this.props.oidc[0];
|
||||||
|
|
||||||
const { protocol, host } = window.location;
|
const { protocol, host } = window.location;
|
||||||
@ -102,58 +161,18 @@ class App extends Component {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userManager = getUserManagerForOpenIdConnectClient(
|
this._userManager = getUserManagerForOpenIdConnectClient(
|
||||||
store,
|
store,
|
||||||
openIdConnectConfiguration
|
openIdConnectConfiguration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initExtensions(this.props.extensions);
|
|
||||||
_initServers(this.props.servers);
|
|
||||||
initWebWorkers();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const userManager = this.userManager;
|
|
||||||
|
|
||||||
if (userManager) {
|
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<OidcProvider store={store} userManager={userManager}>
|
|
||||||
<UserManagerContext.Provider value={userManager}>
|
|
||||||
<Router basename={this.props.routerBasename}>
|
|
||||||
<WhiteLabellingContext.Provider
|
|
||||||
value={this.props.whiteLabelling}
|
|
||||||
>
|
|
||||||
<OHIFStandaloneViewer userManager={userManager} />
|
|
||||||
</WhiteLabellingContext.Provider>
|
|
||||||
</Router>
|
|
||||||
</UserManagerContext.Provider>
|
|
||||||
</OidcProvider>
|
|
||||||
</I18nextProvider>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<Router basename={this.props.routerBasename}>
|
|
||||||
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
|
||||||
<OHIFStandaloneViewer />
|
|
||||||
</WhiteLabellingContext.Provider>
|
|
||||||
</Router>
|
|
||||||
</I18nextProvider>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param
|
* @param
|
||||||
*/
|
*/
|
||||||
function _initExtensions(extensions) {
|
function _initExtensions(extensions, hotkeys) {
|
||||||
const defaultExtensions = [
|
const defaultExtensions = [
|
||||||
GenericViewerCommands,
|
GenericViewerCommands,
|
||||||
MeasurementsPanel,
|
MeasurementsPanel,
|
||||||
@ -163,8 +182,8 @@ function _initExtensions(extensions) {
|
|||||||
extensionManager.registerExtensions(mergedExtensions);
|
extensionManager.registerExtensions(mergedExtensions);
|
||||||
|
|
||||||
// Must run after extension commands are registered
|
// Must run after extension commands are registered
|
||||||
if (window.config.hotkeys) {
|
if (hotkeys) {
|
||||||
hotkeysManager.setHotkeys(window.config.hotkeys, true);
|
hotkeysManager.setHotkeys(hotkeys, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,9 @@ import './OHIFStandaloneViewer.css';
|
|||||||
import './variables.css';
|
import './variables.css';
|
||||||
import './theme-tide.css';
|
import './theme-tide.css';
|
||||||
|
|
||||||
|
// Contexts
|
||||||
|
import AppContext from './context/AppContext';
|
||||||
|
|
||||||
// Dynamic Import Routes (CodeSplitting)
|
// Dynamic Import Routes (CodeSplitting)
|
||||||
// const IHEInvokeImageDisplay = asyncComponent(() =>
|
// const IHEInvokeImageDisplay = asyncComponent(() =>
|
||||||
// import('./routes/IHEInvokeImageDisplay.js')
|
// import('./routes/IHEInvokeImageDisplay.js')
|
||||||
@ -37,6 +40,7 @@ import './theme-tide.css';
|
|||||||
const reload = () => window.location.reload();
|
const reload = () => window.location.reload();
|
||||||
|
|
||||||
class OHIFStandaloneViewer extends Component {
|
class OHIFStandaloneViewer extends Component {
|
||||||
|
static contextType = AppContext;
|
||||||
state = {
|
state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
@ -63,7 +67,7 @@ class OHIFStandaloneViewer extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user, userManager } = this.props;
|
const { user, userManager } = this.props;
|
||||||
|
const { appConfig = {} } = this.context;
|
||||||
const userNotLoggedIn = userManager && (!user || user.expired);
|
const userNotLoggedIn = userManager && (!user || user.expired);
|
||||||
if (userNotLoggedIn) {
|
if (userNotLoggedIn) {
|
||||||
const pathname = this.props.location.pathname;
|
const pathname = this.props.location.pathname;
|
||||||
@ -140,9 +144,7 @@ class OHIFStandaloneViewer extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const showStudyList =
|
const showStudyList =
|
||||||
window.config && window.config.showStudyList !== undefined
|
appConfig.showStudyList !== undefined ? appConfig.showStudyList : true;
|
||||||
? window.config.showStudyList
|
|
||||||
: true;
|
|
||||||
if (showStudyList) {
|
if (showStudyList) {
|
||||||
routes.push({
|
routes.push({
|
||||||
path: '/studylist',
|
path: '/studylist',
|
||||||
|
|||||||
@ -11,13 +11,17 @@ import { AboutModal } from '@ohif/ui';
|
|||||||
import { hotkeysManager } from './../../App.js';
|
import { hotkeysManager } from './../../App.js';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
// Context
|
||||||
|
import AppContext from './../../context/AppContext';
|
||||||
|
|
||||||
class Header extends Component {
|
class Header extends Component {
|
||||||
|
static contextType = AppContext;
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
home: PropTypes.bool.isRequired,
|
home: PropTypes.bool.isRequired,
|
||||||
location: PropTypes.object.isRequired,
|
location: PropTypes.object.isRequired,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
userManager: PropTypes.object
|
userManager: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -63,10 +67,10 @@ class Header extends Component {
|
|||||||
if (this.props.user && this.props.userManager) {
|
if (this.props.user && this.props.userManager) {
|
||||||
this.options.push({
|
this.options.push({
|
||||||
title: t('Logout'),
|
title: t('Logout'),
|
||||||
icon: { name: 'power-off' },
|
icon: { name: 'power-off' },
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
this.props.userManager.signoutRedirect();
|
this.props.userManager.signoutRedirect();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,10 +86,9 @@ class Header extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
|
const { appConfig = {} } = this.context;
|
||||||
const showStudyList =
|
const showStudyList =
|
||||||
window.config.showStudyList !== undefined
|
appConfig.showStudyList !== undefined ? appConfig.showStudyList : true;
|
||||||
? window.config.showStudyList
|
|
||||||
: true;
|
|
||||||
return (
|
return (
|
||||||
<div className={`entry-header ${this.props.home ? 'header-big' : ''}`}>
|
<div className={`entry-header ${this.props.home ? 'header-big' : ''}`}>
|
||||||
<div className="header-left-box">
|
<div className="header-left-box">
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import classNames from 'classnames';
|
|||||||
import { MODULE_TYPES } from '@ohif/core';
|
import { MODULE_TYPES } from '@ohif/core';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import WhiteLabellingContext from '../WhiteLabellingContext.js';
|
|
||||||
import ConnectedHeader from './ConnectedHeader.js';
|
import ConnectedHeader from './ConnectedHeader.js';
|
||||||
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
||||||
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
|
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
|
||||||
@ -13,7 +12,11 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
|||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||||
import SidePanel from './../components/SidePanel.js';
|
import SidePanel from './../components/SidePanel.js';
|
||||||
import { extensionManager } from './../App.js';
|
import { extensionManager } from './../App.js';
|
||||||
import UserManagerContext from '../UserManagerContext';
|
|
||||||
|
// Contexts
|
||||||
|
import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
|
||||||
|
import UserManagerContext from '../context/UserManagerContext';
|
||||||
|
|
||||||
import './Viewer.css';
|
import './Viewer.css';
|
||||||
/**
|
/**
|
||||||
* Inits OHIF Hanging Protocol's onReady.
|
* Inits OHIF Hanging Protocol's onReady.
|
||||||
|
|||||||
5
platform/viewer/src/context/AppContext.js
Normal file
5
platform/viewer/src/context/AppContext.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
let AppContext = React.createContext({});
|
||||||
|
|
||||||
|
export default AppContext;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import OHIFLogo from './components/OHIFLogo/OHIFLogo.js';
|
import OHIFLogo from '../components/OHIFLogo/OHIFLogo.js';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const defaultContextValues = {
|
const defaultContextValues = {
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import ConnectedStudyList from './ConnectedStudyList';
|
import ConnectedStudyList from './ConnectedStudyList';
|
||||||
|
|
||||||
|
// Contexts
|
||||||
|
import AppContext from '../context/AppContext';
|
||||||
|
|
||||||
// TODO: Move to @ohif/ui
|
// TODO: Move to @ohif/ui
|
||||||
|
|
||||||
function toLowerCaseFirstLetter(word) {
|
function toLowerCaseFirstLetter(word) {
|
||||||
@ -22,16 +25,20 @@ function getFilters({ search }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StudyListRouting({ location }) {
|
function StudyListRouting({ location }) {
|
||||||
|
const { appConfig = {} } = useContext(AppContext);
|
||||||
|
|
||||||
const filters = location ? getFilters(location) : undefined;
|
const filters = location ? getFilters(location) : undefined;
|
||||||
|
|
||||||
let studyListFunctionsEnabled = false;
|
let studyListFunctionsEnabled = false;
|
||||||
if (window.config && window.config.studyListFunctionsEnabled) {
|
if (appConfig.studyListFunctionsEnabled) {
|
||||||
studyListFunctionsEnabled = window.config.studyListFunctionsEnabled;
|
studyListFunctionsEnabled = appConfig.studyListFunctionsEnabled;
|
||||||
}
|
}
|
||||||
return <ConnectedStudyList
|
return (
|
||||||
filters={filters}
|
<ConnectedStudyList
|
||||||
studyListFunctionsEnabled={studyListFunctionsEnabled}
|
filters={filters}
|
||||||
/>;
|
studyListFunctionsEnabled={studyListFunctionsEnabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
StudyListRouting.propTypes = {
|
StudyListRouting.propTypes = {
|
||||||
|
|||||||
@ -10,10 +10,14 @@ import moment from 'moment';
|
|||||||
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
|
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
|
||||||
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
|
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
|
||||||
import filesToStudies from '../lib/filesToStudies.js';
|
import filesToStudies from '../lib/filesToStudies.js';
|
||||||
import UserManagerContext from '../UserManagerContext';
|
|
||||||
import WhiteLabellingContext from '../WhiteLabellingContext';
|
// Contexts
|
||||||
|
import UserManagerContext from '../context/UserManagerContext';
|
||||||
|
import WhiteLabellingContext from '../context/WhiteLabellingContext';
|
||||||
|
import AppContext from '../context/AppContext';
|
||||||
|
|
||||||
class StudyListWithData extends Component {
|
class StudyListWithData extends Component {
|
||||||
|
static contextType = AppContext;
|
||||||
state = {
|
state = {
|
||||||
searchData: {},
|
searchData: {},
|
||||||
studies: [],
|
studies: [],
|
||||||
@ -51,9 +55,10 @@ class StudyListWithData extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const { appConfig = {} } = this.context;
|
||||||
// TODO: Avoid using timepoints here
|
// TODO: Avoid using timepoints here
|
||||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
||||||
if (!this.props.server && window.config.enableGoogleCloudAdapter) {
|
if (!this.props.server && appConfig.enableGoogleCloudAdapter) {
|
||||||
this.setState({
|
this.setState({
|
||||||
modalComponentId: 'DicomStorePicker',
|
modalComponentId: 'DicomStorePicker',
|
||||||
});
|
});
|
||||||
@ -184,6 +189,7 @@ class StudyListWithData extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { appConfig = {} } = this.context;
|
||||||
const onDrop = async acceptedFiles => {
|
const onDrop = async acceptedFiles => {
|
||||||
try {
|
try {
|
||||||
const studies = await filesToStudies(acceptedFiles);
|
const studies = await filesToStudies(acceptedFiles);
|
||||||
@ -203,8 +209,7 @@ class StudyListWithData extends Component {
|
|||||||
let healthCareApiButtons = null;
|
let healthCareApiButtons = null;
|
||||||
let healthCareApiWindows = null;
|
let healthCareApiWindows = null;
|
||||||
|
|
||||||
// TODO: This should probably be a prop
|
if (appConfig.enableGoogleCloudAdapter) {
|
||||||
if (window.config.enableGoogleCloudAdapter) {
|
|
||||||
healthCareApiWindows = (
|
healthCareApiWindows = (
|
||||||
<ConnectedDicomStorePicker
|
<ConnectedDicomStorePicker
|
||||||
isOpen={this.state.modalComponentId === 'DicomStorePicker'}
|
isOpen={this.state.modalComponentId === 'DicomStorePicker'}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user