diff options
Diffstat (limited to 'app/assets/javascripts/components/features')
13 files changed, 288 insertions, 67 deletions
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index dead11265..30e0449c5 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -44,7 +44,7 @@ const Header = React.createClass({ <IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> </div> ); - } else { + } else if (!account.getIn(['relationship', 'blocking'])) { actionBtn = ( <div style={{ position: 'absolute', top: '10px', left: '20px' }}> <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> diff --git a/app/assets/javascripts/components/features/blocks/index.jsx b/app/assets/javascripts/components/features/blocks/index.jsx new file mode 100644 index 000000000..e941b27f7 --- /dev/null +++ b/app/assets/javascripts/components/features/blocks/index.jsx @@ -0,0 +1,68 @@ +import { connect } from 'react-redux'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import LoadingIndicator from '../../components/loading_indicator'; +import { ScrollContainer } from 'react-router-scroll'; +import Column from '../ui/components/column'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import AccountContainer from '../../containers/account_container'; +import { fetchBlocks, expandBlocks } from '../../actions/blocks'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + heading: { id: 'column.blocks', defaultMessage: 'Blocked users' } +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'blocks', 'items']) +}); + +const Blocks = React.createClass({ + propTypes: { + params: React.PropTypes.object.isRequired, + dispatch: React.PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + intl: React.PropTypes.object.isRequired + }, + + mixins: [PureRenderMixin], + + componentWillMount () { + this.props.dispatch(fetchBlocks()); + }, + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + + if (scrollTop === scrollHeight - clientHeight) { + this.props.dispatch(expandBlocks()); + } + }, + + render () { + const { intl, accountIds } = this.props; + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column icon='users' heading={intl.formatMessage(messages.heading)}> + <ColumnBackButtonSlim /> + <ScrollContainer scrollKey='blocks'> + <div className='scrollable' onScroll={this.handleScroll}> + {accountIds.map(id => + <AccountContainer key={id} id={id} /> + )} + </div> + </ScrollContainer> + </Column> + ); + } +}); + +export default connect(mapStateToProps)(injectIntl(Blocks)); diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index 5073c9d9e..48939054d 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -117,9 +117,10 @@ const ComposeForm = React.createClass({ }, render () { - const { intl } = this.props; - let replyArea = ''; - const disabled = this.props.is_submitting || this.props.is_uploading; + const { intl } = this.props; + let replyArea = ''; + let publishText = ''; + const disabled = this.props.is_submitting || this.props.is_uploading; if (this.props.in_reply_to) { replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />; @@ -127,6 +128,12 @@ const ComposeForm = React.createClass({ let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me); + if (this.props.private) { + publishText = <span><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; + } else { + publishText = intl.formatMessage(messages.publish) + (!this.props.unlisted ? '!' : ''); + } + return ( <div style={{ padding: '10px' }}> <Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}> @@ -154,19 +161,19 @@ const ComposeForm = React.createClass({ /> <div style={{ marginTop: '10px', overflow: 'hidden' }}> - <div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div> + <div style={{ float: 'right' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled} /></div> <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div> <UploadButtonContainer style={{ paddingTop: '4px' }} /> </div> <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', marginTop: '10px', borderTop: '1px solid #282c37', paddingTop: '10px' }}> - <Toggle checked={this.props.private} onChange={this.handleChangeVisibility} /> - <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span> + <Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} /> + <span style={{ display: 'inline-block', verticalAlign: 'top', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span> </label> - <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle' }}> - <Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} /> - <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span> + <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', borderTop: '1px solid #282c37', paddingTop: '10px' }}> + <Toggle checked={this.props.private} onChange={this.handleChangeVisibility} /> + <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span> </label> <Motion defaultStyle={{ opacity: (this.props.private || reply_to_other) ? 0 : 100, height: (this.props.private || reply_to_other) ? 39.5 : 0 }} style={{ opacity: spring((this.props.private || reply_to_other) ? 0 : 100), height: spring((this.props.private || reply_to_other) ? 0 : 39.5) }}> diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx index 8ccfce059..c027875cd 100644 --- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx @@ -26,14 +26,14 @@ const makeMapStateToProps = () => { sensitive: state.getIn(['compose', 'sensitive']), spoiler: state.getIn(['compose', 'spoiler']), spoiler_text: state.getIn(['compose', 'spoiler_text']), - unlisted: state.getIn(['compose', 'unlisted']), + unlisted: state.getIn(['compose', 'unlisted'], ), private: state.getIn(['compose', 'private']), fileDropDate: state.getIn(['compose', 'fileDropDate']), is_submitting: state.getIn(['compose', 'is_submitting']), is_uploading: state.getIn(['compose', 'is_uploading']), in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])), media_count: state.getIn(['compose', 'media_attachments']).size, - me: state.getIn(['compose', 'me']) + me: state.getIn(['compose', 'me']), }; }; diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 42e0a9e24..a0bf3a694 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -11,7 +11,9 @@ const messages = defineMessages({ preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' } + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' } }); const mapStateToProps = state => ({ @@ -32,6 +34,8 @@ const GettingStarted = ({ intl, me }) => { <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> <ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' /> {followRequests} + <ColumnLink icon='users' text={intl.formatMessage(messages.blocks)} to='/blocks' /> + <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> </div> diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx index 7548e6d56..4a0e7684d 100644 --- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx +++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx @@ -8,45 +8,49 @@ import { deleteFromTimelines } from '../../actions/timelines'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import createStream from '../../stream'; + +const mapStateToProps = state => ({ + accessToken: state.getIn(['meta', 'access_token']) +}); const HashtagTimeline = React.createClass({ propTypes: { params: React.PropTypes.object.isRequired, - dispatch: React.PropTypes.func.isRequired + dispatch: React.PropTypes.func.isRequired, + accessToken: React.PropTypes.string.isRequired }, mixins: [PureRenderMixin], _subscribe (dispatch, id) { - if (typeof App !== 'undefined') { - this.subscription = App.cable.subscriptions.create({ - channel: 'HashtagChannel', - tag: id - }, { - - received (data) { - switch(data.event) { - case 'update': - dispatch(updateTimeline('tag', JSON.parse(data.payload))); - break; - case 'delete': - dispatch(deleteFromTimelines(data.payload)); - break; - } + const { accessToken } = this.props; + + this.subscription = createStream(accessToken, `hashtag&tag=${id}`, { + + received (data) { + switch(data.event) { + case 'update': + dispatch(updateTimeline('tag', JSON.parse(data.payload))); + break; + case 'delete': + dispatch(deleteFromTimelines(data.payload)); + break; } + } - }); - } + }); }, _unsubscribe () { if (typeof this.subscription !== 'undefined') { - this.subscription.unsubscribe(); + this.subscription.close(); + this.subscription = null; } }, - componentWillMount () { + componentDidMount () { const { dispatch } = this.props; const { id } = this.props.params; @@ -79,4 +83,4 @@ const HashtagTimeline = React.createClass({ }); -export default connect()(HashtagTimeline); +export default connect(mapStateToProps)(HashtagTimeline); diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx new file mode 100644 index 000000000..d20a4d170 --- /dev/null +++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx @@ -0,0 +1,21 @@ +const iconStyle = { + fontSize: '16px', + padding: '15px', + position: 'absolute', + right: '48px', + top: '0', + cursor: 'pointer', + background: '#2f3441' +}; + +const ClearColumnButton = ({ onClick }) => ( + <div className='column-icon' style={iconStyle} onClick={onClick}> + <i className='fa fa-trash' /> + </div> +); + +ClearColumnButton.propTypes = { + onClick: React.PropTypes.func.isRequired +}; + +export default ClearColumnButton; diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx index d3300acd5..6d10768de 100644 --- a/app/assets/javascripts/components/features/notifications/index.jsx +++ b/app/assets/javascripts/components/features/notifications/index.jsx @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../ui/components/column'; -import { expandNotifications } from '../../actions/notifications'; +import { expandNotifications, clearNotifications } from '../../actions/notifications'; import NotificationContainer from './containers/notification_container'; import { ScrollContainer } from 'react-router-scroll'; import { defineMessages, injectIntl } from 'react-intl'; @@ -10,6 +10,7 @@ import ColumnSettingsContainer from './containers/column_settings_container'; import { createSelector } from 'reselect'; import Immutable from 'immutable'; import LoadMore from '../../components/load_more'; +import ClearColumnButton from './components/clear_column_button'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' } @@ -64,6 +65,10 @@ const Notifications = React.createClass({ this.props.dispatch(expandNotifications()); }, + handleClear () { + this.props.dispatch(clearNotifications()); + }, + setRef (c) { this.node = c; }, @@ -90,6 +95,7 @@ const Notifications = React.createClass({ return ( <Column icon='bell' heading={intl.formatMessage(messages.title)}> <ColumnSettingsContainer /> + <ClearColumnButton onClick={this.handleClear} /> <ScrollContainer scrollKey='notifications'> {scrollableArea} </ScrollContainer> @@ -99,6 +105,7 @@ const Notifications = React.createClass({ return ( <Column icon='bell' heading={intl.formatMessage(messages.title)}> <ColumnSettingsContainer /> + <ClearColumnButton onClick={this.handleClear} /> {scrollableArea} </Column> ); diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx index 42970061c..36d68dbbb 100644 --- a/app/assets/javascripts/components/features/public_timeline/index.jsx +++ b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -9,46 +9,51 @@ import { } from '../../actions/timelines'; import { defineMessages, injectIntl } from 'react-intl'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import createStream from '../../stream'; const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Public' } }); +const mapStateToProps = state => ({ + accessToken: state.getIn(['meta', 'access_token']) +}); + const PublicTimeline = React.createClass({ propTypes: { dispatch: React.PropTypes.func.isRequired, - intl: React.PropTypes.object.isRequired + intl: React.PropTypes.object.isRequired, + accessToken: React.PropTypes.string.isRequired }, mixins: [PureRenderMixin], - componentWillMount () { - const { dispatch } = this.props; + componentDidMount () { + const { dispatch, accessToken } = this.props; dispatch(refreshTimeline('public')); - if (typeof App !== 'undefined') { - this.subscription = App.cable.subscriptions.create('PublicChannel', { + this.subscription = createStream(accessToken, 'public', { - received (data) { - switch(data.event) { - case 'update': - dispatch(updateTimeline('public', JSON.parse(data.payload))); - break; - case 'delete': - dispatch(deleteFromTimelines(data.payload)); - break; - } + received (data) { + switch(data.event) { + case 'update': + dispatch(updateTimeline('public', JSON.parse(data.payload))); + break; + case 'delete': + dispatch(deleteFromTimelines(data.payload)); + break; } + } - }); - } + }); }, componentWillUnmount () { if (typeof this.subscription !== 'undefined') { - this.subscription.unsubscribe(); + this.subscription.close(); + this.subscription = null; } }, @@ -65,4 +70,4 @@ const PublicTimeline = React.createClass({ }); -export default connect()(injectIntl(PublicTimeline)); +export default connect(mapStateToProps)(injectIntl(PublicTimeline)); 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/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx index c382e108d..2eafe5a8f 100644 --- a/app/assets/javascripts/components/features/ui/components/column.jsx +++ b/app/assets/javascripts/components/features/ui/components/column.jsx @@ -1,4 +1,4 @@ -import ColumnHeader from './column_header'; +import ColumnHeader from './column_header'; import PureRenderMixin from 'react-addons-pure-render-mixin'; const easingOutQuint = (x, t, b, c, d) => c*((t=t/d-1)*t*t*t*t + 1) + b; @@ -58,16 +58,18 @@ const Column = React.createClass({ }, render () { + const { heading, icon, children } = this.props; + let header = ''; - if (this.props.heading) { - header = <ColumnHeader icon={this.props.icon} type={this.props.heading} onClick={this.handleHeaderClick} />; + if (heading) { + header = <ColumnHeader icon={icon} type={heading} onClick={this.handleHeaderClick} />; } return ( <div className='column' style={style} onWheel={this.handleWheel}> {header} - {this.props.children} + {children} </div> ); } 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..334e5c199 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,115 @@ 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(); + }, + + 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 { 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/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx index 8af7b0c3c..100989d22 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 @@ -7,8 +7,9 @@ import { createSelector } from 'reselect'; const getStatusIds = createSelector([ (state, { type }) => state.getIn(['settings', type], Immutable.Map()), (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()), - (state) => state.get('statuses') -], (columnSettings, statusIds, statuses) => statusIds.filter(id => { + (state) => state.get('statuses'), + (state) => state.getIn(['meta', 'me']) +], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => { const statusForId = statuses.get(id); let showStatus = true; @@ -17,7 +18,7 @@ const getStatusIds = createSelector([ } if (columnSettings.getIn(['shows', 'reply']) === false) { - showStatus = showStatus && statusForId.get('in_reply_to_id') === null; + showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me); } if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) { |