about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-02-05 02:48:11 +0100
committerEugen Rochko <eugen@zeonfederated.com>2017-02-05 02:48:11 +0100
commit44fad0160f4b390a97fae8bb23cdc98ccf5649d9 (patch)
treeef09713b0e616ba9054740f983643554be6b1559
parent21972bb39886942d6946757ff8c8f9fe329bb20f (diff)
Add next/previous navigation in modal for media attachments
-rw-r--r--app/assets/javascripts/components/actions/modal.jsx20
-rw-r--r--app/assets/javascripts/components/components/lightbox.jsx6
-rw-r--r--app/assets/javascripts/components/components/media_gallery.jsx11
-rw-r--r--app/assets/javascripts/components/containers/status_container.jsx4
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx4
-rw-r--r--app/assets/javascripts/components/features/ui/containers/modal_container.jsx89
-rw-r--r--app/assets/javascripts/components/reducers/modal.jsx19
7 files changed, 132 insertions, 21 deletions
diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/assets/javascripts/components/actions/modal.jsx
index 89dbc7947..d19218c48 100644
--- a/app/assets/javascripts/components/actions/modal.jsx
+++ b/app/assets/javascripts/components/actions/modal.jsx
@@ -1,10 +1,14 @@
 export const MEDIA_OPEN  = 'MEDIA_OPEN';
 export const MODAL_CLOSE = 'MODAL_CLOSE';
 
-export function openMedia(url) {
+export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE';
+export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE';
+
+export function openMedia(media, index) {
   return {
     type: MEDIA_OPEN,
-    url: url
+    media,
+    index
   };
 };
 
@@ -13,3 +17,15 @@ 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
index 1e3a88955..646484539 100644
--- a/app/assets/javascripts/components/components/lightbox.jsx
+++ b/app/assets/javascripts/components/components/lightbox.jsx
@@ -56,6 +56,10 @@ const Lightbox = React.createClass({
     window.removeEventListener('keyup', this._listener);
   },
 
+  stopPropagation (e) {
+    e.stopPropagation();
+  },
+
   render () {
     const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props;
 
@@ -63,7 +67,7 @@ const Lightbox = React.createClass({
       <Motion defaultStyle={{ backgroundOpacity: 0, opacity: 0, y: -400 }} style={{ backgroundOpacity: spring(isVisible ? 50 : 0), opacity: isVisible ? spring(200) : 0, y: spring(isVisible ? 0 : -400, { stiffness: 150, damping: 12 }) }}>
         {({ backgroundOpacity, opacity, y }) =>
           <div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex'}} onClick={onOverlayClicked}>
-            <div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }}>
+            <div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }} onClick={this.stopPropagation}>
               <IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} />
               {children}
             </div>
diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx
index 7e92abe2d..a13448d0b 100644
--- a/app/assets/javascripts/components/components/media_gallery.jsx
+++ b/app/assets/javascripts/components/components/media_gallery.jsx
@@ -57,15 +57,16 @@ const MediaGallery = React.createClass({
     sensitive: React.PropTypes.bool,
     media: ImmutablePropTypes.list.isRequired,
     height: React.PropTypes.number.isRequired,
-    onOpenMedia: React.PropTypes.func.isRequired
+    onOpenMedia: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
   },
 
   mixins: [PureRenderMixin],
 
-  handleClick (url, e) {
+  handleClick (index, e) {
     if (e.button === 0) {
       e.preventDefault();
-      this.props.onOpenMedia(url);
+      this.props.onOpenMedia(this.props.media, index);
     }
 
     e.stopPropagation();
@@ -151,12 +152,12 @@ const MediaGallery = React.createClass({
 
         return (
           <div key={attachment.get('id')} style={{ boxSizing: 'border-box', position: 'relative', left: left, top: top, right: right, bottom: bottom, float: 'left', border: 'none', display: 'block', width: `${width}%`, height: `${height}%` }}>
-            <a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, attachment.get('url'))} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} />
+            <a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, i)} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} />
           </div>
         );
       });
     }
-    
+
     return (
       <div style={{ ...outerStyle, height: `${this.props.height}px` }}>
         <div style={spoilerButtonStyle} >
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index 1704a8cc2..f5fb09d52 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -91,8 +91,8 @@ const mapDispatchToProps = (dispatch) => ({
     dispatch(mentionCompose(account, router));
   },
 
-  onOpenMedia (url) {
-    dispatch(openMedia(url));
+  onOpenMedia (media, index) {
+    dispatch(openMedia(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 993c649d2..894fa3176 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -84,8 +84,8 @@ const Status = React.createClass({
     this.props.dispatch(mentionCompose(account, router));
   },
 
-  handleOpenMedia (url) {
-    this.props.dispatch(openMedia(url));
+  handleOpenMedia (media, index) {
+    this.props.dispatch(openMedia(media, index));
   },
 
   renderChildren (list) {
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 53d162462..0ffbfe8b3 100644
--- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -1,12 +1,18 @@
 import { connect } from 'react-redux';
-import { closeModal } from '../../../actions/modal';
+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';
 
 const mapStateToProps = state => ({
-  url: state.getIn(['modal', 'url']),
+  media: state.getIn(['modal', 'media']),
+  index: state.getIn(['modal', 'index']),
   isVisible: state.getIn(['modal', 'open'])
 });
 
@@ -17,6 +23,14 @@ const mapDispatchToProps = dispatch => ({
 
   onOverlayClicked () {
     dispatch(closeModal());
+  },
+
+  onNextClicked () {
+    dispatch(increaseIndexInModal());
+  },
+
+  onPrevClicked () {
+    dispatch(decreaseIndexInModal());
   }
 });
 
@@ -38,27 +52,92 @@ const preloader = () => (
   </div>
 );
 
+const leftNavStyle = {
+  position: 'absolute',
+  background: 'rgba(0, 0, 0, 0.5)',
+  padding: '30px 15px',
+  cursor: 'pointer',
+  color: '#fff',
+  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',
+  color: '#fff',
+  fontSize: '24px',
+  top: '0',
+  right: '-61px',
+  boxSizing: 'border-box',
+  height: '100%',
+  display: 'flex',
+  alignItems: 'center'
+};
+
 const Modal = React.createClass({
 
   propTypes: {
-    url: React.PropTypes.string,
+    media: ImmutablePropTypes.list,
+    index: React.PropTypes.number.isRequired,
     isVisible: React.PropTypes.bool,
     onCloseClicked: React.PropTypes.func,
-    onOverlayClicked: 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();
+  },
+
   render () {
-    const { url, ...other } = this.props;
+    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} onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
+    }
+
+    if (hasRight) {
+      rightNav = <div style={rightNavStyle} 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>
     );
   }
diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx
index ac53ea210..07da65771 100644
--- a/app/assets/javascripts/components/reducers/modal.jsx
+++ b/app/assets/javascripts/components/reducers/modal.jsx
@@ -1,8 +1,14 @@
-import { MEDIA_OPEN, MODAL_CLOSE } from '../actions/modal';
-import Immutable                   from 'immutable';
+import {
+  MEDIA_OPEN,
+  MODAL_CLOSE,
+  MODAL_INDEX_DECREASE,
+  MODAL_INDEX_INCREASE
+} from '../actions/modal';
+import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
-  url: '',
+  media: null,
+  index: 0,
   open: false
 });
 
@@ -10,11 +16,16 @@ export default function modal(state = initialState, action) {
   switch(action.type) {
   case MEDIA_OPEN:
     return state.withMutations(map => {
-      map.set('url', action.url);
+      map.set('media', action.media);
+      map.set('index', action.index);
       map.set('open', true);
     });
   case MODAL_CLOSE:
     return state.set('open', false);
+  case MODAL_INDEX_DECREASE:
+    return state.update('index', index => Math.max(index - 1, 0));
+  case MODAL_INDEX_INCREASE:
+    return state.update('index', index => Math.min(index + 1, state.get('media').size - 1));
   default:
     return state;
   }