Browse Source

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.
Patrick Figel 7 years ago
parent
commit
fe8dd58bc1

+ 82 - 0
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
+  };
+};

+ 11 - 2
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} />;
       }

+ 2 - 0
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} />

+ 2 - 0
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>

+ 68 - 0
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));

+ 2 - 0
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",

+ 6 - 0
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:

+ 10 - 1
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;
   }