commit: a6c129ddbdaaa84bc631d85eb248fb5a9fa7eb96
parent 47cee7cc8e47471b372630cd28d50c6284aad8b3
Author: ThibG <thib@sitedethib.com>
Date: Fri, 30 Mar 2018 12:38:00 +0200
Add some UI for user-defined domain blocks (#6628)
* Keep list of blocked domains
Might be overkill, but I'm trying to follow the same logic as for blocked users
* Add basic domain block UI
* Add the domain blocks UI to Getting Started
* Fix undefined URL in `fetchDomainBlocks`
* Update all known users' domain_blocking relationship instead of just one's
Diffstat:
13 files changed, 271 insertions(+), 17 deletions(-)
diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js
@@ -12,12 +12,18 @@ export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL';
-export function blockDomain(domain, accountId) {
+export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST';
+export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS';
+export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL';
+
+export function blockDomain(domain) {
return (dispatch, getState) => {
dispatch(blockDomainRequest(domain));
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
- dispatch(blockDomainSuccess(domain, accountId));
+ const at_domain = '@' + domain;
+ const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
+ dispatch(blockDomainSuccess(domain, accounts));
}).catch(err => {
dispatch(blockDomainFail(domain, err));
});
@@ -31,11 +37,11 @@ export function blockDomainRequest(domain) {
};
};
-export function blockDomainSuccess(domain, accountId) {
+export function blockDomainSuccess(domain, accounts) {
return {
type: DOMAIN_BLOCK_SUCCESS,
domain,
- accountId,
+ accounts,
};
};
@@ -47,12 +53,14 @@ export function blockDomainFail(domain, error) {
};
};
-export function unblockDomain(domain, accountId) {
+export function unblockDomain(domain) {
return (dispatch, getState) => {
dispatch(unblockDomainRequest(domain));
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
- dispatch(unblockDomainSuccess(domain, accountId));
+ const at_domain = '@' + domain;
+ const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
+ dispatch(unblockDomainSuccess(domain, accounts));
}).catch(err => {
dispatch(unblockDomainFail(domain, err));
});
@@ -66,11 +74,11 @@ export function unblockDomainRequest(domain) {
};
};
-export function unblockDomainSuccess(domain, accountId) {
+export function unblockDomainSuccess(domain, accounts) {
return {
type: DOMAIN_UNBLOCK_SUCCESS,
domain,
- accountId,
+ accounts,
};
};
@@ -86,7 +94,7 @@ export function fetchDomainBlocks() {
return (dispatch, getState) => {
dispatch(fetchDomainBlocksRequest());
- api(getState).get().then(response => {
+ api(getState).get('/api/v1/domain_blocks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => {
@@ -115,3 +123,43 @@ export function fetchDomainBlocksFail(error) {
error,
};
};
+
+export function expandDomainBlocks() {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['domain_lists', 'blocks', 'next']);
+
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandDomainBlocksRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
+ }).catch(err => {
+ dispatch(expandDomainBlocksFail(err));
+ });
+ };
+};
+
+export function expandDomainBlocksRequest() {
+ return {
+ type: DOMAIN_BLOCKS_EXPAND_REQUEST,
+ };
+};
+
+export function expandDomainBlocksSuccess(domains, next) {
+ return {
+ type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
+ domains,
+ next,
+ };
+};
+
+export function expandDomainBlocksFail(error) {
+ return {
+ type: DOMAIN_BLOCKS_EXPAND_FAIL,
+ error,
+ };
+};
diff --git a/app/javascript/mastodon/components/domain.js b/app/javascript/mastodon/components/domain.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import IconButton from './icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+const messages = defineMessages({
+ unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+});
+
+@injectIntl
+export default class Account extends ImmutablePureComponent {
+
+ static propTypes = {
+ domain: PropTypes.string,
+ onUnblockDomain: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ handleDomainUnblock = () => {
+ this.props.onUnblockDomain(this.props.domain);
+ }
+
+ render () {
+ const { domain, intl } = this.props;
+
+ return (
+ <div className='domain'>
+ <div className='domain__wrapper'>
+ <span className='domain__domain-name'>
+ <strong>{domain}</strong>
+ </span>
+
+ <div className='domain__buttons'>
+ <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/containers/domain_container.js b/app/javascript/mastodon/containers/domain_container.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { blockDomain, unblockDomain } from '../actions/domain_blocks';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Domain from '../components/domain';
+import { openModal } from '../actions/modal';
+
+const messages = defineMessages({
+ blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
+});
+
+const makeMapStateToProps = () => {
+ const mapStateToProps = (state, { }) => ({
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+ onBlockDomain (domain) {
+ dispatch(openModal('CONFIRM', {
+ message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />,
+ confirm: intl.formatMessage(messages.blockDomainConfirm),
+ onConfirm: () => dispatch(blockDomain(domain)),
+ }));
+ },
+
+ onUnblockDomain (domain) {
+ dispatch(unblockDomain(domain));
+ },
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain));
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -62,7 +62,7 @@ export default class Header extends ImmutablePureComponent {
if (!domain) return;
- this.props.onBlockDomain(domain, this.props.account.get('id'));
+ this.props.onBlockDomain(domain);
}
handleUnblockDomain = () => {
@@ -70,7 +70,7 @@ export default class Header extends ImmutablePureComponent {
if (!domain) return;
- this.props.onUnblockDomain(domain, this.props.account.get('id'));
+ this.props.onUnblockDomain(domain);
}
render () {
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -94,16 +94,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
- onBlockDomain (domain, accountId) {
+ onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
- onConfirm: () => dispatch(blockDomain(domain, accountId)),
+ onConfirm: () => dispatch(blockDomain(domain)),
}));
},
- onUnblockDomain (domain, accountId) {
- dispatch(unblockDomain(domain, accountId));
+ onUnblockDomain (domain) {
+ dispatch(unblockDomain(domain));
},
});
diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import LoadingIndicator from '../../components/loading_indicator';
+import Column from '../ui/components/column';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import DomainContainer from '../../containers/domain_container';
+import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
+import { defineMessages, injectIntl } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { debounce } from 'lodash';
+import ScrollableList from '../../components/scrollable_list';
+
+const messages = defineMessages({
+ heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
+ unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+});
+
+const mapStateToProps = state => ({
+ domains: state.getIn(['domain_lists', 'blocks', 'items']),
+});
+
+@connect(mapStateToProps)
+@injectIntl
+export default class Blocks extends ImmutablePureComponent {
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ domains: ImmutablePropTypes.list,
+ intl: PropTypes.object.isRequired,
+ };
+
+ componentWillMount () {
+ this.props.dispatch(fetchDomainBlocks());
+ }
+
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandDomainBlocks());
+ }, 300, { leading: true });
+
+ render () {
+ const { intl, domains } = this.props;
+
+ if (!domains) {
+ return (
+ <Column>
+ <LoadingIndicator />
+ </Column>
+ );
+ }
+
+ return (
+ <Column icon='ban' heading={intl.formatMessage(messages.heading)}>
+ <ColumnBackButtonSlim />
+ <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore}>
+ {domains.map(domain =>
+ <DomainContainer key={domain} domain={domain} />
+ )}
+ </ScrollableList>
+ </Column>
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
@@ -24,6 +24,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' },
+ domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
@@ -121,6 +122,7 @@ export default class GettingStarted extends ImmutablePureComponent {
<ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
+ <ColumnLink icon='ban' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' />
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
</div>
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
@@ -37,6 +37,7 @@ import {
FavouritedStatuses,
ListTimeline,
Blocks,
+ DomainBlocks,
Mutes,
PinnedStatuses,
Lists,
@@ -158,6 +159,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} />
+ <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -90,6 +90,10 @@ export function Blocks () {
return import(/* webpackChunkName: "features/blocks" */'../../blocks');
}
+export function DomainBlocks () {
+ return import(/* webpackChunkName: "features/domain_blocks" */'../../domain_blocks');
+}
+
export function Mutes () {
return import(/* webpackChunkName: "features/mutes" */'../../mutes');
}
diff --git a/app/javascript/mastodon/reducers/domain_lists.js b/app/javascript/mastodon/reducers/domain_lists.js
@@ -0,0 +1,23 @@
+import {
+ DOMAIN_BLOCKS_FETCH_SUCCESS,
+ DOMAIN_BLOCKS_EXPAND_SUCCESS,
+ DOMAIN_UNBLOCK_SUCCESS,
+} from '../actions/domain_blocks';
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
+
+const initialState = ImmutableMap({
+ blocks: ImmutableMap(),
+});
+
+export default function domainLists(state = initialState, action) {
+ switch(action.type) {
+ case DOMAIN_BLOCKS_FETCH_SUCCESS:
+ return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
+ case DOMAIN_BLOCKS_EXPAND_SUCCESS:
+ return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next);
+ case DOMAIN_UNBLOCK_SUCCESS:
+ return state.updateIn(['blocks', 'items'], set => set.delete(action.domain));
+ default:
+ return state;
+ }
+};
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
@@ -6,6 +6,7 @@ import alerts from './alerts';
import { loadingBarReducer } from 'react-redux-loading-bar';
import modal from './modal';
import user_lists from './user_lists';
+import domain_lists from './domain_lists';
import accounts from './accounts';
import accounts_counters from './accounts_counters';
import statuses from './statuses';
@@ -34,6 +35,7 @@ const reducers = {
loadingBar: loadingBarReducer,
modal,
user_lists,
+ domain_lists,
status_lists,
accounts,
accounts_counters,
diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js
@@ -23,6 +23,14 @@ const normalizeRelationships = (state, relationships) => {
return state;
};
+const setDomainBlocking = (state, accounts, blocking) => {
+ return state.withMutations(map => {
+ accounts.forEach(id => {
+ map.setIn([id, 'domain_blocking'], blocking);
+ });
+ });
+};
+
const initialState = ImmutableMap();
export default function relationships(state = initialState, action) {
@@ -37,9 +45,9 @@ export default function relationships(state = initialState, action) {
case RELATIONSHIPS_FETCH_SUCCESS:
return normalizeRelationships(state, action.relationships);
case DOMAIN_BLOCK_SUCCESS:
- return state.setIn([action.accountId, 'domain_blocking'], true);
+ return setDomainBlocking(state, action.accounts, true);
case DOMAIN_UNBLOCK_SUCCESS:
- return state.setIn([action.accountId, 'domain_blocking'], false);
+ return setDomainBlocking(state, action.accounts, false);
default:
return state;
}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
@@ -1001,6 +1001,30 @@
}
}
+.domain {
+ padding: 10px;
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+ .domain__domain-name {
+ flex: 1 1 auto;
+ display: block;
+ color: $primary-text-color;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ }
+}
+
+.domain__wrapper {
+ display: flex;
+}
+
+.domain_buttons {
+ height: 18px;
+ padding: 10px;
+ white-space: nowrap;
+}
+
.account {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);