commit: 36721239b2985bea833ff684e60efc28d517d36c
parent: 1f4bc613a4ec26fa0f89e3fbdf826a9f2e8c11a2
Author: Morgan Bazalgette <the@howl.moe>
Date: Sat, 31 Mar 2018 12:07:59 +0200
Merge branch 'master' of github.com:tootsuite/mastodon
Diffstat:
19 files changed, 283 insertions(+), 24 deletions(-)
diff --git a/Gemfile b/Gemfile
@@ -25,7 +25,7 @@ gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5'
gem 'bootsnap'
gem 'browser'
-gem 'charlock_holmes', '~> 0.7.5'
+gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
@@ -113,7 +113,7 @@ GEM
xpath (~> 2.0)
case_transform (0.2)
activesupport
- charlock_holmes (0.7.5)
+ charlock_holmes (0.7.6)
chewy (5.0.0)
activesupport (>= 4.0)
elasticsearch (>= 2.0.0)
@@ -632,7 +632,7 @@ DEPENDENCIES
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 2.15)
- charlock_holmes (~> 0.7.5)
+ charlock_holmes (~> 0.7.6)
chewy (~> 5.0)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
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' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
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,
Lists,
} from './util/async-components';
@@ -155,6 +156,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
@@ -86,6 +86,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/locales/ja.json b/app/javascript/mastodon/locales/ja.json
@@ -2,7 +2,7 @@
"account.block": "@{name}さんをブロック",
"account.block_domain": "{domain}全体を非表示",
"account.blocked": "ブロック済み",
- "account.direct": "Direct Message @{name}",
+ "account.direct": "@{name}さんにダイレクトメッセージ",
"account.disclaimer_full": "以下の情報は不正確な可能性があります。",
"account.domain_blocked": "ドメイン非表示中",
"account.edit_profile": "プロフィールを編集",
@@ -57,7 +57,7 @@
"column_header.unpin": "ピン留めを外す",
"column_subheading.navigation": "ナビゲーション",
"column_subheading.settings": "設定",
- "compose_form.direct_message_warning": "This post will only be visible to all the mentioned users.",
+ "compose_form.direct_message_warning": "このトゥートはメンションされた人だけが見ることができます。",
"compose_form.hashtag_warning": "このトゥートは未収載なのでハッシュタグの一覧に表示されません。公開トゥートだけがハッシュタグで検索できます。",
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
"compose_form.lock_disclaimer.lock": "非公開",
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%);
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
@@ -79,6 +79,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag)
status.tags << hashtag
+ rescue ActiveRecord::RecordInvalid
+ nil
end
def process_mention(tag, status)
diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb
@@ -44,7 +44,7 @@ class FetchAtomService < BaseService
json = body_to_json(body)
if supported_context?(json) && json['type'] == 'Person' && json['inbox'].present?
[json['id'], { prefetched_body: body, id: true }, :activitypub]
- elsif supported_context?(json) && json['type'] == 'Note'
+ elsif supported_context?(json) && expected_type?(json)
[json['id'], { prefetched_body: body, id: true }, :activitypub]
else
@unsupported_activity = true
@@ -61,6 +61,10 @@ class FetchAtomService < BaseService
end
end
+ def expected_type?(json)
+ (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? json['type']
+ end
+
def process_html(response)
page = Nokogiri::HTML(response.body_with_limit)
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
@@ -19,7 +19,7 @@ class ResolveURLService < BaseService
case type
when 'Person'
FetchRemoteAccountService.new.call(atom_url, body, protocol)
- when 'Note'
+ when 'Note', 'Article', 'Image', 'Video'
FetchRemoteStatusService.new.call(atom_url, body, protocol)
end
end