diff --git a/app/assets/javascripts/components/actions/mutes.jsx b/app/assets/javascripts/components/actions/mutes.jsx new file mode 100644 index 000000000..824821594 --- /dev/null +++ b/app/assets/javascripts/components/actions/mutes.jsx @@ -0,0 +1,82 @@ +import api, { getLinks } from '../api' +import { fetchRelationships } from './accounts'; + +export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; +export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; +export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL'; + +export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST'; +export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; +export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; + +export function fetchMutes() { + return (dispatch, getState) => { + dispatch(fetchMutesRequest()); + + api(getState).get('/api/v1/mutes').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(fetchMutesFail(error))); + }; +}; + +export function fetchMutesRequest() { + return { + type: MUTES_FETCH_REQUEST + }; +}; + +export function fetchMutesSuccess(accounts, next) { + return { + type: MUTES_FETCH_SUCCESS, + accounts, + next + }; +}; + +export function fetchMutesFail(error) { + return { + type: MUTES_FETCH_FAIL, + error + }; +}; + +export function expandMutes() { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'mutes', 'next']); + + if (url === null) { + return; + } + + dispatch(expandMutesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(expandMutesFail(error))); + }; +}; + +export function expandMutesRequest() { + return { + type: MUTES_EXPAND_REQUEST + }; +}; + +export function expandMutesSuccess(accounts, next) { + return { + type: MUTES_EXPAND_SUCCESS, + accounts, + next + }; +}; + +export function expandMutesFail(error) { + return { + type: MUTES_EXPAND_FAIL, + error + }; +}; diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 8ce9b1929..6fda5915e 100644 --- a/app/assets/javascripts/components/components/account.jsx +++ b/app/assets/javascripts/components/components/account.jsx @@ -10,7 +10,8 @@ const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock' } + unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute' } }); const buttonsStyle = { @@ -25,6 +26,7 @@ const Account = React.createClass({ me: React.PropTypes.number.isRequired, onFollow: React.PropTypes.func.isRequired, onBlock: React.PropTypes.func.isRequired, + onMute: React.PropTypes.func.isRequired, intl: React.PropTypes.object.isRequired }, @@ -38,6 +40,10 @@ const Account = React.createClass({ this.props.onBlock(this.props.account); }, + handleMute () { + this.props.onMute(this.props.account); + }, + render () { const { account, me, intl } = this.props; @@ -51,11 +57,14 @@ const Account = React.createClass({ const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); if (requested) { buttons = } else if (blocking) { - buttons = ; + buttons = ; + } else if (muting) { + buttons = ; } else { buttons = ; } diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 5cd727822..f1038bbe7 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -37,6 +37,7 @@ import FollowRequests from '../features/follow_requests'; import GenericNotFound from '../features/generic_not_found'; import FavouritedStatuses from '../features/favourited_statuses'; import Blocks from '../features/blocks'; +import Mutes from '../features/mutes'; import Report from '../features/report'; import { IntlProvider, addLocaleData } from 'react-intl'; import en from 'react-intl/locale-data/en'; @@ -171,6 +172,7 @@ const Mastodon = React.createClass({ + diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 05bfcc221..6167d7cf0 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -14,6 +14,7 @@ const messages = defineMessages({ sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' } }); @@ -37,6 +38,7 @@ const GettingStarted = ({ intl, me }) => { {followRequests} + diff --git a/app/assets/javascripts/components/features/mutes/index.jsx b/app/assets/javascripts/components/features/mutes/index.jsx new file mode 100644 index 000000000..698b5408a --- /dev/null +++ b/app/assets/javascripts/components/features/mutes/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 { fetchMutes, expandMutes } from '../../actions/mutes'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + heading: { id: 'column.mutes', defaultMessage: 'Muted users' } +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'mutes', 'items']) +}); + +const Mutes = 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(fetchMutes()); + }, + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + + if (scrollTop === scrollHeight - clientHeight) { + this.props.dispatch(expandMutes()); + } + }, + + render () { + const { intl, accountIds } = this.props; + + if (!accountIds) { + return ( + + + + ); + } + + return ( + + + +
+ {accountIds.map(id => + + )} +
+
+
+ ); + } +}); + +export default connect(mapStateToProps)(injectIntl(Mutes)); diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 47e23d983..5dccb8070 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -31,6 +31,7 @@ const en = { "column.favourites": "Favourites", "column.follow_requests": "Follow requests", "column.home": "Home", + "column.mutes": "Muted users", "column.notifications": "Notifications", "column.public": "Federated timeline", "compose_form.placeholder": "What is on your mind?", @@ -68,6 +69,7 @@ const en = { "navigation_bar.follow_requests": "Follow requests", "navigation_bar.info": "Extended information", "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", "navigation_bar.preferences": "Preferences", "navigation_bar.public_timeline": "Federated timeline", "notification.favourite": "{name} favourited your status", diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx index df9440093..60d283b0f 100644 --- a/app/assets/javascripts/components/reducers/accounts.jsx +++ b/app/assets/javascripts/components/reducers/accounts.jsx @@ -15,6 +15,10 @@ import { BLOCKS_FETCH_SUCCESS, BLOCKS_EXPAND_SUCCESS } from '../actions/blocks'; +import { + MUTES_FETCH_SUCCESS, + MUTES_EXPAND_SUCCESS +} from '../actions/mutes'; import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose'; import { REBLOG_SUCCESS, @@ -94,6 +98,8 @@ export default function accounts(state = initialState, action) { case FOLLOW_REQUESTS_EXPAND_SUCCESS: case BLOCKS_FETCH_SUCCESS: case BLOCKS_EXPAND_SUCCESS: + case MUTES_FETCH_SUCCESS: + case MUTES_EXPAND_SUCCESS: return normalizeAccounts(state, action.accounts); case NOTIFICATIONS_REFRESH_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS: diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx index 8c9a3d3aa..7f55c3641 100644 --- a/app/assets/javascripts/components/reducers/user_lists.jsx +++ b/app/assets/javascripts/components/reducers/user_lists.jsx @@ -16,6 +16,10 @@ import { BLOCKS_FETCH_SUCCESS, BLOCKS_EXPAND_SUCCESS } from '../actions/blocks'; +import { + MUTES_FETCH_SUCCESS, + MUTES_EXPAND_SUCCESS +} from '../actions/mutes'; import Immutable from 'immutable'; const initialState = Immutable.Map({ @@ -24,7 +28,8 @@ const initialState = Immutable.Map({ reblogged_by: Immutable.Map(), favourited_by: Immutable.Map(), follow_requests: Immutable.Map(), - blocks: Immutable.Map() + blocks: Immutable.Map(), + mutes: Immutable.Map() }); const normalizeList = (state, type, id, accounts, next) => { @@ -65,6 +70,10 @@ export default function userLists(state = initialState, action) { return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); case BLOCKS_EXPAND_SUCCESS: return state.updateIn(['blocks', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); + case MUTES_FETCH_SUCCESS: + return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + case MUTES_EXPAND_SUCCESS: + return state.updateIn(['mutes', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); default: return state; }