commit: fe8dd58bc1548b59741d30d9b3ce46f08b6afcd8
parent: 92cd207c5083e60074e0ce123bb5b848a58e7417
Author: Patrick Figel <patrick@figel.email>
Date: Sat, 15 Apr 2017 01:23:49 +0200
Add list of muted user to UI and Getting Started (#1799)
Add the same UI that already exists for blocked users for muted
ones and add it to the "Getting Started" menu.
Diffstat:
8 files changed, 183 insertions(+), 3 deletions(-)
diff --git a/app/assets/javascripts/components/actions/mutes.jsx 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
@@ -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 = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
} else if (blocking) {
- buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
+ buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
+ } else if (muting) {
+ buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
} else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}
diff --git 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({
<Route path='follow_requests' component={FollowRequests} />
<Route path='blocks' component={Blocks} />
+ <Route path='mutes' component={Mutes} />
<Route path='report' component={Report} />
<Route path='*' component={GenericNotFound} />
diff --git 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 }) => {
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
{followRequests}
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
+ <ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
<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/mutes/index.jsx 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 (
+ <Column>
+ <LoadingIndicator />
+ </Column>
+ );
+ }
+
+ return (
+ <Column icon='users' heading={intl.formatMessage(messages.heading)}>
+ <ColumnBackButtonSlim />
+ <ScrollContainer scrollKey='mutes'>
+ <div className='scrollable' onScroll={this.handleScroll}>
+ {accountIds.map(id =>
+ <AccountContainer key={id} id={id} />
+ )}
+ </div>
+ </ScrollContainer>
+ </Column>
+ );
+ }
+});
+
+export default connect(mapStateToProps)(injectIntl(Mutes));
diff --git 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
@@ -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
@@ -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;
}