@@ -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 | |||
}; | |||
}; |
@@ -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> | |||
@@ -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} > | |||
@@ -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) { | |||
@@ -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) { | |||
@@ -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> | |||
); | |||
} | |||
@@ -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; | |||
} | |||