about summary refs log tree commit diff
path: root/app/assets/javascripts/components/components
diff options
context:
space:
mode:
authorYamagishi Kazutoshi <ykzts@desire.sh>2017-04-22 03:05:35 +0900
committerEugen <eugen@zeonfederated.com>2017-04-21 20:05:35 +0200
commit1948f9e767c5c8f7cb52337ce777a61b5ad1a599 (patch)
tree4d5f3549f6e5fd88340bb3543e1eb2eafc47952c /app/assets/javascripts/components/components
parent27ea2a88c12835b491ea5537f934983470faf781 (diff)
Remove deprecated features at React v15.5 (#1905)
* Remove deprecated features at React v15.5

- [x] React.PropTypes
- [x] react-addons-pure-render-mixin
- [x] react-addons-test-utils

* Uncommented out & Add browserify_rails options

* re-add react-addons-shallow

* Fix syntax error from resolve conflicts

* follow up 59a77923b368d48c590cd9f4a0c6b73ce972d33f
Diffstat (limited to 'app/assets/javascripts/components/components')
-rw-r--r--app/assets/javascripts/components/components/account.jsx37
-rw-r--r--app/assets/javascripts/components/components/attachment_list.jsx14
-rw-r--r--app/assets/javascripts/components/components/autosuggest_textarea.jsx62
-rw-r--r--app/assets/javascripts/components/components/avatar.jsx47
-rw-r--r--app/assets/javascripts/components/components/button.jsx47
-rw-r--r--app/assets/javascripts/components/components/collapsable.jsx7
-rw-r--r--app/assets/javascripts/components/components/column_back_button.jsx21
-rw-r--r--app/assets/javascripts/components/components/column_back_button_slim.jsx21
-rw-r--r--app/assets/javascripts/components/components/column_collapsable.jsx33
-rw-r--r--app/assets/javascripts/components/components/display_name.jsx15
-rw-r--r--app/assets/javascripts/components/components/dropdown_menu.jsx37
-rw-r--r--app/assets/javascripts/components/components/extended_video_player.jsx36
-rw-r--r--app/assets/javascripts/components/components/icon_button.jsx59
-rw-r--r--app/assets/javascripts/components/components/load_more.jsx3
-rw-r--r--app/assets/javascripts/components/components/media_gallery.jsx68
-rw-r--r--app/assets/javascripts/components/components/permalink.jsx29
-rw-r--r--app/assets/javascripts/components/components/relative_timestamp.jsx5
-rw-r--r--app/assets/javascripts/components/components/status.jsx58
-rw-r--r--app/assets/javascripts/components/components/status_action_bar.jsx75
-rw-r--r--app/assets/javascripts/components/components/status_content.jsx52
-rw-r--r--app/assets/javascripts/components/components/status_list.jsx67
-rw-r--r--app/assets/javascripts/components/components/video_player.jsx76
22 files changed, 466 insertions, 403 deletions
diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx
index 6fda5915e..18197380e 100644
--- a/app/assets/javascripts/components/components/account.jsx
+++ b/app/assets/javascripts/components/components/account.jsx
@@ -1,5 +1,5 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
 import Avatar from './avatar';
 import DisplayName from './display_name';
 import Permalink from './permalink';
@@ -19,30 +19,26 @@ const buttonsStyle = {
   height: '18px'
 };
 
-const Account = React.createClass({
+class Account extends React.PureComponent {
 
-  propTypes: {
-    account: ImmutablePropTypes.map.isRequired,
-    me: React.PropTypes.number.isRequired,
-    onFollow: React.PropTypes.func.isRequired,
-    onBlock: React.PropTypes.func.isRequired,
-    onMute: React.PropTypes.func.isRequired,
-    intl: React.PropTypes.object.isRequired
-  },
-
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.handleFollow = this.handleFollow.bind(this);
+    this.handleBlock = this.handleBlock.bind(this);
+    this.handleMute = this.handleMute.bind(this);
+  }
 
   handleFollow () {
     this.props.onFollow(this.props.account);
-  },
+  }
 
   handleBlock () {
     this.props.onBlock(this.props.account);
-  },
+  }
 
   handleMute () {
     this.props.onMute(this.props.account);
-  },
+  }
 
   render () {
     const { account, me, intl } = this.props;
@@ -86,6 +82,15 @@ const Account = React.createClass({
     );
   }
 
-});
+}
+
+Account.propTypes = {
+  account: ImmutablePropTypes.map.isRequired,
+  me: PropTypes.number.isRequired,
+  onFollow: PropTypes.func.isRequired,
+  onBlock: PropTypes.func.isRequired,
+  onMute: PropTypes.func.isRequired,
+  intl: PropTypes.object.isRequired
+}
 
 export default injectIntl(Account);
diff --git a/app/assets/javascripts/components/components/attachment_list.jsx b/app/assets/javascripts/components/components/attachment_list.jsx
index 56238fe19..54841fa51 100644
--- a/app/assets/javascripts/components/components/attachment_list.jsx
+++ b/app/assets/javascripts/components/components/attachment_list.jsx
@@ -1,14 +1,8 @@
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 
 const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
 
-const AttachmentList = React.createClass({
-  propTypes: {
-    media: ImmutablePropTypes.list.isRequired
-  },
-
-  mixins: [PureRenderMixin],
+class AttachmentList extends React.PureComponent {
 
   render () {
     const { media } = this.props;
@@ -29,6 +23,10 @@ const AttachmentList = React.createClass({
       </div>
     );
   }
-});
+}
+
+AttachmentList.propTypes = {
+  media: ImmutablePropTypes.list.isRequired
+};
 
 export default AttachmentList;
diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
index 7c068955a..90eb5b6a1 100644
--- a/app/assets/javascripts/components/components/autosuggest_textarea.jsx
+++ b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
@@ -1,5 +1,6 @@
 import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
 import { isRtl } from '../rtl';
 
 const textAtCursorMatchesToken = (str, caretPosition) => {
@@ -27,30 +28,23 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
   }
 };
 
-const AutosuggestTextarea = React.createClass({
-
-  propTypes: {
-    value: React.PropTypes.string,
-    suggestions: ImmutablePropTypes.list,
-    disabled: React.PropTypes.bool,
-    placeholder: React.PropTypes.string,
-    onSuggestionSelected: React.PropTypes.func.isRequired,
-    onSuggestionsClearRequested: React.PropTypes.func.isRequired,
-    onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
-    onChange: React.PropTypes.func.isRequired,
-    onKeyUp: React.PropTypes.func,
-    onKeyDown: React.PropTypes.func,
-    onPaste: React.PropTypes.func.isRequired,
-  },
-
-  getInitialState () {
-    return {
+class AutosuggestTextarea extends React.Component {
+
+  constructor (props, context) {
+    super(props, context);
+    this.state = {
       suggestionsHidden: false,
       selectedSuggestion: 0,
       lastToken: null,
       tokenStart: 0
     };
-  },
+    this.onChange = this.onChange.bind(this);
+    this.onKeyDown = this.onKeyDown.bind(this);
+    this.onBlur = this.onBlur.bind(this);
+    this.onSuggestionClick = this.onSuggestionClick.bind(this);
+    this.setTextarea = this.setTextarea.bind(this);
+    this.onPaste = this.onPaste.bind(this);
+  }
 
   onChange (e) {
     const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
@@ -68,7 +62,7 @@ const AutosuggestTextarea = React.createClass({
     e.target.style.height = `${e.target.scrollHeight}px`;
 
     this.props.onChange(e);
-  },
+  }
 
   onKeyDown (e) {
     const { suggestions, disabled } = this.props;
@@ -118,7 +112,7 @@ const AutosuggestTextarea = React.createClass({
     }
 
     this.props.onKeyDown(e);
-  },
+  }
 
   onBlur () {
     // If we hide the suggestions immediately, then this will prevent the
@@ -128,30 +122,30 @@ const AutosuggestTextarea = React.createClass({
     setTimeout(() => {
       this.setState({ suggestionsHidden: true });
     }, 100);
-  },
+  }
 
   onSuggestionClick (suggestion, e) {
     e.preventDefault();
     this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
     this.textarea.focus();
-  },
+  }
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
       this.setState({ suggestionsHidden: false });
     }
-  },
+  }
 
   setTextarea (c) {
     this.textarea = c;
-  },
+  }
 
   onPaste (e) {
     if (e.clipboardData && e.clipboardData.files.length === 1) {
       this.props.onPaste(e.clipboardData.files)
       e.preventDefault();
     }
-  },
+  }
 
   render () {
     const { value, suggestions, disabled, placeholder, onKeyUp } = this.props;
@@ -196,6 +190,20 @@ const AutosuggestTextarea = React.createClass({
     );
   }
 
-});
+};
+
+AutosuggestTextarea.propTypes = {
+  value: PropTypes.string,
+  suggestions: ImmutablePropTypes.list,
+  disabled: PropTypes.bool,
+  placeholder: PropTypes.string,
+  onSuggestionSelected: PropTypes.func.isRequired,
+  onSuggestionsClearRequested: PropTypes.func.isRequired,
+  onSuggestionsFetchRequested: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  onKeyUp: PropTypes.func,
+  onKeyDown: PropTypes.func,
+  onPaste: PropTypes.func.isRequired,
+};
 
 export default AutosuggestTextarea;
diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx
index 673b1a247..54f96b3a7 100644
--- a/app/assets/javascripts/components/components/avatar.jsx
+++ b/app/assets/javascripts/components/components/avatar.jsx
@@ -1,36 +1,23 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
+import PropTypes from 'prop-types';
 
-const Avatar = React.createClass({
+class Avatar extends React.PureComponent {
 
-  propTypes: {
-    src: React.PropTypes.string.isRequired,
-    staticSrc: React.PropTypes.string,
-    size: React.PropTypes.number.isRequired,
-    style: React.PropTypes.object,
-    animate: React.PropTypes.bool
-  },
-
-  getDefaultProps () {
-    return {
-      animate: false
-    };
-  },
-
-  getInitialState () {
-    return {
+  constructor (props, context) {
+    super(props, context);
+    this.state = {
       hovering: false
     };
-  },
-
-  mixins: [PureRenderMixin],
+    this.handleMouseEnter = this.handleMouseEnter.bind(this);
+    this.handleMouseLeave = this.handleMouseLeave.bind(this);
+  }
 
   handleMouseEnter () {
     this.setState({ hovering: true });
-  },
+  }
 
   handleMouseLeave () {
     this.setState({ hovering: false });
-  },
+  }
 
   render () {
     const { src, size, staticSrc, animate } = this.props;
@@ -59,6 +46,18 @@ const Avatar = React.createClass({
     );
   }
 
-});
+}
+
+Avatar.propTypes = {
+  src: PropTypes.string.isRequired,
+  staticSrc: PropTypes.string,
+  size: PropTypes.number.isRequired,
+  style: PropTypes.object,
+  animate: PropTypes.bool
+};
+
+Avatar.defaultProps = {
+  animate: false
+};
 
 export default Avatar;
diff --git a/app/assets/javascripts/components/components/button.jsx b/app/assets/javascripts/components/components/button.jsx
index 6c3da10fe..8d0e49aee 100644
--- a/app/assets/javascripts/components/components/button.jsx
+++ b/app/assets/javascripts/components/components/button.jsx
@@ -1,31 +1,17 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-
-const Button = React.createClass({
-
-  propTypes: {
-    text: React.PropTypes.node,
-    onClick: React.PropTypes.func,
-    disabled: React.PropTypes.bool,
-    block: React.PropTypes.bool,
-    secondary: React.PropTypes.bool,
-    size: React.PropTypes.number,
-    style: React.PropTypes.object,
-    children: React.PropTypes.node
-  },
-
-  getDefaultProps () {
-    return {
-      size: 36
-    };
-  },
+import PropTypes from 'prop-types';
+
+class Button extends React.PureComponent {
 
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+  }
 
   handleClick (e) {
     if (!this.props.disabled) {
       this.props.onClick();
     }
-  },
+  }
 
   render () {
     const style = {
@@ -57,6 +43,21 @@ const Button = React.createClass({
     );
   }
 
-});
+}
+
+Button.propTypes = {
+  text: PropTypes.node,
+  onClick: PropTypes.func,
+  disabled: PropTypes.bool,
+  block: PropTypes.bool,
+  secondary: PropTypes.bool,
+  size: PropTypes.number,
+  style: PropTypes.object,
+  children: PropTypes.node
+};
+
+Button.defaultProps = {
+  size: 36
+};
 
 export default Button;
diff --git a/app/assets/javascripts/components/components/collapsable.jsx b/app/assets/javascripts/components/components/collapsable.jsx
index aeebb4b0f..0810768f0 100644
--- a/app/assets/javascripts/components/components/collapsable.jsx
+++ b/app/assets/javascripts/components/components/collapsable.jsx
@@ -1,4 +1,5 @@
 import { Motion, spring } from 'react-motion';
+import PropTypes from 'prop-types';
 
 const Collapsable = ({ fullHeight, isVisible, children }) => (
   <Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
@@ -11,9 +12,9 @@ const Collapsable = ({ fullHeight, isVisible, children }) => (
 );
 
 Collapsable.propTypes = {
-  fullHeight: React.PropTypes.number.isRequired,
-  isVisible: React.PropTypes.bool.isRequired,
-  children: React.PropTypes.node.isRequired
+  fullHeight: PropTypes.number.isRequired,
+  isVisible: PropTypes.bool.isRequired,
+  children: PropTypes.node.isRequired
 };
 
 export default Collapsable;
diff --git a/app/assets/javascripts/components/components/column_back_button.jsx b/app/assets/javascripts/components/components/column_back_button.jsx
index 91c3b92be..d891b6829 100644
--- a/app/assets/javascripts/components/components/column_back_button.jsx
+++ b/app/assets/javascripts/components/components/column_back_button.jsx
@@ -1,23 +1,22 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import { FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
 
 const iconStyle = {
   display: 'inline-block',
   marginRight: '5px'
 };
 
-const ColumnBackButton = React.createClass({
+class ColumnBackButton extends React.PureComponent {
 
-  contextTypes: {
-    router: React.PropTypes.object
-  },
-
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+  }
 
   handleClick () {
     if (window.history && window.history.length === 1) this.context.router.push("/");
     else this.context.router.goBack();
-  },
+  }
 
   render () {
     return (
@@ -28,6 +27,10 @@ const ColumnBackButton = React.createClass({
     );
   }
 
-});
+};
+
+ColumnBackButton.contextTypes = {
+  router: PropTypes.object
+};
 
 export default ColumnBackButton;
diff --git a/app/assets/javascripts/components/components/column_back_button_slim.jsx b/app/assets/javascripts/components/components/column_back_button_slim.jsx
index 536964b01..0c753436b 100644
--- a/app/assets/javascripts/components/components/column_back_button_slim.jsx
+++ b/app/assets/javascripts/components/components/column_back_button_slim.jsx
@@ -1,5 +1,5 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import { FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
 
 const outerStyle = {
   position: 'absolute',
@@ -16,17 +16,16 @@ const iconStyle = {
   marginRight: '5px'
 };
 
-const ColumnBackButtonSlim = React.createClass({
+class ColumnBackButtonSlim extends React.PureComponent {
 
-  contextTypes: {
-    router: React.PropTypes.object
-  },
-
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+  }
 
   handleClick () {
     this.context.router.push('/');
-  },
+  }
 
   render () {
     return (
@@ -39,6 +38,10 @@ const ColumnBackButtonSlim = React.createClass({
     );
   }
 
-});
+}
+
+ColumnBackButtonSlim.contextTypes = {
+  router: PropTypes.object
+};
 
 export default ColumnBackButtonSlim;
diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx
index 75dfc8a4e..62b645783 100644
--- a/app/assets/javascripts/components/components/column_collapsable.jsx
+++ b/app/assets/javascripts/components/components/column_collapsable.jsx
@@ -1,5 +1,5 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import { Motion, spring } from 'react-motion';
+import PropTypes from 'prop-types';
 
 const iconStyle = {
   fontSize: '16px',
@@ -11,23 +11,16 @@ const iconStyle = {
   zIndex: '3'
 };
 
-const ColumnCollapsable = React.createClass({
+class ColumnCollapsable extends React.PureComponent {
 
-  propTypes: {
-    icon: React.PropTypes.string.isRequired,
-    title: React.PropTypes.string,
-    fullHeight: React.PropTypes.number.isRequired,
-    children: React.PropTypes.node,
-    onCollapse: React.PropTypes.func
-  },
-
-  getInitialState () {
-    return {
+  constructor (props, context) {
+    super(props, context);
+    this.state = {
       collapsed: true
     };
-  },
 
-  mixins: [PureRenderMixin],
+    this.handleToggleCollapsed = this.handleToggleCollapsed.bind(this);
+  }
 
   handleToggleCollapsed () {
     const currentState = this.state.collapsed;
@@ -37,7 +30,7 @@ const ColumnCollapsable = React.createClass({
     if (!currentState && this.props.onCollapse) {
       this.props.onCollapse();
     }
-  },
+  }
 
   render () {
     const { icon, title, fullHeight, children } = this.props;
@@ -60,6 +53,14 @@ const ColumnCollapsable = React.createClass({
       </div>
     );
   }
-});
+}
+
+ColumnCollapsable.propTypes = {
+  icon: PropTypes.string.isRequired,
+  title: PropTypes.string,
+  fullHeight: PropTypes.number.isRequired,
+  children: PropTypes.node,
+  onCollapse: PropTypes.func
+};
 
 export default ColumnCollapsable;
diff --git a/app/assets/javascripts/components/components/display_name.jsx b/app/assets/javascripts/components/components/display_name.jsx
index aa48608d3..9a6e7a9a5 100644
--- a/app/assets/javascripts/components/components/display_name.jsx
+++ b/app/assets/javascripts/components/components/display_name.jsx
@@ -1,15 +1,8 @@
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import escapeTextContentForBrowser from 'escape-html';
 import emojify from '../emoji';
 
-const DisplayName = React.createClass({
-
-  propTypes: {
-    account: ImmutablePropTypes.map.isRequired
-  },
-
-  mixins: [PureRenderMixin],
+class DisplayName extends React.PureComponent {
 
   render () {
     const displayName     = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name');
@@ -22,6 +15,10 @@ const DisplayName = React.createClass({
     );
   }
 
-});
+};
+
+DisplayName.propTypes = {
+  account: ImmutablePropTypes.map.isRequired
+}
 
 export default DisplayName;
diff --git a/app/assets/javascripts/components/components/dropdown_menu.jsx b/app/assets/javascripts/components/components/dropdown_menu.jsx
index 2b42eaa60..2b0929e05 100644
--- a/app/assets/javascripts/components/components/dropdown_menu.jsx
+++ b/app/assets/javascripts/components/components/dropdown_menu.jsx
@@ -1,26 +1,20 @@
 import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
+import PropTypes from 'prop-types';
 
-const DropdownMenu = React.createClass({
+class DropdownMenu extends React.PureComponent {
 
-  propTypes: {
-    icon: React.PropTypes.string.isRequired,
-    items: React.PropTypes.array.isRequired,
-    size: React.PropTypes.number.isRequired,
-    direction: React.PropTypes.string
-  },
-
-  getDefaultProps () {
-    return {
+  constructor (props, context) {
+    super(props, context);
+    this.state = {
       direction: 'left'
     };
-  },
-
-  mixins: [PureRenderMixin],
+    this.setRef = this.setRef.bind(this);
+    this.renderItem = this.renderItem.bind(this);
+  }
 
   setRef (c) {
     this.dropdown = c;
-  },
+  }
 
   handleClick (i, e) {
     const { action } = this.props.items[i];
@@ -30,7 +24,7 @@ const DropdownMenu = React.createClass({
       action();
       this.dropdown.hide();
     }
-  },
+  }
 
   renderItem (item, i) {
     if (item === null) {
@@ -46,7 +40,7 @@ const DropdownMenu = React.createClass({
         </a>
       </li>
     );
-  },
+  }
 
   render () {
     const { icon, items, size, direction } = this.props;
@@ -67,6 +61,13 @@ const DropdownMenu = React.createClass({
     );
   }
 
-});
+}
+
+DropdownMenu.propTypes = {
+  icon: PropTypes.string.isRequired,
+  items: PropTypes.array.isRequired,
+  size: PropTypes.number.isRequired,
+  direction: PropTypes.string
+};
 
 export default DropdownMenu;
diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx
index a64515583..bbbe09da8 100644
--- a/app/assets/javascripts/components/components/extended_video_player.jsx
+++ b/app/assets/javascripts/components/components/extended_video_player.jsx
@@ -1,33 +1,30 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
+import PropTypes from 'prop-types';
 
-const ExtendedVideoPlayer = React.createClass({
+class ExtendedVideoPlayer extends React.PureComponent {
 
-  propTypes: {
-    src: React.PropTypes.string.isRequired,
-    time: React.PropTypes.number,
-    controls: React.PropTypes.bool.isRequired,
-    muted: React.PropTypes.bool.isRequired
-  },
-
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.handleLoadedData = this.handleLoadedData.bind(this);
+    this.setRef = this.setRef.bind(this);
+  }
 
   handleLoadedData () {
     if (this.props.time) {
       this.video.currentTime = this.props.time;
     }
-  },
+  }
 
   componentDidMount () {
     this.video.addEventListener('loadeddata', this.handleLoadedData);
-  },
+  }
 
   componentWillUnmount () {
     this.video.removeEventListener('loadeddata', this.handleLoadedData);
-  },
+  }
 
   setRef (c) {
     this.video = c;
-  },
+  }
 
   render () {
     return (
@@ -42,8 +39,15 @@ const ExtendedVideoPlayer = React.createClass({
         />
       </div>
     );
-  },
+  }
+
+}
 
-});
+ExtendedVideoPlayer.propTypes = {
+  src: PropTypes.string.isRequired,
+  time: PropTypes.number,
+  controls: PropTypes.bool.isRequired,
+  muted: PropTypes.bool.isRequired
+};
 
 export default ExtendedVideoPlayer;
diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx
index 0c683db5d..e2059fc4f 100644
--- a/app/assets/javascripts/components/components/icon_button.jsx
+++ b/app/assets/javascripts/components/components/icon_button.jsx
@@ -1,33 +1,12 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import { Motion, spring } from 'react-motion';
+import PropTypes from 'prop-types';
 
-const IconButton = React.createClass({
-
-  propTypes: {
-    title: React.PropTypes.string.isRequired,
-    icon: React.PropTypes.string.isRequired,
-    onClick: React.PropTypes.func,
-    size: React.PropTypes.number,
-    active: React.PropTypes.bool,
-    style: React.PropTypes.object,
-    activeStyle: React.PropTypes.object,
-    disabled: React.PropTypes.bool,
-    inverted: React.PropTypes.bool,
-    animate: React.PropTypes.bool,
-    overlay: React.PropTypes.bool
-  },
-
-  getDefaultProps () {
-    return {
-      size: 18,
-      active: false,
-      disabled: false,
-      animate: false,
-      overlay: false
-    };
-  },
+class IconButton extends React.PureComponent {
 
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+  }
 
   handleClick (e) {
     e.preventDefault();
@@ -35,7 +14,7 @@ const IconButton = React.createClass({
     if (!this.props.disabled) {
       this.props.onClick(e);
     }
-  },
+  }
 
   render () {
     let style = {
@@ -84,6 +63,28 @@ const IconButton = React.createClass({
     );
   }
 
-});
+}
+
+IconButton.propTypes = {
+  title: PropTypes.string.isRequired,
+  icon: PropTypes.string.isRequired,
+  onClick: PropTypes.func,
+  size: PropTypes.number,
+  active: PropTypes.bool,
+  style: PropTypes.object,
+  activeStyle: PropTypes.object,
+  disabled: PropTypes.bool,
+  inverted: PropTypes.bool,
+  animate: PropTypes.bool,
+  overlay: PropTypes.bool
+};
+
+IconButton.defaultProps = {
+  size: 18,
+  active: false,
+  disabled: false,
+  animate: false,
+  overlay: false
+};
 
 export default IconButton;
diff --git a/app/assets/javascripts/components/components/load_more.jsx b/app/assets/javascripts/components/components/load_more.jsx
index b4bc8b711..f59ff1103 100644
--- a/app/assets/javascripts/components/components/load_more.jsx
+++ b/app/assets/javascripts/components/components/load_more.jsx
@@ -1,4 +1,5 @@
 import { FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
 
 const LoadMore = ({ onClick }) => (
   <a href="#" className='load-more' role='button' onClick={onClick}>
@@ -7,7 +8,7 @@ const LoadMore = ({ onClick }) => (
 );
 
 LoadMore.propTypes = {
-  onClick: React.PropTypes.func
+  onClick: PropTypes.func
 };
 
 export default LoadMore;
diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx
index f334af9cf..ebc6e709d 100644
--- a/app/assets/javascripts/components/components/media_gallery.jsx
+++ b/app/assets/javascripts/components/components/media_gallery.jsx
@@ -1,5 +1,5 @@
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
+import PropTypes from 'prop-types';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { isIOS } from '../is_mobile';
@@ -72,17 +72,12 @@ const gifvThumbStyle = {
   cursor: 'zoom-in'
 };
 
-const Item = React.createClass({
+class Item extends React.PureComponent {
 
-  propTypes: {
-    attachment: ImmutablePropTypes.map.isRequired,
-    index: React.PropTypes.number.isRequired,
-    size: React.PropTypes.number.isRequired,
-    onClick: React.PropTypes.func.isRequired,
-    autoPlayGif: React.PropTypes.bool.isRequired
-  },
-
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+  }
 
   handleClick (e) {
     const { index, onClick } = this.props;
@@ -93,7 +88,7 @@ const Item = React.createClass({
     }
 
     e.stopPropagation();
-  },
+  }
 
   render () {
     const { attachment, index, size } = this.props;
@@ -184,34 +179,34 @@ const Item = React.createClass({
     );
   }
 
-});
+}
 
-const MediaGallery = React.createClass({
-
-  getInitialState () {
-    return {
-      visible: !this.props.sensitive
-    };
-  },
+Item.propTypes = {
+  attachment: ImmutablePropTypes.map.isRequired,
+  index: PropTypes.number.isRequired,
+  size: PropTypes.number.isRequired,
+  onClick: PropTypes.func.isRequired,
+  autoPlayGif: PropTypes.bool.isRequired
+};
 
-  propTypes: {
-    sensitive: React.PropTypes.bool,
-    media: ImmutablePropTypes.list.isRequired,
-    height: React.PropTypes.number.isRequired,
-    onOpenMedia: React.PropTypes.func.isRequired,
-    intl: React.PropTypes.object.isRequired,
-    autoPlayGif: React.PropTypes.bool.isRequired
-  },
+class MediaGallery extends React.PureComponent {
 
-  mixins: [PureRenderMixin],
+  constructor (props, context) {
+    super(props, context);
+    this.state = {
+      visible: !props.sensitive
+    };
+    this.handleOpen = this.handleOpen.bind(this);
+    this.handleClick = this.handleClick.bind(this);
+  }
 
   handleOpen (e) {
     this.setState({ visible: !this.state.visible });
-  },
+  }
 
   handleClick (index) {
     this.props.onOpenMedia(this.props.media, index);
-  },
+  }
 
   render () {
     const { media, intl, sensitive } = this.props;
@@ -249,6 +244,15 @@ const MediaGallery = React.createClass({
     );
   }
 
-});
+}
+
+MediaGallery.propTypes = {
+  sensitive: PropTypes.bool,
+  media: ImmutablePropTypes.list.isRequired,
+  height: PropTypes.number.isRequired,
+  onOpenMedia: PropTypes.func.isRequired,
+  intl: PropTypes.object.isRequired,
+  autoPlayGif: PropTypes.bool.isRequired
+};
 
 export default injectIntl(MediaGallery);
diff --git a/app/assets/javascripts/components/components/permalink.jsx b/app/assets/javascripts/components/components/permalink.jsx
index c39546b53..0ad477db7 100644
--- a/app/assets/javascripts/components/components/permalink.jsx
+++ b/app/assets/javascripts/components/components/permalink.jsx
@@ -1,21 +1,18 @@
-const Permalink = React.createClass({
+import PropTypes from 'prop-types';
 
-  contextTypes: {
-    router: React.PropTypes.object
-  },
+class Permalink extends React.Component {
 
-  propTypes: {
-    href: React.PropTypes.string.isRequired,
-    to: React.PropTypes.string.isRequired,
-    children: React.PropTypes.node
-  },
+  constructor (props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+  }
 
   handleClick (e) {
     if (e.button === 0) {
       e.preventDefault();
       this.context.router.push(this.props.to);
     }
-  },
+  }
 
   render () {
     const { href, children, ...other } = this.props;
@@ -23,6 +20,16 @@ const Permalink = React.createClass({
     return <a href={href} onClick={this.handleClick} {...other}>{children}</a>;
   }
 
-});
+}
+
+Permalink.contextTypes = {
+  router: PropTypes.object
+};
+
+Permalink.propTypes = {
+  href: PropTypes.string.isRequired,
+  to: PropTypes.string.isRequired,
+  children: PropTypes.node
+};
 
 export default Permalink;
diff --git a/app/assets/javascripts/components/components/relative_timestamp.jsx b/app/assets/javascripts/components/components/relative_timestamp.jsx
index 3b012b184..9ab472e2c 100644
--- a/app/assets/javascripts/components/components/relative_timestamp.jsx
+++ b/app/assets/javascripts/components/components/relative_timestamp.jsx
@@ -1,4 +1,5 @@
 import { injectIntl, FormattedRelative } from 'react-intl';
+import PropTypes from 'prop-types';
 
 const RelativeTimestamp = ({ intl, timestamp }) => {
   const date = new Date(timestamp);
@@ -11,8 +12,8 @@ const RelativeTimestamp = ({ intl, timestamp }) => {
 };
 
 RelativeTimestamp.propTypes = {
-  intl: React.PropTypes.object.isRequired,
-  timestamp: React.PropTypes.string.isRequired
+  intl: PropTypes.object.isRequired,
+  timestamp: PropTypes.string.isRequired
 };
 
 export default injectIntl(RelativeTimestamp);
diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx
index a5b9ad87c..c57ee5c89 100644
--- a/app/assets/javascripts/components/components/status.jsx
+++ b/app/assets/javascripts/components/components/status.jsx
@@ -1,7 +1,7 @@
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
 import Avatar from './avatar';
 import RelativeTimestamp from './relative_timestamp';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import DisplayName from './display_name';
 import MediaGallery from './media_gallery';
 import VideoPlayer from './video_player';
@@ -12,41 +12,25 @@ import { FormattedMessage } from 'react-intl';
 import emojify from '../emoji';
 import escapeTextContentForBrowser from 'escape-html';
 
-const Status = React.createClass({
-
-  contextTypes: {
-    router: React.PropTypes.object
-  },
-
-  propTypes: {
-    status: ImmutablePropTypes.map,
-    wrapped: React.PropTypes.bool,
-    onReply: React.PropTypes.func,
-    onFavourite: React.PropTypes.func,
-    onReblog: React.PropTypes.func,
-    onDelete: React.PropTypes.func,
-    onOpenMedia: React.PropTypes.func,
-    onOpenVideo: React.PropTypes.func,
-    onBlock: React.PropTypes.func,
-    me: React.PropTypes.number,
-    boostModal: React.PropTypes.bool,
-    autoPlayGif: React.PropTypes.bool,
-    muted: React.PropTypes.bool
-  },
-
-  mixins: [PureRenderMixin],
+class Status extends React.PureComponent {
+
+  constructor (props, context) {
+    super(props, context);
+    this.handleClick = this.handleClick.bind(this);
+    this.handleAccountClick = this.handleAccountClick.bind(this);
+  }
 
   handleClick () {
     const { status } = this.props;
     this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
-  },
+  }
 
   handleAccountClick (id, e) {
     if (e.button === 0) {
       e.preventDefault();
       this.context.router.push(`/accounts/${id}`);
     }
-  },
+  }
 
   render () {
     let media = '';
@@ -112,6 +96,26 @@ const Status = React.createClass({
     );
   }
 
-});
+}
+
+Status.contextTypes = {
+  router: PropTypes.object
+};
+
+Status.propTypes = {
+  status: ImmutablePropTypes.map,
+  wrapped: PropTypes.bool,
+  onReply: PropTypes.func,
+  onFavourite: PropTypes.func,
+  onReblog: PropTypes.func,
+  onDelete: PropTypes.func,
+  onOpenMedia: PropTypes.func,
+  onOpenVideo: PropTypes.func,
+  onBlock: PropTypes.func,
+  me: PropTypes.number,
+  boostModal: PropTypes.bool,
+  autoPlayGif: PropTypes.bool,
+  muted: PropTypes.bool
+};
 
 export default Status;
diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx
index 213aa7743..b452cb8cf 100644
--- a/app/assets/javascripts/components/components/status_action_bar.jsx
+++ b/app/assets/javascripts/components/components/status_action_bar.jsx
@@ -1,5 +1,5 @@
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
+import PropTypes from 'prop-types';
 import IconButton from './icon_button';
 import DropdownMenu from './dropdown_menu';
 import { defineMessages, injectIntl } from 'react-intl';
@@ -17,64 +17,57 @@ const messages = defineMessages({
   report: { id: 'status.report', defaultMessage: 'Report @{name}' }
 });
 
-const StatusActionBar = React.createClass({
-
-  contextTypes: {
-    router: React.PropTypes.object
-  },
-
-  propTypes: {
-    status: ImmutablePropTypes.map.isRequired,
-    onReply: React.PropTypes.func,
-    onFavourite: React.PropTypes.func,
-    onReblog: React.PropTypes.func,
-    onDelete: React.PropTypes.func,
-    onMention: React.PropTypes.func,
-    onMute: React.PropTypes.func,
-    onBlock: React.PropTypes.func,
-    onReport: React.PropTypes.func,
-    me: React.PropTypes.number.isRequired,
-    intl: React.PropTypes.object.isRequired
-  },
-
-  mixins: [PureRenderMixin],
+class StatusActionBar extends React.PureComponent {
+
+  constructor (props, context) {
+    super(props, context);
+    this.handleReplyClick = this.handleReplyClick.bind(this);
+    this.handleFavouriteClick = this.handleFavouriteClick.bind(this);
+    this.handleReblogClick = this.handleReblogClick.bind(this);
+    this.handleDeleteClick = this.handleDeleteClick.bind(this);
+    this.handleMentionClick = this.handleMentionClick.bind(this);
+    this.handleMuteClick = this.handleMuteClick.bind(this);
+    this.handleBlockClick = this.handleBlockClick.bind(this);
+    this.handleOpen = this.handleOpen.bind(this);
+    this.handleReport = this.handleReport.bind(this);
+  }
 
   handleReplyClick () {
     this.props.onReply(this.props.status, this.context.router);
-  },
+  }
 
   handleFavouriteClick () {
     this.props.onFavourite(this.props.status);
-  },
+  }
 
   handleReblogClick (e) {
     this.props.onReblog(this.props.status, e);
-  },
+  }
 
   handleDeleteClick () {
     this.props.onDelete(this.props.status);
-  },
+  }
 
   handleMentionClick () {
     this.props.onMention(this.props.status.get('account'), this.context.router);
-  },
+  }
 
   handleMuteClick () {
     this.props.onMute(this.props.status.get('account'));
-  },
+  }
 
   handleBlockClick () {
     this.props.onBlock(this.props.status.get('account'));
-  },
+  }
 
   handleOpen () {
     this.context.router.push(`/statuses/${this.props.status.get('id')}`);
-  },
+  }
 
   handleReport () {
     this.props.onReport(this.props.status);
     this.context.router.push('/report');
-  },
+  }
 
   render () {
     const { status, me, intl } = this.props;
@@ -119,6 +112,24 @@ const StatusActionBar = React.createClass({
     );
   }
 
-});
+}
+
+StatusActionBar.contextTypes = {
+  router: PropTypes.object
+};
+
+StatusActionBar.propTypes = {
+  status: ImmutablePropTypes.map.isRequired,
+  onReply: PropTypes.func,
+  onFavourite: PropTypes.func,
+  onReblog: PropTypes.func,
+  onDelete: PropTypes.func,
+  onMention: PropTypes.func,
+  onMute: PropTypes.func,
+  onBlock: PropTypes.func,
+  onReport: PropTypes.func,
+  me: PropTypes.number.isRequired,
+  intl: PropTypes.object.isRequired
+};
 
 export default injectIntl(StatusActionBar);
diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx
index ce8ead41e..08370b189 100644
--- a/app/assets/javascripts/components/components/status_content.jsx
+++ b/app/assets/javascripts/components/components/status_content.jsx
@@ -1,29 +1,24 @@
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import escapeTextContentForBrowser from 'escape-html';
+import PropTypes from 'prop-types';
 import emojify from '../emoji';
 import { isRtl } from '../rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
 
-const StatusContent = React.createClass({
+class StatusContent extends React.PureComponent {
 
-  contextTypes: {
-    router: React.PropTypes.object
-  },
-
-  propTypes: {
-    status: ImmutablePropTypes.map.isRequired,
-    onClick: React.PropTypes.func
-  },
-
-  getInitialState () {
-    return {
+  constructor (props, context) {
+    super(props, context);
+    this.state = {
       hidden: true
     };
-  },
-
-  mixins: [PureRenderMixin],
+    this.onMentionClick = this.onMentionClick.bind(this);
+    this.onHashtagClick = this.onHashtagClick.bind(this);
+    this.handleMouseDown = this.handleMouseDown.bind(this)
+    this.handleMouseUp = this.handleMouseUp.bind(this);
+    this.handleSpoilerClick = this.handleSpoilerClick.bind(this);
+  };
 
   componentDidMount () {
     const node  = ReactDOM.findDOMNode(this);
@@ -47,14 +42,14 @@ const StatusContent = React.createClass({
         link.setAttribute('title', link.href);
       }
     }
-  },
+  }
 
   onMentionClick (mention, e) {
     if (e.button === 0) {
       e.preventDefault();
       this.context.router.push(`/accounts/${mention.get('id')}`);
     }
-  },
+  }
 
   onHashtagClick (hashtag, e) {
     hashtag = hashtag.replace(/^#/, '').toLowerCase();
@@ -63,11 +58,11 @@ const StatusContent = React.createClass({
       e.preventDefault();
       this.context.router.push(`/timelines/tag/${hashtag}`);
     }
-  },
+  }
 
   handleMouseDown (e) {
     this.startXY = [e.clientX, e.clientY];
-  },
+  }
 
   handleMouseUp (e) {
     const [ startX, startY ] = this.startXY;
@@ -82,12 +77,12 @@ const StatusContent = React.createClass({
     }
 
     this.startXY = null;
-  },
+  }
 
   handleSpoilerClick (e) {
     e.preventDefault();
     this.setState({ hidden: !this.state.hidden });
-  },
+  }
 
   render () {
     const { status } = this.props;
@@ -146,8 +141,17 @@ const StatusContent = React.createClass({
         />
       );
     }
-  },
+  }
+
+}
+
+StatusContent.contextTypes = {
+  router: PropTypes.object
+};
 
-});
+StatusContent.propTypes = {
+  status: ImmutablePropTypes.map.isRequired,
+  onClick: PropTypes.func
+};
 
 export default StatusContent;
diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx
index 345944e4d..7ba8bad1d 100644
--- a/app/assets/javascripts/components/components/status_list.jsx
+++ b/app/assets/javascripts/components/components/status_list.jsx
@@ -1,32 +1,18 @@
 import Status from './status';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
 import { ScrollContainer } from 'react-router-scroll';
+import PropTypes from 'prop-types';
 import StatusContainer from '../containers/status_container';
 import LoadMore from './load_more';
 
-const StatusList = React.createClass({
-
-  propTypes: {
-    statusIds: ImmutablePropTypes.list.isRequired,
-    onScrollToBottom: React.PropTypes.func,
-    onScrollToTop: React.PropTypes.func,
-    onScroll: React.PropTypes.func,
-    trackScroll: React.PropTypes.bool,
-    isLoading: React.PropTypes.bool,
-    isUnread: React.PropTypes.bool,
-    hasMore: React.PropTypes.bool,
-    prepend: React.PropTypes.node,
-    emptyMessage: React.PropTypes.node
-  },
-
-  getDefaultProps () {
-    return {
-      trackScroll: true
-    };
-  },
-
-  mixins: [PureRenderMixin],
+class StatusList extends React.PureComponent {
+
+  constructor (props, context) {
+    super(props, context);
+    this.handleScroll = this.handleScroll.bind(this);
+    this.setRef = this.setRef.bind(this);
+    this.handleLoadMore = this.handleLoadMore.bind(this);
+  }
 
   handleScroll (e) {
     const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -40,38 +26,38 @@ const StatusList = React.createClass({
     } else if (this.props.onScroll) {
       this.props.onScroll();
     }
-  },
+  }
 
   componentDidMount () {
     this.attachScrollListener();
-  },
+  }
 
   componentDidUpdate (prevProps) {
     if (this.node.scrollTop > 0 && (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition)) {
       this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
     }
-  },
+  }
 
   componentWillUnmount () {
     this.detachScrollListener();
-  },
+  }
 
   attachScrollListener () {
     this.node.addEventListener('scroll', this.handleScroll);
-  },
+  }
 
   detachScrollListener () {
     this.node.removeEventListener('scroll', this.handleScroll);
-  },
+  }
 
   setRef (c) {
     this.node = c;
-  },
+  }
 
   handleLoadMore (e) {
     e.preventDefault();
     this.props.onScrollToBottom();
-  },
+  }
 
   render () {
     const { statusIds, onScrollToBottom, trackScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
@@ -123,6 +109,23 @@ const StatusList = React.createClass({
     }
   }
 
-});
+}
+
+StatusList.propTypes = {
+  statusIds: ImmutablePropTypes.list.isRequired,
+  onScrollToBottom: PropTypes.func,
+  onScrollToTop: PropTypes.func,
+  onScroll: PropTypes.func,
+  trackScroll: PropTypes.bool,
+  isLoading: PropTypes.bool,
+  isUnread: PropTypes.bool,
+  hasMore: PropTypes.bool,
+  prepend: PropTypes.node,
+  emptyMessage: PropTypes.node
+};
+
+StatusList.defaultProps = {
+  trackScroll: true
+};
 
 export default StatusList;
diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx
index 0da544746..50a69a759 100644
--- a/app/assets/javascripts/components/components/video_player.jsx
+++ b/app/assets/javascripts/components/components/video_player.jsx
@@ -1,5 +1,5 @@
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
+import PropTypes from 'prop-types';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { isIOS } from '../is_mobile';
@@ -72,39 +72,30 @@ const expandButtonStyle = {
   zIndex: '100'
 };
 
-const VideoPlayer = React.createClass({
-  propTypes: {
-    media: ImmutablePropTypes.map.isRequired,
-    width: React.PropTypes.number,
-    height: React.PropTypes.number,
-    sensitive: React.PropTypes.bool,
-    intl: React.PropTypes.object.isRequired,
-    autoplay: React.PropTypes.bool,
-    onOpenVideo: React.PropTypes.func.isRequired
-  },
-
-  getDefaultProps () {
-    return {
-      width: 239,
-      height: 110
-    };
-  },
+class VideoPlayer extends React.PureComponent {
 
-  getInitialState () {
-    return {
+  constructor (props, context) {
+    super(props, context);
+    this.state = {
       visible: !this.props.sensitive,
       preview: true,
       muted: true,
       hasAudio: true,
       videoError: false
     };
-  },
-
-  mixins: [PureRenderMixin],
+    this.handleClick = this.handleClick.bind(this);
+    this.handleVideoClick = this.handleVideoClick.bind(this);
+    this.handleOpen = this.handleOpen.bind(this);
+    this.handleVisibility = this.handleVisibility.bind(this);
+    this.handleExpand = this.handleExpand.bind(this);
+    this.setRef = this.setRef.bind(this);
+    this.handleLoadedData = this.handleLoadedData.bind(this);
+    this.handleVideoError = this.handleVideoError.bind(this);
+  }
 
   handleClick () {
     this.setState({ muted: !this.state.muted });
-  },
+  }
 
   handleVideoClick (e) {
     e.stopPropagation();
@@ -116,37 +107,37 @@ const VideoPlayer = React.createClass({
     } else {
       node.pause();
     }
-  },
+  }
 
   handleOpen () {
     this.setState({ preview: !this.state.preview });
-  },
+  }
 
   handleVisibility () {
     this.setState({
       visible: !this.state.visible,
       preview: true
     });
-  },
+  }
 
   handleExpand () {
     this.video.pause();
     this.props.onOpenVideo(this.props.media, this.video.currentTime);
-  },
+  }
 
   setRef (c) {
     this.video = c;
-  },
+  }
 
   handleLoadedData () {
     if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
       this.setState({ hasAudio: false });
     }
-  },
+  }
 
   handleVideoError () {
     this.setState({ videoError: true });
-  },
+  }
 
   componentDidMount () {
     if (!this.video) {
@@ -155,7 +146,7 @@ const VideoPlayer = React.createClass({
 
     this.video.addEventListener('loadeddata', this.handleLoadedData);
     this.video.addEventListener('error', this.handleVideoError);
-  },
+  }
 
   componentDidUpdate () {
     if (!this.video) {
@@ -164,7 +155,7 @@ const VideoPlayer = React.createClass({
 
     this.video.addEventListener('loadeddata', this.handleLoadedData);
     this.video.addEventListener('error', this.handleVideoError);
-  },
+  }
 
   componentWillUnmount () {
     if (!this.video) {
@@ -173,7 +164,7 @@ const VideoPlayer = React.createClass({
 
     this.video.removeEventListener('loadeddata', this.handleLoadedData);
     this.video.removeEventListener('error', this.handleVideoError);
-  },
+  }
 
   render () {
     const { media, intl, width, height, sensitive, autoplay } = this.props;
@@ -247,6 +238,21 @@ const VideoPlayer = React.createClass({
     );
   }
 
-});
+}
+
+VideoPlayer.propTypes = {
+  media: ImmutablePropTypes.map.isRequired,
+  width: PropTypes.number,
+  height: PropTypes.number,
+  sensitive: PropTypes.bool,
+  intl: PropTypes.object.isRequired,
+  autoplay: PropTypes.bool,
+  onOpenVideo: PropTypes.func.isRequired
+};
+
+VideoPlayer.defaultProps = {
+  width: 239,
+  height: 110
+};
 
 export default injectIntl(VideoPlayer);