diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index c148dded54..464327cb58 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -15,6 +15,7 @@ import SensitiveButtonContainer from '../containers/sensitive_button_container';
import EmojiPickerDropdown from './emoji_picker_dropdown';
import UploadFormContainer from '../containers/upload_form_container';
import TextIconButton from './text_icon_button';
+import WarningContainer from '../containers/warning_container';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -116,26 +117,13 @@ class ComposeForm extends React.PureComponent {
}
render () {
- const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
+ const { intl, onPaste } = this.props;
const disabled = this.props.is_submitting;
const text = [this.props.spoiler_text, this.props.text].join('');
let publishText = '';
- let privacyWarning = '';
let reply_to_other = false;
- if (needsPrivacyWarning) {
- privacyWarning = (
-
- {mentionedDomains.join(', ')}, domainsCount: mentionedDomains.length }}
- />
-
- );
- }
-
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = {intl.formatMessage(messages.publish)};
} else {
@@ -150,7 +138,7 @@ class ComposeForm extends React.PureComponent {
- {privacyWarning}
+
@@ -208,8 +196,6 @@ ComposeForm.propTypes = {
is_submitting: PropTypes.bool,
is_uploading: PropTypes.bool,
me: PropTypes.number,
- needsPrivacyWarning: PropTypes.bool,
- mentionedDomains: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired,
diff --git a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx
index 507fe7b580..82b3454c61 100644
--- a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx
+++ b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx
@@ -7,7 +7,7 @@ const messages = defineMessages({
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
- private_short: { id: 'privacy.private.short', defaultMessage: 'Private' },
+ private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
diff --git a/app/assets/javascripts/components/features/compose/components/warning.jsx b/app/assets/javascripts/components/features/compose/components/warning.jsx
new file mode 100644
index 0000000000..ff1989755a
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/warning.jsx
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types';
+
+class Warning extends React.PureComponent {
+
+ constructor (props) {
+ super(props);
+ }
+
+ render () {
+ const { message } = this.props;
+
+ return (
+
+ {message}
+
+ );
+ }
+
+}
+
+Warning.propTypes = {
+ message: PropTypes.node.isRequired
+};
+
+export default Warning;
diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
index 604e1182f9..892183b831 100644
--- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
@@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import ComposeForm from '../components/compose_form';
import { uploadCompose } from '../../../actions/compose';
-import { createSelector } from 'reselect';
import {
changeCompose,
submitCompose,
@@ -12,33 +11,20 @@ import {
insertEmojiCompose
} from '../../../actions/compose';
-const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
-
-const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
- return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
+const mapStateToProps = state => ({
+ text: state.getIn(['compose', 'text']),
+ suggestion_token: state.getIn(['compose', 'suggestion_token']),
+ suggestions: state.getIn(['compose', 'suggestions']),
+ spoiler: state.getIn(['compose', 'spoiler']),
+ spoiler_text: state.getIn(['compose', 'spoiler_text']),
+ privacy: state.getIn(['compose', 'privacy']),
+ focusDate: state.getIn(['compose', 'focusDate']),
+ preselectDate: state.getIn(['compose', 'preselectDate']),
+ is_submitting: state.getIn(['compose', 'is_submitting']),
+ is_uploading: state.getIn(['compose', 'is_uploading']),
+ me: state.getIn(['compose', 'me'])
});
-const mapStateToProps = (state, props) => {
- const mentionedUsernames = getMentionedUsernames(state);
- const mentionedUsernamesWithDomains = getMentionedDomains(state);
-
- return {
- text: state.getIn(['compose', 'text']),
- suggestion_token: state.getIn(['compose', 'suggestion_token']),
- suggestions: state.getIn(['compose', 'suggestions']),
- spoiler: state.getIn(['compose', 'spoiler']),
- spoiler_text: state.getIn(['compose', 'spoiler_text']),
- privacy: state.getIn(['compose', 'privacy']),
- focusDate: state.getIn(['compose', 'focusDate']),
- preselectDate: state.getIn(['compose', 'preselectDate']),
- is_submitting: state.getIn(['compose', 'is_submitting']),
- is_uploading: state.getIn(['compose', 'is_uploading']),
- me: state.getIn(['compose', 'me']),
- needsPrivacyWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
- mentionedDomains: mentionedUsernamesWithDomains
- };
-};
-
const mapDispatchToProps = (dispatch) => ({
onChange (text) {
diff --git a/app/assets/javascripts/components/features/compose/containers/warning_container.jsx b/app/assets/javascripts/components/features/compose/containers/warning_container.jsx
new file mode 100644
index 0000000000..62a9bb5710
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/warning_container.jsx
@@ -0,0 +1,48 @@
+import { connect } from 'react-redux';
+import Warning from '../components/warning';
+import { createSelector } from 'reselect';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+
+const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
+
+const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
+ return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
+});
+
+const mapStateToProps = state => {
+ const mentionedUsernames = getMentionedUsernames(state);
+ const mentionedUsernamesWithDomains = getMentionedDomains(state);
+
+ return {
+ needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
+ mentionedDomains: mentionedUsernamesWithDomains,
+ needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked'])
+ };
+};
+
+const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => {
+ if (needsLockWarning) {
+ return }} />} />;
+ } else if (needsLeakWarning) {
+ return (
+ {mentionedDomains.join(', ')}, domainsCount: mentionedDomains.length }}
+ />}
+ />
+ );
+ }
+
+ return null;
+};
+
+WarningWrapper.propTypes = {
+ needsLeakWarning: PropTypes.bool,
+ needsLockWarning: PropTypes.bool,
+ mentionedDomains: PropTypes.array.isRequired,
+};
+
+export default connect(mapStateToProps)(WarningWrapper);
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index 180caeaf17..ae14843c19 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -99,7 +99,7 @@ const en = {
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",
"privacy.private.long": "Post to followers only",
- "privacy.private.short": "Private",
+ "privacy.private.short": "Followers-only",
"privacy.public.long": "Post to public timelines",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss
index 11d155d54b..99af9c9820 100644
--- a/app/assets/stylesheets/accounts.scss
+++ b/app/assets/stylesheets/accounts.scss
@@ -173,7 +173,7 @@
text-align: center;
overflow: hidden;
- a, .current, .page, .gap {
+ a, .current, .next, .prev, .page, .gap {
font-size: 14px;
color: $color5;
font-weight: 500;
@@ -187,6 +187,7 @@
border-radius: 100px;
color: $color1;
cursor: default;
+ margin: 0 10px;
}
.gap {
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 1c798f2f2d..800c97a6bb 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -1,6 +1,6 @@
@import 'variables';
-.app-body{
+.app-body {
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
@@ -203,18 +203,29 @@
}
.compose-form__warning {
- color: $color2;
+ color: darken($color3, 33%);
margin-bottom: 15px;
- border: 1px solid $color3;
+ background: $color3;
+ box-shadow: 0 2px 6px rgba($color8, 0.3);
padding: 8px 10px;
border-radius: 4px;
- font-size: 12px;
+ font-size: 13px;
font-weight: 400;
strong {
- color: $color5;
+ color: darken($color3, 33%);
font-weight: 500;
}
+
+ a {
+ color: darken($color3, 33%);
+ font-weight: 500;
+ text-decoration: underline;
+
+ &:hover, &:active, &:focus {
+ text-decoration: none;
+ }
+ }
}
.compose-form__modifiers {
@@ -1619,7 +1630,7 @@ a.status__content__spoiler-link {
}
.character-counter {
- cursor: default;
+ cursor: default;
font-size: 16px;
}
@@ -1667,7 +1678,7 @@ a.status__content__spoiler-link {
font-size: 16px;
}
}
-
+
@import 'boost';
button.icon-button i.fa-retweet {
@@ -1766,6 +1777,7 @@ button.icon-button.active i.fa-retweet {
cursor: pointer;
position: relative;
z-index: 2;
+ outline: 0;
&.active {
box-shadow: 0 1px 0 rgba($color4, 0.3);
@@ -1781,6 +1793,10 @@ button.icon-button.active i.fa-retweet {
display: none;
}
}
+
+ &:focus, &:active {
+ outline: 0;
+ }
}
.column-header__icon {
diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index c6a8b5b024..890a005105 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -269,3 +269,60 @@ code {
font-size: 14px;
}
}
+
+.table-form {
+ p {
+ max-width: 400px;
+ margin-bottom: 15px;
+
+ strong {
+ font-weight: 500;
+ }
+ }
+
+ .warning {
+ max-width: 400px;
+ box-sizing: border-box;
+ background: rgba($color6, 0.5);
+ color: $color5;
+ text-shadow: 1px 1px 0 rgba($color8, 0.3);
+ box-shadow: 0 2px 6px rgba($color8, 0.4);
+ border-radius: 4px;
+ padding: 10px;
+ margin-bottom: 15px;
+
+ a {
+ color: $color5;
+ text-decoration: underline;
+
+ &:hover, &:focus, &:active {
+ text-decoration: none;
+ }
+ }
+
+ strong {
+ font-weight: 600;
+ display: block;
+ margin-bottom: 5px;
+
+ .fa {
+ font-weight: 400;
+ }
+ }
+ }
+}
+
+.action-pagination {
+ display: flex;
+ align-items: center;
+
+ .actions, .pagination {
+ flex: 1 1 auto;
+ }
+
+ .actions {
+ padding: 30px 0;
+ padding-right: 20px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb
new file mode 100644
index 0000000000..13722345fd
--- /dev/null
+++ b/app/controllers/settings/follower_domains_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class Settings::FollowerDomainsController < ApplicationController
+ layout 'admin'
+
+ before_action :authenticate_user!
+
+ def show
+ @account = current_account
+ @domains = current_account.followers.reorder(nil).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
+ end
+
+ def update
+ domains = bulk_params[:select] || []
+
+ domains.each do |domain|
+ SoftBlockDomainFollowersWorker.perform_async(current_account.id, domain)
+ end
+
+ redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size)
+ end
+
+ private
+
+ def bulk_params
+ params.permit(select: [])
+ end
+end
diff --git a/app/models/account.rb b/app/models/account.rb
index b497a90a34..084b17f43d 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -135,6 +135,10 @@ class Account < ApplicationRecord
!subscription_expires_at.blank?
end
+ def followers_domains
+ followers.reorder(nil).pluck('distinct accounts.domain')
+ end
+
def favourited?(status)
status.proper.favourites.where(account: self).count.positive?
end
diff --git a/app/views/settings/follower_domains/show.html.haml b/app/views/settings/follower_domains/show.html.haml
new file mode 100644
index 0000000000..dad2770f10
--- /dev/null
+++ b/app/views/settings/follower_domains/show.html.haml
@@ -0,0 +1,33 @@
+- content_for :page_title do
+ = t('settings.followers')
+
+= form_tag settings_follower_domains_path, method: :patch, class: 'table-form' do
+ - unless @account.locked?
+ .warning
+ %strong
+ = fa_icon('warning')
+ = t('followers.unlocked_warning_title')
+ = t('followers.unlocked_warning_html', lock_link: link_to(t('followers.lock_link'), settings_profile_url))
+
+ %p= t('followers.explanation_html')
+ %p= t('followers.true_privacy_html')
+
+ %table.table
+ %thead
+ %tr
+ %th
+ %th= t('followers.domain')
+ %th= t('followers.followers_count')
+ %tbody
+ - @domains.each do |domain|
+ %tr
+ %td
+ = check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
+ %td
+ %samp= domain.domain.presence || Rails.configuration.x.local_domain
+ %td= number_with_delimiter domain.accounts_from_domain
+
+ .action-pagination
+ .actions
+ = button_tag t('followers.purge'), type: :submit, class: 'button', disabled: !@account.locked?
+ = paginate @domains
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index d009e51ec5..8a4113ab48 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -7,7 +7,7 @@
.fields-group
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
- = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| I18n.t("statuses.visibilities.#{visibility}") }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+ = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-group
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb
index bb21468e7b..e93fa33cf9 100644
--- a/app/workers/import_worker.rb
+++ b/app/workers/import_worker.rb
@@ -4,6 +4,7 @@ require 'csv'
class ImportWorker
include Sidekiq::Worker
+
sidekiq_options queue: 'pull', retry: false
attr_reader :import
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
index 68ca0f870c..c0e03990ab 100644
--- a/app/workers/pubsubhubbub/distribution_worker.rb
+++ b/app/workers/pubsubhubbub/distribution_worker.rb
@@ -8,12 +8,14 @@ class Pubsubhubbub::DistributionWorker
def perform(stream_entry_id)
stream_entry = StreamEntry.find(stream_entry_id)
- return if stream_entry.hidden?
+ return if stream_entry.status&.direct_visibility?
account = stream_entry.account
payload = AtomSerializer.render(AtomSerializer.new.feed(account, [stream_entry]))
+ domains = account.followers_domains
Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
+ next unless domains.include?(Addressable::URI.parse(subscription.callback_url).host)
Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
end
rescue ActiveRecord::RecordNotFound
diff --git a/app/workers/soft_block_domain_followers_worker.rb b/app/workers/soft_block_domain_followers_worker.rb
new file mode 100644
index 0000000000..2782d05d27
--- /dev/null
+++ b/app/workers/soft_block_domain_followers_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class SoftBlockDomainFollowersWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'pull'
+
+ def perform(account_id, domain)
+ Account.find(account_id).followers.where(domain: domain).pluck(:id).each do |follower_id|
+ SoftBlockWorker.perform_async(account_id, follower_id)
+ end
+ end
+end
diff --git a/app/workers/soft_block_worker.rb b/app/workers/soft_block_worker.rb
new file mode 100644
index 0000000000..312d880b97
--- /dev/null
+++ b/app/workers/soft_block_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class SoftBlockWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'pull'
+
+ def perform(account_id, target_account_id)
+ account = Account.find(account_id)
+ target_account = Account.find(target_account_id)
+
+ BlockService.new.call(account, target_account)
+ UnblockService.new.call(account, target_account)
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index cbe2b4cbd8..dda2acc135 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -41,14 +41,14 @@ en:
remote_follow: Remote follow
unfollow: Unfollow
activitypub:
- outbox:
- name: "%{account_name}'s Outbox"
- summary: "A collection of activities from user %{account_name}."
activity:
- create:
- name: "%{account_name} created a note."
announce:
name: "%{account_name} announced an activity."
+ create:
+ name: "%{account_name} created a note."
+ outbox:
+ name: "%{account_name}'s Outbox"
+ summary: A collection of activities from user %{account_name}.
admin:
accounts:
are_you_sure: Are you sure?
@@ -227,6 +227,18 @@ en:
follows: You follow
mutes: You mute
storage: Media storage
+ followers:
+ domain: Domain
+ explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. Your private statuses are delivered to all instances where you have followers. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those instances.
+ followers_count: Number of followers
+ lock_link: Lock your account
+ purge: Remove from followers
+ success:
+ one: In the process of soft-blocking followers from one domain...
+ other: In the process of soft-blocking followers from %{count} domains...
+ true_privacy_html: Please mind that true privacy can only be achieved with end-to-end encryption.
+ unlocked_warning_html: Anyone can follow you to immediately view your private statuses. %{lock_link} to be able to review and reject followers.
+ unlocked_warning_title: Your account is not locked
generic:
changes_saved_msg: Changes successfully saved!
powered_by: powered by %{link}
@@ -286,6 +298,7 @@ en:
back: Back to Mastodon
edit_profile: Edit profile
export: Data export
+ followers: Authorized followers
import: Import
preferences: Preferences
settings: Settings
@@ -295,9 +308,12 @@ en:
over_character_limit: character limit of %{max} exceeded
show_more: Show more
visibilities:
- private: Only show to followers
+ private: Followers-only
+ private_long: Only show to followers
public: Public
- unlisted: Public, but do not display on the public timeline
+ public_long: Everyone can see
+ unlisted: Unlisted
+ unlisted_long: Everyone can see, but not listed on public timelines
stream_entries:
click_to_show: Click to show
reblogged: boosted
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 492849f5ed..acf9bd9dcd 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -39,6 +39,48 @@ nl:
posts: Berichten
remote_follow: Extern volgen
unfollow: Ontvolgen
+ admin:
+ settings:
+ click_to_edit: Klik om te bewerken
+ contact_information:
+ email: Vul een openbaar gebruikt e-mailadres in
+ label: Contactgegevens
+ username: Vul een gebruikersnaam in
+ registrations:
+ closed_message:
+ desc_html: Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld
En ook hier kan je HTML gebruiken
+ title: Bericht wanneer registratie is uitgeschakeld
+ open:
+ disabled: Uitgeschakeld
+ enabled: Ingeschakeld
+ title: Open registratie
+ setting: Instelling
+ site_description:
+ desc_html: Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.
Je kan HTML gebruiken, zoals <a>
en <em>
.
+ title: Omschrijving Mastodon-server
+ site_description_extended:
+ desc_html: Wordt op de uitgebreide informatiepagina weergegeven
Je kan ook hier HTML gebruiken
+ title: Uitgebreide omschrijving Mastodon-server
+ site_title: Naam Mastodon-server
+ title: Server-instellingen
+ admin.reports:
+ comment:
+ label: Opmerking
+ none: Geen
+ delete: Verwijderen
+ id: ID
+ mark_as_resolved: Markeer als opgelost
+ report: 'Gerapporteerde toot #%{id}'
+ reported_account: Gerapporteerde account
+ reported_by: Gerapporteerd door
+ resolved: Opgelost
+ silence_account: Account stilzwijgen
+ status: Toot
+ suspend_account: Account blokkeren
+ target: Target
+ title: Gerapporteerde toots
+ unresolved: Onopgelost
+ view: Weergeven
application_mailer:
settings: 'E-mailvoorkeuren wijzigen: %{link}'
signature: Mastodon-meldingen van %{instance}
@@ -74,6 +116,12 @@ nl:
x_minutes: "%{count}m"
x_months: "%{count}ma"
x_seconds: "%{count}s"
+ errors:
+ '404': De pagina waarnaar jij op zoek bent bestaat niet.
+ '410': De pagina waarnaar jij op zoek bent bestaat niet meer.
+ '422':
+ content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies?
+ title: Veiligheidsverificatie mislukt
exports:
blocks: Jij blokkeert
csv: CSV
@@ -161,52 +209,3 @@ nl:
users:
invalid_email: E-mailadres is ongeldig
invalid_otp_token: Ongeldige tweestaps-aanmeldcode
- errors:
- 404: De pagina waarnaar jij op zoek bent bestaat niet.
- 410: De pagina waarnaar jij op zoek bent bestaat niet meer.
- 422:
- title: Veiligheidsverificatie mislukt
- content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies?
- admin.reports:
- title: Gerapporteerde toots
- status: Toot
- unresolved: Onopgelost
- resolved: Opgelost
- id: ID
- target: Target
- reported_by: Gerapporteerd door
- comment:
- label: Opmerking
- none: Geen
- view: Weergeven
- report: 'Gerapporteerde toot #%{id}'
- delete: Verwijderen
- reported_account: Gerapporteerde account
- reported_by: Gerapporteerd door
- silence_account: Account stilzwijgen
- suspend_account: Account blokkeren
- mark_as_resolved: Markeer als opgelost
- admin:
- settings:
- title: Server-instellingen
- setting: Instelling
- click_to_edit: Klik om te bewerken
- contact_information:
- label: Contactgegevens
- username: Vul een gebruikersnaam in
- email: Vul een openbaar gebruikt e-mailadres in
- site_title: Naam Mastodon-server
- site_description:
- title: Omschrijving Mastodon-server
- desc_html: "Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.
Je kan HTML gebruiken, zoals <a>
en <em>
."
- site_description_extended:
- title: Uitgebreide omschrijving Mastodon-server
- desc_html: "Wordt op de uitgebreide informatiepagina weergegeven
Je kan ook hier HTML gebruiken"
- registrations:
- open:
- title: Open registratie
- enabled: Ingeschakeld
- disabled: Uitgeschakeld
- closed_message:
- title: Bericht wanneer registratie is uitgeschakeld
- desc_html: "Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld
En ook hier kan je HTML gebruiken"
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 551e92271b..e8ad1279b4 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -22,8 +22,8 @@ pt-BR:
features_headline: O que torna Mastodon diferente
get_started: Comece aqui
links: Links
- source_code: Source code
other_instances: Outras instâncias
+ source_code: Source code
terms: Termos
user_count_after: usuários
user_count_before: Lugar de
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 790d564521..4aa3818fd4 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -23,7 +23,7 @@ en:
email: E-mail address
header: Header
locale: Language
- locked: Make account private
+ locked: Lock account
new_password: New password
note: Bio
otp_attempt: Two-factor code
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 7b3ba74444..9b3608f244 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -30,8 +30,8 @@ zh-CN:
user_count_before: 这里共注册有
accounts:
follow: 关注
- followers: 粉丝 # "Fans"
- following: 关注 # "Follow"
+ followers: 粉丝
+ following: 关注
nothing_here: 神马都没有!
people_followed_by: 正关注
people_who_follow: 粉丝
@@ -80,15 +80,14 @@ zh-CN:
web: 用户页面
domain_blocks:
add_new: 添加
- domain: 域名阻隔
created_msg: 正处理域名阻隔
destroyed_msg: 已撤销域名阻隔
+ domain: 域名阻隔
new:
create: 添加域名阻隔
- hint: 「域名阻隔」不会隔绝该域名用户的嘟账户入本站数据库,但会嘟文抵达后,自动套用特定的审批操作。
+ hint: "「域名阻隔」不会隔绝该域名用户的嘟账户入本站数据库,但会嘟文抵达后,自动套用特定的审批操作。"
severity:
- desc_html: 「自动静音」令该域名用户的嘟文,设为只对关注者显示,没有关注的人会看不到。
- 「自动除名」会自动将该域名用户的嘟文、媒体文件、个人资料自本服务站删除。
+ desc_html: "「自动静音」令该域名用户的嘟文,设为只对关注者显示,没有关注的人会看不到。 「自动除名」会自动将该域名用户的嘟文、媒体文件、个人资料自本服务站删除。"
silence: 自动静音
suspend: 自动除名
title: 添加域名阻隔
@@ -99,10 +98,8 @@ zh-CN:
suspend: 自动除名
severity: 阻隔程度
show:
- # It turns out that Chinese only uses an "other"
- # Well, we don't have these -s magic anyway...
affected_accounts:
- other: "数据库中有%{count}个账户受影响"
+ other: 数据库中有%{count}个账户受影响
retroactive:
silence: 对此域名的所有账户取消静音
suspend: 对此域名的所有账户取消除名
@@ -147,8 +144,7 @@ zh-CN:
username: 输入用户名称
registrations:
closed_message:
- desc_html: 当本站暂停接受注册时,会显示这个消息。
- 可使用 HTML
+ desc_html: 当本站暂停接受注册时,会显示这个消息。
可使用 HTML
title: 暂停注册消息
open:
disabled: 停用
@@ -187,11 +183,10 @@ zh-CN:
title: 关注 %{acct}
datetime:
distance_in_words:
- # Ditching "about" as in en
about_x_hours: "%{count} 小时"
about_x_months: "%{count} 个月"
about_x_years: "%{count} 年"
- almost_x_years: "接近 %{count} 年"
+ almost_x_years: 接近 %{count} 年
half_a_minute: 刚刚
less_than_x_minutes: "%{count} 分不到"
less_than_x_seconds: 刚刚
@@ -232,7 +227,6 @@ zh-CN:
body: 自从你在%{since}使用%{instance}以后,错过了这些嘟嘟滴滴:
mention: "%{name} 在此提及了你︰"
new_followers_summary:
- # censorship note: Better not mention "don't move your chicken", even if it's a phonetic joke
one: 有人关注你了!耶!
other: 有 %{count} 个人关注了你!别激动!
subject:
@@ -271,7 +265,6 @@ zh-CN:
settings: 设置
two_factor_authentication: 两步认证
statuses:
- # Hey, this is already in a web browser!
open_in_web: 打开网页
over_character_limit: 超过了 %{max} 字的限制
show_more: 显示更多
diff --git a/config/navigation.rb b/config/navigation.rb
index bdc0a7b6c5..16bc86696d 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -12,6 +12,7 @@ SimpleNavigation::Configuration.run do |navigation|
settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
+ settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url
end
primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.admin? } do |admin|
diff --git a/config/routes.rb b/config/routes.rb
index 6893aa06b6..34c4fca4c7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -63,6 +63,8 @@ Rails.application.routes.draw do
resources :recovery_codes, only: [:create]
resource :confirmation, only: [:new, :create]
end
+
+ resource :follower_domains, only: [:show, :update]
end
resources :media, only: [:show]
@@ -109,9 +111,7 @@ Rails.application.routes.draw do
# ActivityPub
namespace :activitypub do
get '/users/:id/outbox', to: 'outbox#show', as: :outbox
-
get '/statuses/:id', to: 'activities#show_status', as: :status
-
resources :notes, only: [:show]
end
diff --git a/spec/controllers/settings/follower_domains_controller_spec.rb b/spec/controllers/settings/follower_domains_controller_spec.rb
new file mode 100644
index 0000000000..1afdb97576
--- /dev/null
+++ b/spec/controllers/settings/follower_domains_controller_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+describe Settings::FollowerDomainsController do
+ let(:user) { Fabricate(:user) }
+
+ before do
+ sign_in user, scope: :user
+ end
+
+ describe 'GET #show' do
+ it 'returns http success' do
+ get :show
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ describe 'PATCH #update' do
+ let(:poopfeast) { Fabricate(:account, username: 'poopfeast', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
+
+ before do
+ stub_request(:post, 'http://example.com/salmon').to_return(status: 200)
+ poopfeast.follow!(user.account)
+ patch :update, params: { select: ['example.com'] }
+ end
+
+ it 'redirects back to followers page' do
+ expect(response).to redirect_to(settings_follower_domains_path)
+ end
+
+ it 'soft-blocks followers from selected domains' do
+ expect(poopfeast.following?(user.account)).to be false
+ end
+ end
+end
diff --git a/spec/controllers/settings/preferences_controller_spec.rb b/spec/controllers/settings/preferences_controller_spec.rb
index cdf595d4d9..0d3dc059ad 100644
--- a/spec/controllers/settings/preferences_controller_spec.rb
+++ b/spec/controllers/settings/preferences_controller_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe Settings::PreferencesController do
let(:user) { Fabricate(:user) }
+
before do
sign_in user, scope: :user
end
@@ -9,13 +10,12 @@ describe Settings::PreferencesController do
describe 'GET #show' do
it 'returns http success' do
get :show
-
expect(response).to have_http_status(:success)
end
end
describe 'PUT #update' do
- it 'udpates the user record' do
+ it 'updates the user record' do
put :update, params: { user: { locale: 'en' } }
expect(response).to redirect_to(settings_preferences_path)
@@ -31,7 +31,7 @@ describe Settings::PreferencesController do
user: {
setting_boost_modal: '1',
notification_emails: { follow: '1' },
- interactions: { must_be_follower: '0' }
+ interactions: { must_be_follower: '0' },
}
}
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 60d45ddc02..4ddc6d032d 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -12,7 +12,7 @@ require 'capybara/rspec'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
ActiveRecord::Migration.maintain_test_schema!
-WebMock.disable_net_connect!(allow: 'localhost:7575')
+WebMock.disable_net_connect!
Sidekiq::Testing.inline!
RSpec.configure do |config|