about summary refs log tree commit diff
path: root/app/assets/javascripts/components/features/ui
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/components/features/ui')
-rw-r--r--app/assets/javascripts/components/features/ui/components/column.jsx7
-rw-r--r--app/assets/javascripts/components/features/ui/components/column_header.jsx7
-rw-r--r--app/assets/javascripts/components/features/ui/components/media_modal.jsx133
-rw-r--r--app/assets/javascripts/components/features/ui/components/modal_root.jsx80
-rw-r--r--app/assets/javascripts/components/features/ui/components/tabs_bar.jsx28
-rw-r--r--app/assets/javascripts/components/features/ui/components/upload_area.jsx32
-rw-r--r--app/assets/javascripts/components/features/ui/containers/modal_container.jsx163
-rw-r--r--app/assets/javascripts/components/features/ui/containers/status_list_container.jsx24
-rw-r--r--app/assets/javascripts/components/features/ui/index.jsx71
9 files changed, 355 insertions, 190 deletions
diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx
index 5b0603ee9..2b7e11bf1 100644
--- a/app/assets/javascripts/components/features/ui/components/column.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column.jsx
@@ -34,7 +34,8 @@ const Column = React.createClass({
   propTypes: {
     heading: React.PropTypes.string,
     icon: React.PropTypes.string,
-    children: React.PropTypes.node
+    children: React.PropTypes.node,
+    active: React.PropTypes.bool
   },
 
   mixins: [PureRenderMixin],
@@ -51,12 +52,12 @@ const Column = React.createClass({
   },
 
   render () {
-    const { heading, icon, children } = this.props;
+    const { heading, icon, children, active } = this.props;
 
     let header = '';
 
     if (heading) {
-      header = <ColumnHeader icon={icon} type={heading} onClick={this.handleHeaderClick} />;
+      header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} />;
     }
 
     return (
diff --git a/app/assets/javascripts/components/features/ui/components/column_header.jsx b/app/assets/javascripts/components/features/ui/components/column_header.jsx
index 8b072d723..de55fa748 100644
--- a/app/assets/javascripts/components/features/ui/components/column_header.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column_header.jsx
@@ -5,6 +5,7 @@ const ColumnHeader = React.createClass({
   propTypes: {
     icon: React.PropTypes.string,
     type: React.PropTypes.string,
+    active: React.PropTypes.bool,
     onClick: React.PropTypes.func
   },
 
@@ -15,6 +16,8 @@ const ColumnHeader = React.createClass({
   },
 
   render () {
+    const { type, active } = this.props;
+
     let icon = '';
 
     if (this.props.icon) {
@@ -22,9 +25,9 @@ const ColumnHeader = React.createClass({
     }
 
     return (
-      <div className='column-header' onClick={this.handleClick}>
+      <div className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
         {icon}
-        {this.props.type}
+        {type}
       </div>
     );
   }
diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx
new file mode 100644
index 000000000..35eb2cb0c
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx
@@ -0,0 +1,133 @@
+import LoadingIndicator from '../../../components/loading_indicator';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ExtendedVideoPlayer from '../../../components/extended_video_player';
+import ImageLoader from 'react-imageloader';
+import { defineMessages, injectIntl } from 'react-intl';
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' }
+});
+
+const leftNavStyle = {
+  position: 'absolute',
+  background: 'rgba(0, 0, 0, 0.5)',
+  padding: '30px 15px',
+  cursor: 'pointer',
+  fontSize: '24px',
+  top: '0',
+  left: '-61px',
+  boxSizing: 'border-box',
+  height: '100%',
+  display: 'flex',
+  alignItems: 'center'
+};
+
+const rightNavStyle = {
+  position: 'absolute',
+  background: 'rgba(0, 0, 0, 0.5)',
+  padding: '30px 15px',
+  cursor: 'pointer',
+  fontSize: '24px',
+  top: '0',
+  right: '-61px',
+  boxSizing: 'border-box',
+  height: '100%',
+  display: 'flex',
+  alignItems: 'center'
+};
+
+const closeStyle = {
+  position: 'absolute',
+  top: '4px',
+  right: '4px'
+};
+
+const MediaModal = React.createClass({
+
+  propTypes: {
+    media: ImmutablePropTypes.list.isRequired,
+    index: React.PropTypes.number.isRequired,
+    onClose: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  getInitialState () {
+    return {
+      index: null
+    };
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleNextClick () {
+    this.setState({ index: (this.getIndex() + 1) % this.props.media.size});
+  },
+
+  handlePrevClick () {
+    this.setState({ index: (this.getIndex() - 1) % this.props.media.size});
+  },
+
+  handleKeyUp (e) {
+    switch(e.key) {
+    case 'ArrowLeft':
+      this.handlePrevClick();
+      break;
+    case 'ArrowRight':
+      this.handleNextClick();
+      break;
+    }
+  },
+
+  componentDidMount () {
+    window.addEventListener('keyup', this.handleKeyUp, false);
+  },
+
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this.handleKeyUp);
+  },
+
+  getIndex () {
+    return this.state.index !== null ? this.state.index : this.props.index;
+  },
+
+  render () {
+    const { media, intl, onClose } = this.props;
+
+    const index = this.getIndex();
+    const attachment = media.get(index);
+    const url = attachment.get('url');
+
+    let leftNav, rightNav, content;
+
+    leftNav = rightNav = content = '';
+
+    if (media.size > 1) {
+      leftNav  = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
+      rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
+    }
+
+    if (attachment.get('type') === 'image') {
+      content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />;
+    } else if (attachment.get('type') === 'gifv') {
+      content = <ExtendedVideoPlayer src={url} />;
+    }
+
+    return (
+      <div className='modal-root__modal media-modal'>
+        {leftNav}
+
+        <div>
+          <IconButton title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} style={closeStyle} />
+          {content}
+        </div>
+
+        {rightNav}
+      </div>
+    );
+  }
+
+});
+
+export default injectIntl(MediaModal);
diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
new file mode 100644
index 000000000..d2ae5e145
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
@@ -0,0 +1,80 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import MediaModal from './media_modal';
+import { TransitionMotion, spring } from 'react-motion';
+
+const MODAL_COMPONENTS = {
+  'MEDIA': MediaModal
+};
+
+const ModalRoot = React.createClass({
+
+  propTypes: {
+    type: React.PropTypes.string,
+    props: React.PropTypes.object,
+    onClose: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleKeyUp (e) {
+    if (e.key === 'Escape' && !!this.props.type) {
+      this.props.onClose();
+    }
+  },
+
+  componentDidMount () {
+    window.addEventListener('keyup', this.handleKeyUp, false);
+  },
+
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this.handleKeyUp);
+  },
+
+  willEnter () {
+    return { opacity: 0, scale: 0.98 };
+  },
+
+  willLeave () {
+    return { opacity: spring(0), scale: spring(0.98) };
+  },
+
+  render () {
+    const { type, props, onClose } = this.props;
+    const items = [];
+
+    if (!!type) {
+      items.push({
+        key: type,
+        data: { type, props },
+        style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) }
+      });
+    }
+
+    return (
+      <TransitionMotion
+        styles={items}
+        willEnter={this.willEnter}
+        willLeave={this.willLeave}>
+        {interpolatedStyles =>
+          <div className='modal-root'>
+            {interpolatedStyles.map(({ key, data: { type, props }, style }) => {
+              const SpecificComponent = MODAL_COMPONENTS[type];
+
+              return (
+                <div key={key}>
+                  <div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
+                  <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
+                    <SpecificComponent {...props} onClose={onClose} />
+                  </div>
+                </div>
+              );
+            })}
+          </div>
+        }
+      </TransitionMotion>
+    );
+  }
+
+});
+
+export default ModalRoot;
diff --git a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx
index 225a6a5fc..6cdb29dbf 100644
--- a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx
+++ b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx
@@ -1,15 +1,23 @@
 import { Link } from 'react-router';
 import { FormattedMessage } from 'react-intl';
 
-const TabsBar = () => {
-  return (
-    <div className='tabs-bar'>
-      <Link className='tabs-bar__link' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /> <FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
-      <Link className='tabs-bar__link' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /> <FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
-      <Link className='tabs-bar__link' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /> <FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
-      <Link className='tabs-bar__link' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link>
-    </div>
-  );
-};
+const TabsBar = React.createClass({
+
+  render () {
+    return (
+      <div className='tabs-bar'>
+        <Link className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
+        <Link className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
+        <Link className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
+
+        <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local'><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></Link>
+        <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public'><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></Link>
+
+        <Link className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link>
+      </div>
+    );
+  }
+
+});
 
 export default TabsBar;
diff --git a/app/assets/javascripts/components/features/ui/components/upload_area.jsx b/app/assets/javascripts/components/features/ui/components/upload_area.jsx
new file mode 100644
index 000000000..70b687019
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/upload_area.jsx
@@ -0,0 +1,32 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { Motion, spring } from 'react-motion';
+import { FormattedMessage } from 'react-intl';
+
+const UploadArea = React.createClass({
+
+  propTypes: {
+    active: React.PropTypes.bool
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { active } = this.props;
+
+    return (
+      <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
+        {({ backgroundOpacity, backgroundScale }) =>
+          <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
+            <div className='upload-area__drop'>
+              <div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} />
+              <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
+            </div>
+          </div>
+        }
+      </Motion>
+    );
+  }
+
+});
+
+export default UploadArea;
diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
index 4c47fb8c5..26d77818c 100644
--- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -1,167 +1,16 @@
 import { connect } from 'react-redux';
-import {
-  closeModal,
-  decreaseIndexInModal,
-  increaseIndexInModal
-} from '../../../actions/modal';
-import Lightbox from '../../../components/lightbox';
-import ImageLoader from 'react-imageloader';
-import LoadingIndicator from '../../../components/loading_indicator';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import ImmutablePropTypes from 'react-immutable-proptypes';
+import { closeModal } from '../../../actions/modal';
+import ModalRoot from '../components/modal_root';
 
 const mapStateToProps = state => ({
-  media: state.getIn(['modal', 'media']),
-  index: state.getIn(['modal', 'index']),
-  isVisible: state.getIn(['modal', 'open'])
+  type: state.get('modal').modalType,
+  props: state.get('modal').modalProps
 });
 
 const mapDispatchToProps = dispatch => ({
-  onCloseClicked () {
+  onClose () {
     dispatch(closeModal());
   },
-
-  onOverlayClicked () {
-    dispatch(closeModal());
-  },
-
-  onNextClicked () {
-    dispatch(increaseIndexInModal());
-  },
-
-  onPrevClicked () {
-    dispatch(decreaseIndexInModal());
-  }
-});
-
-const imageStyle = {
-  display: 'block',
-  maxWidth: '80vw',
-  maxHeight: '80vh'
-};
-
-const loadingStyle = {
-  width: '400px',
-  paddingBottom: '120px'
-};
-
-const preloader = () => (
-  <div className='modal-container--preloader' style={loadingStyle}>
-    <LoadingIndicator />
-  </div>
-);
-
-const leftNavStyle = {
-  position: 'absolute',
-  background: 'rgba(0, 0, 0, 0.5)',
-  padding: '30px 15px',
-  cursor: 'pointer',
-  fontSize: '24px',
-  top: '0',
-  left: '-61px',
-  boxSizing: 'border-box',
-  height: '100%',
-  display: 'flex',
-  alignItems: 'center'
-};
-
-const rightNavStyle = {
-  position: 'absolute',
-  background: 'rgba(0, 0, 0, 0.5)',
-  padding: '30px 15px',
-  cursor: 'pointer',
-  fontSize: '24px',
-  top: '0',
-  right: '-61px',
-  boxSizing: 'border-box',
-  height: '100%',
-  display: 'flex',
-  alignItems: 'center'
-};
-
-const Modal = React.createClass({
-
-  propTypes: {
-    media: ImmutablePropTypes.list,
-    index: React.PropTypes.number.isRequired,
-    isVisible: React.PropTypes.bool,
-    onCloseClicked: React.PropTypes.func,
-    onOverlayClicked: React.PropTypes.func,
-    onNextClicked: React.PropTypes.func,
-    onPrevClicked: React.PropTypes.func
-  },
-
-  mixins: [PureRenderMixin],
-
-  handleNextClick () {
-    this.props.onNextClicked();
-  },
-
-  handlePrevClick () {
-    this.props.onPrevClicked();
-  },
-
-  componentDidMount () {
-    this._listener = e => {
-      if (!this.props.isVisible) {
-        return;
-      }
-
-      switch(e.key) {
-      case 'ArrowLeft':
-        this.props.onPrevClicked();
-        break;
-      case 'ArrowRight':
-        this.props.onNextClicked();
-        break;
-      }
-    };
-
-    window.addEventListener('keyup', this._listener);
-  },
-
-  componentWillUnmount () {
-    window.removeEventListener('keyup', this._listener);
-  },
-
-  render () {
-    const { media, index, ...other } = this.props;
-
-    if (!media) {
-      return null;
-    }
-
-    const url      = media.get(index).get('url');
-    const hasLeft  = index > 0;
-    const hasRight = index + 1 < media.size;
-
-    let leftNav, rightNav;
-
-    leftNav = rightNav = '';
-
-    if (hasLeft) {
-      leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
-    }
-
-    if (hasRight) {
-      rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
-    }
-
-    return (
-      <Lightbox {...other}>
-        {leftNav}
-
-        <ImageLoader
-          src={url}
-          preloader={preloader}
-          imgProps={{ style: imageStyle }}
-        />
-
-        {rightNav}
-      </Lightbox>
-    );
-  }
-
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(Modal);
+export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);
diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
index 100989d22..f249240d8 100644
--- a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
@@ -3,8 +3,9 @@ import StatusList from '../../../components/status_list';
 import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines';
 import Immutable from 'immutable';
 import { createSelector } from 'reselect';
+import { debounce } from 'react-decoration';
 
-const getStatusIds = createSelector([
+const makeGetStatusIds = () => createSelector([
   (state, { type }) => state.getIn(['settings', type], Immutable.Map()),
   (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()),
   (state)           => state.get('statuses'),
@@ -33,26 +34,37 @@ const getStatusIds = createSelector([
   return showStatus;
 }));
 
-const mapStateToProps = (state, props) => ({
-  statusIds: getStatusIds(state, props),
-  isLoading: state.getIn(['timelines', props.type, 'isLoading'], true)
-});
+const makeMapStateToProps = () => {
+  const getStatusIds = makeGetStatusIds();
+
+  const mapStateToProps = (state, props) => ({
+    statusIds: getStatusIds(state, props),
+    isLoading: state.getIn(['timelines', props.type, 'isLoading'], true),
+    isUnread: state.getIn(['timelines', props.type, 'unread']) > 0,
+    hasMore: !!state.getIn(['timelines', props.type, 'next'])
+  });
+
+  return mapStateToProps;
+};
 
 const mapDispatchToProps = (dispatch, { type, id }) => ({
 
+  @debounce(300, true)
   onScrollToBottom () {
     dispatch(scrollTopTimeline(type, false));
     dispatch(expandTimeline(type, id));
   },
 
+  @debounce(100)
   onScrollToTop () {
     dispatch(scrollTopTimeline(type, true));
   },
 
+  @debounce(100)
   onScroll () {
     dispatch(scrollTopTimeline(type, false));
   }
 
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
+export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
index 900d83dba..89fb82568 100644
--- a/app/assets/javascripts/components/features/ui/index.jsx
+++ b/app/assets/javascripts/components/features/ui/index.jsx
@@ -13,6 +13,7 @@ import { debounce } from 'react-decoration';
 import { uploadCompose } from '../../actions/compose';
 import { refreshTimeline } from '../../actions/timelines';
 import { refreshNotifications } from '../../actions/notifications';
+import UploadArea from './components/upload_area';
 
 const UI = React.createClass({
 
@@ -23,7 +24,8 @@ const UI = React.createClass({
 
   getInitialState () {
     return {
-      width: window.innerWidth
+      width: window.innerWidth,
+      draggingOver: false
     };
   },
 
@@ -34,29 +36,64 @@ const UI = React.createClass({
     this.setState({ width: window.innerWidth });
   },
 
+  handleDragEnter (e) {
+    e.preventDefault();
+
+    if (!this.dragTargets) {
+      this.dragTargets = [];
+    }
+
+    if (this.dragTargets.indexOf(e.target) === -1) {
+      this.dragTargets.push(e.target);
+    }
+
+    if (e.dataTransfer && e.dataTransfer.files.length > 0) {
+      this.setState({ draggingOver: true });
+    }
+  },
+
   handleDragOver (e) {
     e.preventDefault();
     e.stopPropagation();
 
-    e.dataTransfer.dropEffect = 'copy';
+    try {
+      e.dataTransfer.dropEffect = 'copy';
+    } catch (err) {
 
-    if (e.dataTransfer.effectAllowed === 'all' || e.dataTransfer.effectAllowed === 'uninitialized') {
-      //
     }
+
+    return false;
   },
 
   handleDrop (e) {
     e.preventDefault();
 
+    this.setState({ draggingOver: false });
+
     if (e.dataTransfer && e.dataTransfer.files.length === 1) {
       this.props.dispatch(uploadCompose(e.dataTransfer.files));
     }
   },
 
+  handleDragLeave (e) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el));
+
+    if (this.dragTargets.length > 0) {
+      return;
+    }
+
+    this.setState({ draggingOver: false });
+  },
+
   componentWillMount () {
     window.addEventListener('resize', this.handleResize, { passive: true });
-    window.addEventListener('dragover', this.handleDragOver);
-    window.addEventListener('drop', this.handleDrop);
+    document.addEventListener('dragenter', this.handleDragEnter, false);
+    document.addEventListener('dragover', this.handleDragOver, false);
+    document.addEventListener('drop', this.handleDrop, false);
+    document.addEventListener('dragleave', this.handleDragLeave, false);
 
     this.props.dispatch(refreshTimeline('home'));
     this.props.dispatch(refreshNotifications());
@@ -64,17 +101,26 @@ const UI = React.createClass({
 
   componentWillUnmount () {
     window.removeEventListener('resize', this.handleResize);
-    window.removeEventListener('dragover', this.handleDragOver);
-    window.removeEventListener('drop', this.handleDrop);
+    document.removeEventListener('dragenter', this.handleDragEnter);
+    document.removeEventListener('dragover', this.handleDragOver);
+    document.removeEventListener('drop', this.handleDrop);
+    document.removeEventListener('dragleave', this.handleDragLeave);
+  },
+
+  setRef (c) {
+    this.node = c;
   },
 
   render () {
+    const { width, draggingOver } = this.state;
+    const { children } = this.props;
+
     let mountedColumns;
 
-    if (isMobile(this.state.width)) {
+    if (isMobile(width)) {
       mountedColumns = (
         <ColumnsArea>
-          {this.props.children}
+          {children}
         </ColumnsArea>
       );
     } else {
@@ -83,13 +129,13 @@ const UI = React.createClass({
           <Compose withHeader={true} />
           <HomeTimeline trackScroll={false} />
           <Notifications trackScroll={false} />
-          {this.props.children}
+          {children}
         </ColumnsArea>
       );
     }
 
     return (
-      <div className='ui'>
+      <div className='ui' ref={this.setRef}>
         <TabsBar />
 
         {mountedColumns}
@@ -97,6 +143,7 @@ const UI = React.createClass({
         <NotificationsContainer />
         <LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
         <ModalContainer />
+        <UploadArea active={draggingOver} />
       </div>
     );
   }