Add the same UI that already exists for blocked users for muted ones and add it to the "Getting Started" menu.master
@@ -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 | |||
}; | |||
}; |
@@ -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} />; | |||
} | |||
@@ -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} /> | |||
@@ -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> | |||
@@ -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)); |
@@ -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", | |||
@@ -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: | |||
@@ -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; | |||
} | |||