diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/assets/javascripts/components/actions/modal.jsx
index d19218c485..615cd6bfe8 100644
--- a/app/assets/javascripts/components/actions/modal.jsx
+++ b/app/assets/javascripts/components/actions/modal.jsx
@@ -1,14 +1,11 @@
-export const MEDIA_OPEN = 'MEDIA_OPEN';
+export const MODAL_OPEN = 'MODAL_OPEN';
export const MODAL_CLOSE = 'MODAL_CLOSE';
-export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE';
-export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE';
-
-export function openMedia(media, index) {
+export function openModal(type, props) {
return {
- type: MEDIA_OPEN,
- media,
- index
+ type: MODAL_OPEN,
+ modalType: type,
+ modalProps: props
};
};
@@ -17,15 +14,3 @@ export function closeModal() {
type: MODAL_CLOSE
};
};
-
-export function decreaseIndexInModal() {
- return {
- type: MODAL_INDEX_DECREASE
- };
-};
-
-export function increaseIndexInModal() {
- return {
- type: MODAL_INDEX_INCREASE
- };
-};
diff --git a/app/assets/javascripts/components/components/lightbox.jsx b/app/assets/javascripts/components/components/lightbox.jsx
deleted file mode 100644
index f04ca47bae..0000000000
--- a/app/assets/javascripts/components/components/lightbox.jsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import IconButton from './icon_button';
-import { Motion, spring } from 'react-motion';
-import { injectIntl } from 'react-intl';
-
-const overlayStyle = {
- position: 'fixed',
- top: '0',
- left: '0',
- width: '100%',
- height: '100%',
- background: 'rgba(0, 0, 0, 0.5)',
- display: 'flex',
- justifyContent: 'center',
- alignContent: 'center',
- flexDirection: 'row',
- zIndex: '9999'
-};
-
-const dialogStyle = {
- color: '#282c37',
- boxShadow: '0 0 30px rgba(0, 0, 0, 0.8)',
- margin: 'auto',
- position: 'relative'
-};
-
-const closeStyle = {
- position: 'absolute',
- top: '4px',
- right: '4px'
-};
-
-const Lightbox = React.createClass({
-
- propTypes: {
- isVisible: React.PropTypes.bool,
- onOverlayClicked: React.PropTypes.func,
- onCloseClicked: React.PropTypes.func,
- intl: React.PropTypes.object.isRequired,
- children: React.PropTypes.node
- },
-
- mixins: [PureRenderMixin],
-
- componentDidMount () {
- this._listener = e => {
- if (this.props.isVisible && e.key === 'Escape') {
- this.props.onCloseClicked();
- }
- };
-
- window.addEventListener('keyup', this._listener);
- },
-
- componentWillUnmount () {
- window.removeEventListener('keyup', this._listener);
- },
-
- stopPropagation (e) {
- e.stopPropagation();
- },
-
- render () {
- const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props;
-
- return (
-
- {({ backgroundOpacity, opacity, y }) =>
-
- }
-
- );
- }
-
-});
-
-export default injectIntl(Lightbox);
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index e7543bc397..fd3fbe4c32 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -17,7 +17,7 @@ import {
} from '../actions/accounts';
import { deleteStatus } from '../actions/statuses';
import { initReport } from '../actions/reports';
-import { openMedia } from '../actions/modal';
+import { openModal } from '../actions/modal';
import { createSelector } from 'reselect'
import { isMobile } from '../is_mobile'
@@ -63,7 +63,7 @@ const mapDispatchToProps = (dispatch) => ({
},
onOpenMedia (media, index) {
- dispatch(openMedia(media, index));
+ dispatch(openModal('MEDIA', { media, index }));
},
onBlock (account) {
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 6a7635cc66..f98fe1b013 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -28,7 +28,7 @@ import {
import { ScrollContainer } from 'react-router-scroll';
import ColumnBackButton from '../../components/column_back_button';
import StatusContainer from '../../containers/status_container';
-import { openMedia } from '../../actions/modal';
+import { openModal } from '../../actions/modal';
import { isMobile } from '../../is_mobile'
const makeMapStateToProps = () => {
@@ -99,7 +99,7 @@ const Status = React.createClass({
},
handleOpenMedia (media, index) {
- this.props.dispatch(openMedia(media, index));
+ this.props.dispatch(openModal('MEDIA', { media, index }));
},
handleReport (status) {
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 0000000000..e8b7180944
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx
@@ -0,0 +1,134 @@
+import Lightbox from '../../../components/lightbox';
+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 =
;
+ rightNav =
;
+ }
+
+ if (attachment.get('type') === 'image') {
+ content = ;
+ } else if (attachment.get('type') === 'gifv') {
+ content = ;
+ }
+
+ return (
+
+ {leftNav}
+
+
+
+ {content}
+
+
+ {rightNav}
+
+ );
+ }
+
+});
+
+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 0000000000..d2ae5e145e
--- /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 (
+
+ {interpolatedStyles =>
+
+ {interpolatedStyles.map(({ key, data: { type, props }, style }) => {
+ const SpecificComponent = MODAL_COMPONENTS[type];
+
+ return (
+
+ );
+ })}
+
+ }
+
+ );
+ }
+
+});
+
+export default ModalRoot;
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 e3c4281b93..26d77818c2 100644
--- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -1,170 +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 ExtendedVideoPlayer from '../../../components/extended_video_player';
+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 = () => (
-
-
-
-);
-
-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 attachment = media.get(index);
- const url = attachment.get('url');
-
- let leftNav, rightNav, content;
-
- leftNav = rightNav = content = '';
-
- if (media.size > 1) {
- leftNav =
;
- rightNav =
;
- }
-
- if (attachment.get('type') === 'image') {
- content = (
-
- );
- } else if (attachment.get('type') === 'gifv') {
- content = ;
- }
-
- return (
-
- {leftNav}
- {content}
- {rightNav}
-
- );
- }
-
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(Modal);
+export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
index 4b7e4bb460..89fb82568f 100644
--- a/app/assets/javascripts/components/features/ui/index.jsx
+++ b/app/assets/javascripts/components/features/ui/index.jsx
@@ -47,7 +47,9 @@ const UI = React.createClass({
this.dragTargets.push(e.target);
}
- this.setState({ draggingOver: true });
+ if (e.dataTransfer && e.dataTransfer.files.length > 0) {
+ this.setState({ draggingOver: true });
+ }
},
handleDragOver (e) {
diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx
index 37ffbc62b2..3566820ef1 100644
--- a/app/assets/javascripts/components/reducers/modal.jsx
+++ b/app/assets/javascripts/components/reducers/modal.jsx
@@ -1,31 +1,17 @@
-import {
- MEDIA_OPEN,
- MODAL_CLOSE,
- MODAL_INDEX_DECREASE,
- MODAL_INDEX_INCREASE
-} from '../actions/modal';
+import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
import Immutable from 'immutable';
-const initialState = Immutable.Map({
- media: null,
- index: 0,
- open: false
-});
+const initialState = {
+ modalType: null,
+ modalProps: {}
+};
export default function modal(state = initialState, action) {
switch(action.type) {
- case MEDIA_OPEN:
- return state.withMutations(map => {
- map.set('media', action.media);
- map.set('index', action.index);
- map.set('open', true);
- });
+ case MODAL_OPEN:
+ return { modalType: action.modalType, modalProps: action.modalProps };
case MODAL_CLOSE:
- return state.set('open', false);
- case MODAL_INDEX_DECREASE:
- return state.update('index', index => (index - 1) % state.get('media').size);
- case MODAL_INDEX_INCREASE:
- return state.update('index', index => (index + 1) % state.get('media').size);
+ return initialState;
default:
return state;
}
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index a4dce7f181..d87e094535 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -1311,7 +1311,7 @@ button.active i.fa-retweet {
color: $color3;
}
-.modal-container--nav {
+.modal-container__nav {
color: $color5;
}
@@ -1848,3 +1848,47 @@ button.active i.fa-retweet {
text-decoration: underline;
}
}
+
+.modal-root__overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 9999;
+ opacity: 0;
+ background: rgba($color8, 0.7);
+}
+
+.modal-root__container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ align-content: space-around;
+ z-index: 9999;
+ opacity: 0;
+ pointer-events: none;
+ user-select: none;
+}
+
+.modal-root__modal {
+ pointer-events: auto;
+ display: flex;
+}
+
+.media-modal {
+ max-width: 80vw;
+ max-height: 80vh;
+ position: relative;
+
+ img, video {
+ max-width: 80vw;
+ max-height: 80vh;
+ }
+}