Merge branch 'master' into glitch-soc/merge-upstream

Conflicts manually resolved:
- app/services/post_status_service.rb
- config/locales/simple_form.pl.yml
- config/routes.rb
- config/webpack/loaders/sass.js
- config/webpack/shared.js
- package.json
- yarn.lock
th-downstream
Thibaut Girka 6 years ago
commit 44f2224606

@ -3,7 +3,7 @@ version: 2
aliases:
- &defaults
docker:
- image: circleci/ruby:2.5.1-stretch-node
- image: circleci/ruby:2.6.0-stretch-node
environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/
DB_HOST: localhost
@ -98,21 +98,21 @@ jobs:
<<: *defaults
<<: *install_steps
install-ruby2.5:
install-ruby2.6:
<<: *defaults
<<: *install_ruby_dependencies
install-ruby2.4:
install-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.4.4-stretch-node
- image: circleci/ruby:2.5.3-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies
install-ruby2.3:
install-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.3.7-stretch-node
- image: circleci/ruby:2.4.5-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies
@ -131,43 +131,43 @@ jobs:
- ./mastodon/public/assets
- ./mastodon/public/packs-test/
test-ruby2.5:
test-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.5.1-stretch-node
- image: circleci/ruby:2.6.0-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.3-alpine
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine
- image: circleci/redis:5.0.3-alpine3.8
<<: *test_steps
test-ruby2.4:
test-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.4.4-stretch-node
- image: circleci/ruby:2.5.3-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.3-alpine
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine
- image: circleci/redis:4.0.12-alpine
<<: *test_steps
test-ruby2.3:
test-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.3.7-stretch-node
- image: circleci/ruby:2.4.5-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.3-alpine
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine
- image: circleci/redis:4.0.12-alpine
<<: *test_steps
test-webui:
<<: *defaults
docker:
- image: circleci/node:8.11.1-stretch
- image: circleci/node:8.15.0-stretch
steps:
- *attach_workspace
- run: ./bin/retry yarn test:jest
@ -186,20 +186,24 @@ workflows:
build-and-test:
jobs:
- install
- install-ruby2.5:
- install-ruby2.6:
requires:
- install
- install-ruby2.4:
- install-ruby2.5
- install-ruby2.5:
requires:
- install
- install-ruby2.5
- install-ruby2.3:
- install-ruby2.4:
requires:
- install
- install-ruby2.5
- build:
requires:
- install-ruby2.5
- test-ruby2.6:
requires:
- install-ruby2.6
- build
- test-ruby2.5:
requires:
- install-ruby2.5
@ -208,10 +212,6 @@ workflows:
requires:
- install-ruby2.4
- build
- test-ruby2.3:
requires:
- install-ruby2.3
- build
- test-webui:
requires:
- install

@ -27,7 +27,7 @@ plugins:
enabled: true
eslint:
enabled: true
channel: eslint-4
channel: eslint-5
rubocop:
enabled: true
channel: rubocop-0-54

@ -26,6 +26,8 @@ parserOptions:
ecmaVersion: 2018
settings:
react:
version: detect
import/extensions:
- .js
import/ignore:

@ -1,9 +0,0 @@
plugins:
postcss-smart-import: {}
precss: {}
autoprefixer:
browsers:
- last 2 versions
- IE >= 11
- iOS >= 9
postcss-object-fit-images: {}

@ -1 +1 @@
2.5.3
2.6.0

@ -31,6 +31,8 @@ RUN apk -U upgrade \
libidn-dev \
libressl \
libtool \
libxml2-dev \
libxslt-dev \
postgresql-dev \
protobuf-dev \
python \
@ -43,6 +45,8 @@ RUN apk -U upgrade \
imagemagick \
libidn \
libpq \
libxml2 \
libxslt \
protobuf \
tini \
tzdata \
@ -67,7 +71,7 @@ COPY stack-fix.c /lib
RUN gcc -shared -fPIC /lib/stack-fix.c -o /lib/stack-fix.so
RUN rm /lib/stack-fix.c
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
RUN bundle config build.nokogiri --use-system-libraries --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
&& yarn install --pure-lockfile --ignore-engines \
&& yarn cache clean

@ -1,7 +1,7 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 2.3.0', '< 2.6.0'
ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.3'
@ -29,7 +29,7 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0'
gem 'cld3', '~> 3.2.3'
gem 'devise', '~> 4.5'
gem 'devise-two-factor', '~> 3.0'
@ -115,7 +115,7 @@ group :test do
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.4'
gem 'webmock', '~> 3.5'
gem 'parallel_tests', '~> 2.27'
end

@ -142,7 +142,7 @@ GEM
elasticsearch (>= 2.0.0)
elasticsearch-dsl
chunky_png (1.3.10)
cld3 (3.2.2)
cld3 (3.2.3)
ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0)
cocaine (0.5.8)
@ -631,7 +631,7 @@ GEM
uniform_notifier (1.12.1)
warden (1.2.7)
rack (>= 1.0)
webmock (3.4.2)
webmock (3.5.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
@ -672,7 +672,7 @@ DEPENDENCIES
capybara (~> 3.12)
charlock_holmes (~> 0.7.6)
chewy (~> 5.0)
cld3 (~> 3.2.0)
cld3 (~> 3.2.3)
climate_control (~> 0.2)
derailed_benchmarks
devise (~> 4.5)
@ -766,7 +766,7 @@ DEPENDENCIES
tty-prompt (~> 0.18)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2018)
webmock (~> 3.4)
webmock (~> 3.5)
webpacker (~> 3.5)
webpush

2
Vagrantfile vendored

@ -44,7 +44,7 @@ sudo apt-get install \
# Install rvm
read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm

@ -17,7 +17,7 @@ module Admin
account_action.save!
if account_action.with_report?
redirect_to admin_report_path(account_action.report)
redirect_to admin_reports_path
else
redirect_to admin_account_path(@account.id)
end

@ -62,9 +62,8 @@ module Admin
def redownload
authorize @account, :redownload?
@account.reset_avatar!
@account.reset_header!
@account.save!
@account.update!(last_webfingered_at: nil)
ResolveAccountService.new.call(@account)
redirect_to admin_account_path(@account.id)
end

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Admin
class FollowersController < BaseController
before_action :set_account
PER_PAGE = 40
def index
authorize :account, :index?
@followers = followers.recent.page(params[:page]).per(PER_PAGE)
end
def set_account
@account = Account.find(params[:account_id])
end
def followers
Follow.includes(:account).where(target_account: @account)
end
end
end

@ -68,12 +68,14 @@ class Api::BaseController < ApplicationController
end
def require_user!
if current_user && !current_user.disabled?
set_user_activity
elsif current_user
if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed?
render json: { error: 'Email confirmation is not completed' }, status: 403
else
render json: { error: 'This method requires an authenticated user' }, status: 422
set_user_activity
end
end

@ -28,13 +28,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses
end
@ -65,6 +63,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
Status.without_replies
end
def no_reblogs_scope
Status.without_reblogs
end
def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end

@ -1,14 +1,16 @@
# frozen_string_literal: true
class Api::V1::AccountsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
before_action :require_user!, except: [:show]
before_action :set_account
before_action :require_user!, except: [:show, :create]
before_action :set_account, except: [:create]
before_action :check_account_suspension, only: [:show]
before_action :check_enabled_registrations, only: [:create]
respond_to :json
@ -16,6 +18,16 @@ class Api::V1::AccountsController < Api::BaseController
render json: @account, serializer: REST::AccountSerializer
end
def create
token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers)
self.response_body = Oj.dump(response.body)
self.status = response.status
end
def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
@ -62,4 +74,12 @@ class Api::V1::AccountsController < Api::BaseController
def check_account_suspension
gone if @account.suspended?
end
def account_params
params.permit(:username, :email, :password, :agreement)
end
def check_enabled_registrations
forbidden if single_user_mode? || !Setting.open_registrations
end
end

@ -7,9 +7,9 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
before_action :set_user, only: [:finish_signup]
before_action :set_pack
# GET/PATCH /users/:id/finish_signup
def finish_signup
return unless request.patch? && params[:user]
if @user.update(user_params)
@user.skip_reconfirmation!
bypass_sign_in(@user)
@ -36,4 +36,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
def user_params
params.require(:user).permit(:email)
end
def after_confirmation_path_for(_resource_name, user)
if user.created_by_application && truthy_param?(:redirect_to_app)
user.created_by_application.redirect_uri
else
super
end
end
end

@ -27,6 +27,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.agreement = true
resource.build_account if resource.account.nil?
end

@ -47,6 +47,7 @@ module SignatureVerification
.with_fallback { nil }
.with_threshold(1)
.with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
account = account_stoplight.run

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Settings
module Exports
class BlockedDomainsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data
@export.to_blocked_domains_csv
end
end
end
end

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Settings
module Exports
class ListsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data
@export.to_lists_csv
end
end
end
end

@ -30,6 +30,7 @@ module SettingsHelper
ja: '日本語',
ka: 'ქართული',
ko: '한국어',
ml: 'മലയാളം',
nl: 'Nederlands',
no: 'Norsk',
oc: 'Occitan',

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 27 KiB

@ -130,6 +130,12 @@ export function submitCompose(routerHistory) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/timelines/direct');
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
routerHistory.goBack();
}
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
@ -142,12 +148,6 @@ export function submitCompose(routerHistory) {
}
};
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/timelines/direct');
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
routerHistory.goBack();
}
if (response.data.visibility !== 'direct') {
insertIfOnline('home');
}

@ -1,6 +1,6 @@
import axios from 'axios';
import LinkHeader from 'http-link-header';
import ready from './ready';
import LinkHeader from './link_header';
export const getLinks = response => {
const value = response.headers.link;

@ -148,6 +148,7 @@ class StatusActionBar extends ImmutablePureComponent {
let menu = [];
let reblogIcon = 'retweet';
let replyIcon;
let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
@ -190,8 +191,10 @@ class StatusActionBar extends ImmutablePureComponent {
}
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
}
@ -201,7 +204,7 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon='reply' onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton}

@ -10,8 +10,7 @@ const messages = defineMessages({
});
const makeMapStateToProps = () => {
const mapStateToProps = (state, { }) => ({
});
const mapStateToProps = () => ({});
return mapStateToProps;
};

File diff suppressed because one or more lines are too long

@ -7,7 +7,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, invitesEnabled, version } from '../../initial_state';
import { me, invitesEnabled, version, profile_directory } from '../../initial_state';
import { fetchFollowRequests } from '../../actions/accounts';
import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom';
@ -136,7 +136,7 @@ class GettingStarted extends ImmutablePureComponent {
<div className='getting-started__footer'>
<ul>
<li><a href='/explore' target='_blank'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></a> · </li>
{profile_directory && <li><a href='/explore' target='_blank'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></a> · </li>}
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>

@ -98,8 +98,8 @@ FrameInteractions.propTypes = {
onNext: PropTypes.func.isRequired,
};
@connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
export default class Introduction extends React.PureComponent {
export default @connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
class Introduction extends React.PureComponent {
static propTypes = {
domain: PropTypes.string.isRequired,

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContainer from '../../../containers/status_container';
import AccountContainer from '../../../containers/account_container';
import RelativeTimestamp from '../../../components/relative_timestamp';
import { injectIntl, FormattedMessage } from 'react-intl';
import Permalink from '../../../components/permalink';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -86,13 +85,12 @@ class Notification extends ImmutablePureComponent {
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-user-plus' />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
<span className='notification__relative_time'>
<RelativeTimestamp timestamp={notification.get('created_at')} />
</span>
</span>
</div>
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
</div>
</HotKeys>
@ -122,9 +120,9 @@ class Notification extends ImmutablePureComponent {
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-star star-icon' />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
<span className='notification__relative_time'>
<RelativeTimestamp className='notification__relative_time' timestamp={notification.get('created_at')} />
</span>
</div>
@ -144,9 +142,9 @@ class Notification extends ImmutablePureComponent {
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-retweet' />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
<span className='notification__relative_time'>
<RelativeTimestamp className='notification__relative_time' timestamp={notification.get('created_at')} />
</span>
</div>

@ -151,6 +151,13 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
);
let replyIcon;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
} else {
replyIcon = 'reply-all';
}
let reblogIcon = 'retweet';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
@ -159,7 +166,7 @@ class ActionBar extends React.PureComponent {
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton}

@ -6,4 +6,4 @@ const mapStateToProps = state => ({
isModalOpen: !!state.get('modal').modalType,
});
export default connect(mapStateToProps, null, null, { withRef: true })(ColumnsArea);
export default connect(mapStateToProps, null, null, { forwardRef: true })(ColumnsArea);

@ -1,8 +1,8 @@
import { connect } from 'react-redux';
import LoadingBar from 'react-redux-loading-bar';
const mapStateToProps = (state) => ({
loading: state.get('loadingBar'),
const mapStateToProps = (state, ownProps) => ({
loading: state.get('loadingBar')[ownProps.scope || 'default'],
});
export default connect(mapStateToProps)(LoadingBar.WrappedComponent);

@ -134,7 +134,7 @@ class SwitchingColumnsArea extends React.PureComponent {
});
setRef = c => {
this.node = c.getWrappedInstance().getWrappedInstance();
this.node = c.getWrappedInstance();
}
render () {

@ -16,5 +16,6 @@ export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export const invitesEnabled = getMeta('invites_enabled');
export const version = getMeta('version');
export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory');
export default initialState;

@ -1,33 +0,0 @@
import Link from 'http-link-header';
import querystring from 'querystring';
Link.parseAttrs = (link, parts) => {
let match = null;
let attr = '';
let value = '';
let attrs = '';
let uriAttrs = /<(.*)>;\s*(.*)/gi.exec(parts);
if(uriAttrs) {
attrs = uriAttrs[2];
link = Link.parseParams(link, uriAttrs[1]);
}
while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign
attr = match[1].toLowerCase();
value = match[4] || match[3] || match[2];
if( /\*$/.test(attr)) {
Link.setAttr(link, attr, Link.parseExtendedValue(value));
} else if(/%/.test(value)) {
Link.setAttr(link, attr, querystring.decode(value));
} else {
Link.setAttr(link, attr, value);
}
}
return link;
};
export default Link;

@ -149,22 +149,22 @@
"home.column_settings.basic": "أساسية",
"home.column_settings.show_reblogs": "عرض الترقيات",
"home.column_settings.show_replies": "عرض الردود",
"introduction.federation.action": "Next",
"introduction.federation.action": "التالي",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "كافة المنشورات التي نُشِرت إلى العامة على الخوادم الأخرى للفديفرس سوف يتم عرضها على الخيط المُوحَّد.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "سوف تُعرَض منشورات الأشخاص الذين تُتابِعهم على الخيط الرئيسي. بإمكانك متابعة أي حساب أيا كان الخادم الذي هو عليه!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.favourite.headline": "الإضافة إلى المفضلة",
"introduction.interactions.favourite.text": "يمكِنك إضافة أي تبويق إلى المفضلة و إعلام صاحبه أنك أعجِبت بذاك التبويق.",
"introduction.interactions.reblog.headline": "الترقية",
"introduction.interactions.reblog.text": "يمكنكم مشاركة تبويقات الأشخاص الآخرين مع متابِعيكم عن طريق ترقيتها.",
"introduction.interactions.reply.headline": "الرد",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.action": "هيا بنا!",
"introduction.welcome.headline": "الخطوات الأولى",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"keyboard_shortcuts.back": "للعودة",
"keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟",
"notifications.column_settings.alert": "إشعارات سطح المكتب",
"notifications.column_settings.favourite": "المُفَضَّلة :",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "عرض كافة الفئات",
"notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة",
"notifications.column_settings.filter_bar.show": "عرض",
"notifications.column_settings.follow": "متابعُون جُدُد :",
"notifications.column_settings.mention": "الإشارات :",
"notifications.column_settings.push": "الإخطارات المدفوعة",
"notifications.column_settings.reblog": "الترقيّات:",
"notifications.column_settings.show": "إعرِضها في عمود",
"notifications.column_settings.sound": "أصدر صوتا",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.all": "الكل",
"notifications.filter.boosts": "الترقيات",
"notifications.filter.favourites": "المفضلة",
"notifications.filter.follows": "يتابِع",
"notifications.filter.mentions": "الإشارات",
"notifications.group": "{count} إشعارات",
"privacy.change": "إضبط خصوصية المنشور",
"privacy.direct.long": "أنشر إلى المستخدمين المشار إليهم فقط",

@ -341,7 +341,7 @@
"upload_area.title": "Arrossega i deixa anar per carregar",
"upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_form.description": "Descriure els problemes visuals",
"upload_form.focus": "Retallar",
"upload_form.focus": "Modificar la previsualització",
"upload_form.undo": "Esborra",
"upload_progress.label": "Pujant...",
"video.close": "Tancar el vídeo",

@ -145,27 +145,27 @@
"hashtag.column_settings.tag_mode.all": "Tutti quessi",
"hashtag.column_settings.tag_mode.any": "Unu di quessi",
"hashtag.column_settings.tag_mode.none": "Nisunu di quessi",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"hashtag.column_settings.tag_toggle": "Inchjude tag addiziunali per sta colonna",
"home.column_settings.basic": "Bàsichi",
"home.column_settings.show_reblogs": "Vede e spartere",
"home.column_settings.show_replies": "Vede e risposte",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.federation.action": "Cuntinuà",
"introduction.federation.federated.headline": "Federata",
"introduction.federation.federated.text": "I statuti pubblichi da l'altri servori di u fediverse saranu mustrati nant'à a linea pubblica federata.",
"introduction.federation.home.headline": "Accolta",
"introduction.federation.home.text": "I statuti da a ghjente che vo siguitate saranu affissati nant'à a linea d'accolta. Pudete seguità qualvogliasia nant'à tutti i servori!",
"introduction.federation.local.headline": "Lucale",
"introduction.federation.local.text": "I statuti pubblichi da quelli chì sò nant'a listessu servore chì voi ponu esse visti indè a linea pubblica lucale.",
"introduction.interactions.action": "Finisce u tutoriale!",
"introduction.interactions.favourite.headline": "Favuritu",
"introduction.interactions.favourite.text": "Pudete salvà un statutu per ritruvallu più tardi, è fà sapè à l'autore chì v'hè piaciutu, l'aghustendu à i vostri favuriti.",
"introduction.interactions.reblog.headline": "Sparte",
"introduction.interactions.reblog.text": "Pudete sparte i statuti d'altre persone à i vostri abbunati cù u buttone di spartera.",
"introduction.interactions.reply.headline": "Risponde",
"introduction.interactions.reply.text": "Pudete risponde à d'altre persone o a i vostri propii statuti, cio chì i ligarà indè una cunversazione.",
"introduction.welcome.action": "Andemu!",
"introduction.welcome.headline": "Primi passi",
"introduction.welcome.text": "Benvenutu·a indè u fediverse! In qualchi minuta, puderete diffonde missaghji è parlà à i vostri amichi nant'à una varietà maiò di servori. Mà quess'istanza, {domain}, hè speciale—ghjè induve hè uspitatu u vostru prufile, allora ricurdatevi di u so nome.",
"keyboard_shortcuts.back": "rivultà",
"keyboard_shortcuts.blocked": "per apre una lista d'utilizatori bluccati",
"keyboard_shortcuts.boost": "sparte",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "Site sicuru·a che vulete toglie tutte ste nutificazione?",
"notifications.column_settings.alert": "Nutificazione nant'à l'urdinatore",
"notifications.column_settings.favourite": "Favuriti:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Affissà tutte e categurie",
"notifications.column_settings.filter_bar.category": "Barra di ricerca pronta",
"notifications.column_settings.filter_bar.show": "Mustrà",
"notifications.column_settings.follow": "Abbunati novi:",
"notifications.column_settings.mention": "Minzione:",
"notifications.column_settings.push": "Nutificazione Push",
"notifications.column_settings.reblog": "Spartere:",
"notifications.column_settings.show": "Mustrà indè a colonna",
"notifications.column_settings.sound": "Sunà",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.all": "Tuttu",
"notifications.filter.boosts": "Spartere",
"notifications.filter.favourites": "Favuriti",
"notifications.filter.follows": "Abbunamenti",
"notifications.filter.mentions": "Minzione",
"notifications.group": "{count} nutificazione",
"privacy.change": "Mudificà a cunfidenzialità di u statutu",
"privacy.direct.long": "Mandà solu à quelli chì so mintuvati",

@ -149,23 +149,23 @@
"home.column_settings.basic": "Základní",
"home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Zobrazit odpovědi",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.federation.action": "Další",
"introduction.federation.federated.headline": "Federovaná",
"introduction.federation.federated.text": "Veřejné příspěvky z jiných serverů na fediverse se zobrazí na federované časové ose.",
"introduction.federation.home.headline": "Domů",
"introduction.federation.home.text": "Příspěvky od lidí, které sledujete, se objeví ve vašem domovském proudu. Můžete sledovat kohokoliv na jakémkoliv serveru!",
"introduction.federation.local.headline": "Místní",
"introduction.federation.local.text": "Veřejné příspěvky od lidí ze stejného serveru, jako vy, se zobrazí na místní časové ose.",
"introduction.interactions.action": "Dokončit tutoriál!",
"introduction.interactions.favourite.headline": "Oblíbení",
"introduction.interactions.favourite.text": "Oblíbením si můžete uložit toot na později a dát jeho autorovi vědět, že se vám líbí.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.interactions.reblog.text": "Boostnutím můžete sdílet tooty jiných lidí s vašimi sledovately.",
"introduction.interactions.reply.headline": "Odpověď",
"introduction.interactions.reply.text": "Můžete odpovídat na tooty jiných lidí i vaše vlastní, což je propojí do konverzace.",
"introduction.welcome.action": "Jdeme na to!",
"introduction.welcome.headline": "První kroky",
"introduction.welcome.text": "Vítejte na fediverse! Za malou chvíli budete moci posílat zprávy a povídat si se svými přátely přes širokou škálu serverů. Tento server, {domain}, je však speciální—je na něm váš profil, proto si zapamatujte jeho jméno.",
"keyboard_shortcuts.back": "k návratu zpět",
"keyboard_shortcuts.blocked": "k otevření seznamu blokovaných uživatelů",
"keyboard_shortcuts.boost": "k boostnutí",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení?",
"notifications.column_settings.alert": "Desktopová oznámení",
"notifications.column_settings.favourite": "Oblíbené:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Zobrazit všechny kategorie",
"notifications.column_settings.filter_bar.category": "Panel rychlého filtrování",
"notifications.column_settings.filter_bar.show": "Zobrazit",
"notifications.column_settings.follow": "Noví sledovatelé:",
"notifications.column_settings.mention": "Zmínky:",
"notifications.column_settings.push": "Push oznámení",
"notifications.column_settings.reblog": "Boosty:",
"notifications.column_settings.show": "Zobrazit ve sloupci",
"notifications.column_settings.sound": "Přehrát zvuk",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.all": "Vše",
"notifications.filter.boosts": "Boosty",
"notifications.filter.favourites": "Oblíbení",
"notifications.filter.follows": "Sledování",
"notifications.filter.mentions": "Zmínky",
"notifications.group": "{count} oznámení",
"privacy.change": "Změnit soukromí příspěvku",
"privacy.direct.long": "Odeslat pouze zmíněným uživatelům",

@ -17,7 +17,7 @@
"account.follows_you": "Folgt dir",
"account.hide_reblogs": "Geteilte Beiträge von @{name} verbergen",
"account.link_verified_on": "Besitz dieses Links wurde geprüft am {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.locked_info": "Der Privatsphärenstatus dieses Accounts wurde auf gesperrt gesetzt. Die Person bestimmt manuell wer ihm/ihr folgen darf.",
"account.media": "Medien",
"account.mention": "@{name} erwähnen",
"account.moved_to": "{name} ist umgezogen auf:",
@ -149,23 +149,23 @@
"home.column_settings.basic": "Einfach",
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
"home.column_settings.show_replies": "Antworten anzeigen",
"introduction.federation.action": "Next",
"introduction.federation.action": "Weiter",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "Öffentliche Beiträge von anderen Servern im Fediverse werden in der föderierten Zeitleiste erscheinen.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "Beiträge von Leuten, denen du folgst werden in deiner Startseite erscheinen. Du kannst jedem auf irgendeinen Server folgen!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.federation.local.text": "Öffentliche Beiträge von Leuten auf demselben Server wie du werden in der lokalen Zeitleiste erscheinen.",
"introduction.interactions.action": "Tutorial beenden!",
"introduction.interactions.favourite.headline": "Favorisieren",
"introduction.interactions.favourite.text": "Du kannst einen Beitrag für später speichern und dem Autor wissen lassen, dass du ihn magst, indem du ihn favorisierst.",
"introduction.interactions.reblog.headline": "Teilen",
"introduction.interactions.reblog.text": "Du kannst Beiträge von anderen Leuten an deine Follower teilen (oder auch \"boosten\").",
"introduction.interactions.reply.headline": "Antworten",
"introduction.interactions.reply.text": "Du kannst auf die Beiträge von anderen Leuten antworten und die Beiträge werden dann in eine Konversation zusammengebunden.",
"introduction.welcome.action": "Lasst uns loslegen!",
"introduction.welcome.headline": "Erste Schritte",
"introduction.welcome.text": "Willkommen im Fediverse! In wenigen Momenten wirst du in der Lage sein Nachrichten zu versenden und mit deinen Freunden über Server hinweg in Kontakt zu treten. Aber dieser Server, {domain}, ist sehr speziell — er hostet dein Profil, also merke dir den Namen.",
"keyboard_shortcuts.back": "zurück navigieren",
"keyboard_shortcuts.blocked": "Liste blockierter Profile öffnen",
"keyboard_shortcuts.boost": "boosten",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "Bist du dir sicher, dass du alle Mitteilungen löschen möchtest?",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.favourite": "Favorisierungen:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Zeige alle Kategorien an",
"notifications.column_settings.filter_bar.category": "Schnellfilterleiste",
"notifications.column_settings.filter_bar.show": "Anzeigen",
"notifications.column_settings.follow": "Neue Folgende:",
"notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.push": "Push-Benachrichtigungen",
"notifications.column_settings.reblog": "Geteilte Beiträge:",
"notifications.column_settings.show": "In der Spalte anzeigen",
"notifications.column_settings.sound": "Ton abspielen",
"notifications.filter.all": "All",
"notifications.filter.all": "Alle",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.favourites": "Favoriten",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.mentions": "Erwähnungen",
"notifications.group": "{count} Benachrichtigungen",
"privacy.change": "Sichtbarkeit des Beitrags anpassen",
"privacy.direct.long": "Beitrag nur an erwähnte Profile",

@ -149,23 +149,23 @@
"home.column_settings.basic": "Βασικά",
"home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
"home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
"introduction.federation.action": "Next",
"introduction.federation.action": "Επόμενο",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "Οι δημόσιες αναρτήσεις από άλλους κόμβους του fediverse θα εμφανίζονται στην ομοσπονδιακή ροή.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "Οι αναρτήσεις όσων ακολουθείς θα εμφανίζονται στην αρχική ροή. Μπορείς να ακολουθήσεις όποιον θέλεις σε οποιονδήποτε κόμβο!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.federation.local.text": "Οι δημόσιες αναρτήσεις από άτομα στον ίδιο κόμβο με εσένα θα εμφανίζονται στην τοπική ροή.",
"introduction.interactions.action": "Τέλος μαθήματος!",
"introduction.interactions.favourite.headline": "Αγαπημένο",
"introduction.interactions.favourite.text": "Φύλαξε ένα τουτ για αργότερα και να ειδοποιήσεις τον δημιουργό του ότι σου άρεσε σημειώνοντας το ως αγαπημένο.",
"introduction.interactions.reblog.headline": "Προώθηση",
"introduction.interactions.reblog.text": "Μοιράσου τουτ άλλων χρηστών με όσους σε ακολουθούν προωθώντας τα.",
"introduction.interactions.reply.headline": "Απάντηση",
"introduction.interactions.reply.text": "Μπορείς να απαντήσεις στα τουτ άλλων αλλά ακόμα και στα δικά σου, δένοντας τα όλα μαζί σε μια συζήτηση.",
"introduction.welcome.action": "Ας ξεκινήσουμε!",
"introduction.welcome.headline": "Πρώτα βήματα",
"introduction.welcome.text": "Καλώς ήρθες στο fediverse! Σε πολύ λίγο θα μπορείς να στέλνεις δημοσιεύσεις και να μιλάς με τους φίλους σου σε πολλούς, διαφορετικούς κόμβους. Ο κόμβος {domain} όμως είναι ξεχωριστός — φιλοξενεί τον λογαριασμό σου, για αυτό μα θυμάσαι το όνομά του.",
"keyboard_shortcuts.back": "επιστροφή",
"keyboard_shortcuts.blocked": "άνοιγμα λίστας αποκλεισμένων χρηστών",
"keyboard_shortcuts.boost": "προώθηση",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "Σίγουρα θέλεις να καθαρίσεις όλες τις ειδοποιήσεις σου;",
"notifications.column_settings.alert": "Ειδοποιήσεις επιφάνειας εργασίας",
"notifications.column_settings.favourite": "Αγαπημένα:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Εμφάνιση όλων των κατηγοριών",
"notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου",
"notifications.column_settings.filter_bar.show": "Εμφάνιση",
"notifications.column_settings.follow": "Νέοι ακόλουθοι:",
"notifications.column_settings.mention": "Αναφορές:",
"notifications.column_settings.push": "Άμεσες ειδοποιήσεις",
"notifications.column_settings.reblog": "Προωθήσεις:",
"notifications.column_settings.show": "Εμφάνισε σε στήλη",
"notifications.column_settings.sound": "Ηχητική ειδοποίηση",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.all": "Όλες",
"notifications.filter.boosts": "Προωθήσεις",
"notifications.filter.favourites": "Αγαπημένα",
"notifications.filter.follows": "Ακόλουθοι",
"notifications.filter.mentions": "Αναφορές",
"notifications.group": "{count} ειδοποιήσεις",
"privacy.change": "Προσαρμογή ιδιωτικότητας δημοσίευσης",
"privacy.direct.long": "Δημοσίευση μόνο σε όσους και όσες αναφέρονται",

@ -346,7 +346,7 @@
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_form.description": "Describe for the visually impaired",
"upload_form.focus": "Crop",
"upload_form.focus": "Change preview",
"upload_form.undo": "Delete",
"upload_progress.label": "Uploading...",
"video.close": "Close video",

@ -11,13 +11,13 @@
"account.endorse": "Montri en profilo",
"account.follow": "Sekvi",
"account.followers": "Sekvantoj",
"account.followers.empty": "Neniu ankoraŭ sekvas ĉi tiun uzanton.",
"account.followers.empty": "Ankoraŭ neniu sekvas tiun uzanton.",
"account.follows": "Sekvatoj",
"account.follows.empty": "Ĉi tiu uzanto ne ankoraŭ sekvas iun.",
"account.follows.empty": "Tiu uzanto ankoraŭ ne sekvas iun.",
"account.follows_you": "Sekvas vin",
"account.hide_reblogs": "Kaŝi diskonigojn de @{name}",
"account.link_verified_on": "Proprieto de ĉi tiu ligilo estis kontrolita je {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.link_verified_on": "La posedanto de tiu ligilo estis kontrolita je {date}",
"account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.",
"account.media": "Aŭdovidaĵoj",
"account.mention": "Mencii @{name}",
"account.moved_to": "{name} moviĝis al:",
@ -92,9 +92,9 @@
"confirmations.mute.confirm": "Silentigi",
"confirmations.mute.message": "Ĉu vi certas, ke vi volas silentigi {name}?",
"confirmations.redraft.confirm": "Forigi kaj reskribi",
"confirmations.redraft.message": "Ĉu vi certas ke vi volas forigi tiun mesaĝon kaj reskribi ĝin? Ĉiuj diskonigoj kaj stelumoj estos perditaj, kaj respondoj al la originala mesaĝo estos orfigitaj.",
"confirmations.redraft.message": "Ĉu vi certas ke vi volas forigi tiun mesaĝon kaj reskribi ĝin? Ĉiuj diskonigoj kaj stelumoj estos perditaj, kaj respondoj al la originala mesaĝo estos senparentaj.",
"confirmations.reply.confirm": "Respondi",
"confirmations.reply.message": "Respondi nun anstataŭigos la mesaĝon ke vi aktuale skribas. Ĉu vi certas ke vi volas daŭrigi?",
"confirmations.reply.message": "Respondi nun anstataŭigos la mesaĝon, kiun vi nun skribas. Ĉu vi certas, ke vi volas daŭrigi?",
"confirmations.unfollow.confirm": "Ne plu sekvi",
"confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?",
"embed.instructions": "Enkorpigu ĉi tiun mesaĝon en vian retejon per kopio de la suba kodo.",
@ -114,18 +114,18 @@
"emoji_button.symbols": "Simboloj",
"emoji_button.travel": "Vojaĝoj kaj lokoj",
"empty_column.account_timeline": "Neniu mesaĝo ĉi tie!",
"empty_column.blocks": "Vi ne ankoraŭ blokis iun uzanton.",
"empty_column.blocks": "Vi ankoraŭ ne blokis uzanton.",
"empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!",
"empty_column.direct": "Vi ankoraŭ ne havas rektan mesaĝon. Kiam vi sendos aŭ ricevos iun, ĝi aperos ĉi tie.",
"empty_column.domain_blocks": "Ankoraŭ estas neniu domajno blokita.",
"empty_column.favourited_statuses": "Vi ne ankoraŭ havas iun stelumitan mesaĝon. Kiam vi stelumos iun, tiu aperos ĉi tie.",
"empty_column.favourites": "Neniu ankoraŭ stelumis ĉi tiun mesaĝon. Kiam iu faros ĝin, tiu aperos ĉi tie.",
"empty_column.domain_blocks": "Ankoraŭ neniu domajno estas blokita.",
"empty_column.favourited_statuses": "Vi ankoraŭ ne stelumis mesaĝon. Kiam vi stelumos iun, tiu aperos ĉi tie.",
"empty_column.favourites": "Ankoraŭ neniu stelumis tiun mesaĝon. Kiam iu faros tion, tiu aperos ĉi tie.",
"empty_column.follow_requests": "Vi ne ankoraŭ havas iun peton de sekvado. Kiam vi ricevos unu, ĝi aperos ĉi tie.",
"empty_column.hashtag": "Ankoraŭ estas nenio per ĉi tiu kradvorto.",
"empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
"empty_column.home.public_timeline": "la publikan tempolinion",
"empty_column.list": "Ankoraŭ estas nenio en ĉi tiu listo. Kiam membroj de ĉi tiu listo afiŝos novajn mesaĝojn, ili aperos ĉi tie.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.lists": "Vi ankoraŭ ne havas liston. Kiam vi kreos iun, ĝi aperos ĉi tie.",
"empty_column.mutes": "Vi ne ankoraŭ silentigis iun uzanton.",
"empty_column.notifications": "Vi ankoraŭ ne havas sciigojn. Interagu kun aliaj por komenci konversacion.",
"empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj nodoj por plenigi la publikan tempolinion",
@ -139,9 +139,9 @@
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.",
"getting_started.security": "Sekureco",
"getting_started.terms": "Uzkondiĉoj",
"hashtag.column_header.tag_mode.all": "and {additional}",
"hashtag.column_header.tag_mode.any": "or {additional}",
"hashtag.column_header.tag_mode.none": "without {additional}",
"hashtag.column_header.tag_mode.all": "kaj {additional}",
"hashtag.column_header.tag_mode.any": " {additional}",
"hashtag.column_header.tag_mode.none": "sen {additional}",
"hashtag.column_settings.tag_mode.all": "Ĉiuj",
"hashtag.column_settings.tag_mode.any": "Iu ajn",
"hashtag.column_settings.tag_mode.none": "Neniu",
@ -149,7 +149,7 @@
"home.column_settings.basic": "Bazaj agordoj",
"home.column_settings.show_reblogs": "Montri diskonigojn",
"home.column_settings.show_replies": "Montri respondojn",
"introduction.federation.action": "Next",
"introduction.federation.action": "Sekva",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
@ -216,7 +216,7 @@
"navigation_bar.apps": "Telefonaj aplikaĵoj",
"navigation_bar.blocks": "Blokitaj uzantoj",
"navigation_bar.community_timeline": "Loka tempolinio",
"navigation_bar.compose": "Redakti novan mesaĝon",
"navigation_bar.compose": "Skribi novan mesaĝon",
"navigation_bar.direct": "Rektaj mesaĝoj",
"navigation_bar.discover": "Esplori",
"navigation_bar.domain_blocks": "Kaŝitaj domajnoj",
@ -314,7 +314,7 @@
"status.reblog": "Diskonigi",
"status.reblog_private": "Diskonigi al la originala atentaro",
"status.reblogged_by": "{name} diskonigis",
"status.reblogs.empty": "Neniu ankoraŭ diskonigis ĉi tiun mesaĝon. Kiam iu faris ĝin, tiu aperos ĉi tie.",
"status.reblogs.empty": "Ankoraŭ neniu diskonigis tiun mesaĝon. Kiam iu faros tion, tiu aperos ĉi tie.",
"status.redraft": "Forigi kaj reskribi",
"status.reply": "Respondi",
"status.replyAll": "Respondi al la fadeno",
@ -326,8 +326,8 @@
"status.show_less_all": "Malgrandigi ĉiujn",
"status.show_more": "Grandigi",
"status.show_more_all": "Grandigi ĉiujn",
"status.show_thread": "Montri fadenon",
"status.unmute_conversation": "Malsilentigi konversacion",
"status.show_thread": "Montri la fadenon",
"status.unmute_conversation": "Malsilentigi la konversacion",
"status.unpin": "Depingli de profilo",
"suggestions.dismiss": "Forigi la proponon",
"suggestions.header": "Vi povus interesiĝi pri…",

@ -149,23 +149,23 @@
"home.column_settings.basic": "Basique",
"home.column_settings.show_reblogs": "Afficher les partages",
"home.column_settings.show_replies": "Afficher les réponses",
"introduction.federation.action": "Next",
"introduction.federation.action": "Suivant",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "Les messages publics provenant d'autres serveurs du fediverse apparaîtront dans le fil public global.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "Les messages des personnes que vous suivez apparaîtront dans votre fil d'accueil. Vous pouvez suivre n'importe qui sur n'importe quel serveur !",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.federation.local.text": "Les messages publics de personnes se trouvant sur le même serveur que vous apparaîtront sur le fil public local.",
"introduction.interactions.action": "Finir le tutoriel !",
"introduction.interactions.favourite.headline": "Favoris",
"introduction.interactions.favourite.text": "Vous pouvez garder un pouet pour plus tard, et faire savoir à l'auteur que vous l'avez aimé, en le favorisant.",
"introduction.interactions.reblog.headline": "Repartager",
"introduction.interactions.reblog.text": "Vous pouvez partager les pouets d'autres personnes avec vos suiveurs en les repartageant.",
"introduction.interactions.reply.headline": "Répondre",
"introduction.interactions.reply.text": "Vous pouvez répondre aux pouets d'autres personnes et à vos propres pouets, ce qui les enchaînera dans une conversation.",
"introduction.welcome.action": "Allons-y !",
"introduction.welcome.headline": "Premiers pas",
"introduction.welcome.text": "Bienvenue dans le fediverse ! Dans quelques instants, vous pourrez diffuser des messages et parler à vos amis sur une grande variété de serveurs. Mais ce serveur, {domain}, est spécial - il héberge votre profil, alors souvenez-vous de son nom.",
"keyboard_shortcuts.back": "revenir en arrière",
"keyboard_shortcuts.blocked": "pour ouvrir une liste dutilisateurs bloqués",
"keyboard_shortcuts.boost": "partager",
@ -242,19 +242,19 @@
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications?",
"notifications.column_settings.alert": "Notifications locales",
"notifications.column_settings.favourite": "Favoris:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Afficher toutes les catégories",
"notifications.column_settings.filter_bar.category": "Barre de recherche rapide",
"notifications.column_settings.filter_bar.show": "Afficher",
"notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e·s:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.push": "Notifications",
"notifications.column_settings.reblog": "Partages:",
"notifications.column_settings.show": "Afficher dans la colonne",
"notifications.column_settings.sound": "Émettre un son",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.all": "Tout",
"notifications.filter.boosts": "Repartages",
"notifications.filter.favourites": "Favoris",
"notifications.filter.follows": "Suiveurs",
"notifications.filter.mentions": "Mentions",
"notifications.group": "{count} notifications",
"privacy.change": "Ajuster la confidentialité du message",
@ -341,7 +341,7 @@
"upload_area.title": "Glissez et déposez pour envoyer",
"upload_button.label": "Joindre un média (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_form.description": "Décrire pour les malvoyant·e·s",
"upload_form.focus": "Recadrer",
"upload_form.focus": "Modifier laperçu",
"upload_form.undo": "Supprimer",
"upload_progress.label": "Envoi en cours…",
"video.close": "Fermer la vidéo",

@ -149,23 +149,23 @@
"home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar repeticións",
"home.column_settings.show_replies": "Mostrar respostas",
"introduction.federation.action": "Next",
"introduction.federation.action": "Seguinte",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "Publicacións públicas desde outros servidores do fediverso aparecerán na liña temporal federada.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "Publicacións de xente que vostede segue aparecerán no TL de Inicio. Pode seguir a calquera en calquer servidor!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.federation.local.text": "Publicacións públicas de xente no seu mesmo servidor aparecerán na liña temporal local.",
"introduction.interactions.action": "Rematar titorial!",
"introduction.interactions.favourite.headline": "Favorito",
"introduction.interactions.favourite.text": "Pode gardar un toot para máis tarde, e facerlle saber a autora que lle gustou, dándolle a Favorito.",
"introduction.interactions.reblog.headline": "Promocionar",
"introduction.interactions.reblog.text": "Pode compartir os toots de outra xente coas súas seguirodas promocionándoos.",
"introduction.interactions.reply.headline": "Respostar",
"introduction.interactions.reply.text": "Pode respostar aos toots de outras persoas e aos seus propios, así quedarán encadeados nunha conversa.",
"introduction.welcome.action": "Imos!",
"introduction.welcome.headline": "Primeiros pasos",
"introduction.welcome.text": "Benvida ao fediverso! Nun intre poderá difundir mensaxes e falar cos seus amigos nun gran número de servidores. Pero este servidor (dominio) é especial—hospeda o seu perfil, así que lémbreo.",
"keyboard_shortcuts.back": "voltar atrás",
"keyboard_shortcuts.blocked": "abrir lista de usuarias bloqueadas",
"keyboard_shortcuts.boost": "promover",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "Estás seguro de que queres limpar permanentemente todas as túas notificacións?",
"notifications.column_settings.alert": "Notificacións de escritorio",
"notifications.column_settings.favourite": "Favoritas:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas as categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.filter_bar.show": "Mostrar",
"notifications.column_settings.follow": "Novos seguidores:",
"notifications.column_settings.mention": "Mencións:",
"notifications.column_settings.push": "Enviar notificacións",
"notifications.column_settings.reblog": "Promocións:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir son",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.all": "Todo",
"notifications.filter.boosts": "Promocións",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguimentos",
"notifications.filter.mentions": "Mencións",
"notifications.group": "{count} notificacións",
"privacy.change": "Axustar a intimidade do estado",
"privacy.direct.long": "Enviar exclusivamente as usuarias mencionadas",

@ -149,23 +149,23 @@
"home.column_settings.basic": "기본 설정",
"home.column_settings.show_reblogs": "부스트 표시",
"home.column_settings.show_replies": "답글 표시",
"introduction.federation.action": "Next",
"introduction.federation.action": "다음",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "페디버스의 다른 서버의 공개 게시물이 연합 타임라인에 나타납니다.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "당신이 팔로우 하고 있는 사람의 게시물이 홈 타임라인에 나타납니다. 어느 서버에 있는 사람이라도 팔로우가 가능합니다!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.federation.local.text": "같은 서버에 있는 공개 게시물은 로컬 타임라인에 나타납니다.",
"introduction.interactions.action": "튜토리얼 마치기!",
"introduction.interactions.favourite.headline": "즐겨찾기",
"introduction.interactions.favourite.text": "나중을 위해 툿을 저장할 수 있습니다, 그리고 작성자에게 당신이 이 글을 마음에 들어한다는 걸 알립니다.",
"introduction.interactions.reblog.headline": "부스트",
"introduction.interactions.reblog.text": "부스트를 통해 다른 사람의 툿을 당신의 팔로워들에게 공유할 수 있습니다.",
"introduction.interactions.reply.headline": "답글",
"introduction.interactions.reply.text": "다른 사람이나 나의 툿에 답글을 달 수 있습니다, 이 답글은 하나의 타래글로 이어집니다.",
"introduction.welcome.action": "출발!",
"introduction.welcome.headline": "첫걸음",
"introduction.welcome.text": "페디버스에 오신 것을 환영합니다! 잠시 후, 당신은 수 많은 다양한 서버들에 존재하는 친구들에게 메시지를 보내고 대화 할 수 있게 됩니다. 하지만 이 서버, {domain}은 특별합니다. 이 서버는 당신의 프로필을 제공하니 이름을 기억하세요.",
"keyboard_shortcuts.back": "뒤로가기",
"keyboard_shortcuts.blocked": "차단한 유저 리스트 열기",
"keyboard_shortcuts.boost": "부스트",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "정말로 알림을 삭제하시겠습니까?",
"notifications.column_settings.alert": "데스크탑 알림",
"notifications.column_settings.favourite": "즐겨찾기:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "카테고리의 모든 종류를 표시",
"notifications.column_settings.filter_bar.category": "퀵 필터 바",
"notifications.column_settings.filter_bar.show": "표시",
"notifications.column_settings.follow": "새 팔로워:",
"notifications.column_settings.mention": "답글:",
"notifications.column_settings.push": "푸시 알림",
"notifications.column_settings.reblog": "부스트:",
"notifications.column_settings.show": "컬럼에 표시",
"notifications.column_settings.sound": "효과음 재생",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.all": "모두",
"notifications.filter.boosts": "부스트",
"notifications.filter.favourites": "즐겨찾기",
"notifications.filter.follows": "팔로우",
"notifications.filter.mentions": "멘션",
"notifications.group": "{count} 개의 알림",
"privacy.change": "포스트의 프라이버시 설정을 변경",
"privacy.direct.long": "멘션한 사용자에게만 공개",

@ -145,27 +145,27 @@
"hashtag.column_settings.tag_mode.all": "Allemaal",
"hashtag.column_settings.tag_mode.any": "Een van deze",
"hashtag.column_settings.tag_mode.none": "Geen van deze",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"hashtag.column_settings.tag_toggle": "Additionele tags aan deze kolom toevoegen",
"home.column_settings.basic": "Algemeen",
"home.column_settings.show_reblogs": "Boosts tonen",
"home.column_settings.show_replies": "Reacties tonen",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.action": "Volgende",
"introduction.federation.federated.headline": "Globaal",
"introduction.federation.federated.text": "Openbare toots van mensen op andere servers in de fediverse verschijnen op de globale tijdlijn.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "Toots van mensen die jij volgt verschijnen onder start. Je kunt iedereen op elke server volgen!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.federation.local.text": "Openbare toots van mensen die ook op jouw server zitten verschijnen op de lokale tijdlijn.",
"introduction.interactions.action": "Introductie beëindigen!",
"introduction.interactions.favourite.headline": "Favorieten",
"introduction.interactions.favourite.text": "Je kunt door een toot als favoriet te markeren, deze voor later bewaren en de auteur laten weten dat je het leuk vond.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.interactions.reblog.text": "Je kunt toots van andere mensen met jouw volgers delen door deze te boosten.",
"introduction.interactions.reply.headline": "Reageren",
"introduction.interactions.reply.text": "Je kunt op toots van andere mensen en op die van jezelf reageren, waardoor er een gesprek ontstaat.",
"introduction.welcome.action": "Laten we beginnen!",
"introduction.welcome.headline": "Eerste stappen",
"introduction.welcome.text": "Welkom in de fediverse! Binnen enkele ogenblikken kun jij berichten (toots) versturen en met vrienden op veel verschillende servers praten. Maar deze server, {domain}, is speciaal—het herbergt jouw profiel, onthou dus de naam.",
"keyboard_shortcuts.back": "om terug te gaan",
"keyboard_shortcuts.blocked": "om de door jou geblokkeerde gebruikers te tonen",
"keyboard_shortcuts.boost": "om te boosten",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "Weet je het zeker dat je al jouw meldingen wilt verwijderen?",
"notifications.column_settings.alert": "Desktopmeldingen",
"notifications.column_settings.favourite": "Favorieten:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Alle categorieën tonen",
"notifications.column_settings.filter_bar.category": "Snelle filterbalk",
"notifications.column_settings.filter_bar.show": "Tonen",
"notifications.column_settings.follow": "Nieuwe volgers:",
"notifications.column_settings.mention": "Vermeldingen:",
"notifications.column_settings.push": "Pushmeldingen",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "In kolom tonen",
"notifications.column_settings.sound": "Geluid afspelen",
"notifications.filter.all": "All",
"notifications.filter.all": "Alles",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.favourites": "Favorieten",
"notifications.filter.follows": "Die jij volgt",
"notifications.filter.mentions": "Vermeldingen",
"notifications.group": "{count} meldingen",
"privacy.change": "Zichtbaarheid toot aanpassen",
"privacy.direct.long": "Alleen aan vermelde gebruikers tonen",

@ -341,7 +341,7 @@
"upload_area.title": "Lisatz e depausatz per mandar",
"upload_button.label": "Ajustar un mèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_form.description": "Descripcion pels mal vesents",
"upload_form.focus": "Retalhar",
"upload_form.focus": "Modificar lapercebut",
"upload_form.undo": "Suprimir",
"upload_progress.label": "Mandadís…",
"video.close": "Tampar la vidèo",

@ -29,9 +29,9 @@
"account.report": "Nahlás @{name}",
"account.requested": "Čaká na schválenie. Kliknite pre zrušenie žiadosti",
"account.share": "Zdieľať @{name} profil",
"account.show_reblogs": "Ukáž povýšenia od @{name}",
"account.unblock": "Odblokovať @{name}",
"account.unblock_domain": "Prestať blokovať {domain}",
"account.show_reblogs": "Ukáž vyzdvihnutia od @{name}",
"account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Prestaň skrývať {domain}",
"account.unendorse": "Nezobrazuj na profile",
"account.unfollow": "Prestať nasledovať",
"account.unmute": "Prestať ignorovať @{name}",
@ -92,8 +92,8 @@
"confirmations.mute.confirm": "Ignoruj",
"confirmations.mute.message": "Naozaj chcete ignorovať {name}?",
"confirmations.redraft.confirm": "Vyčistiť a prepísať",
"confirmations.redraft.message": "Si si istý/á, že chceš premazať a prepísať tento príspevok? Jeho nadobudnuté odpovede, povýšenia a obľúbenia, ale i odpovede na pôvodný príspevok budú odlúčené.",
"confirmations.reply.confirm": "Odpoved",
"confirmations.redraft.message": "Si si istý/á, že chceš premazať a prepísať tento príspevok? Jeho nadobudnuté vyzdvihnutia a obľúbenia, ale i odpovede na pôvodný príspevok budú odlúčené.",
"confirmations.reply.confirm": "Odpovedz",
"confirmations.reply.message": "Odpovedaním akurát teraz prepíšeš správu, ktorú máš práve rozpísanú. Si si istý/á, že chceš pokračovať?",
"confirmations.unfollow.confirm": "Nesledovať",
"confirmations.unfollow.message": "Naozaj chcete prestať sledovať {name}?",
@ -149,23 +149,23 @@
"home.column_settings.basic": "Základné",
"home.column_settings.show_reblogs": "Zobraziť povýšené",
"home.column_settings.show_replies": "Ukázať odpovede",
"introduction.federation.action": "Next",
"introduction.federation.action": "Ďalej",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.federated.text": "Verejné príspevky z ostatných serverov vo fediverse budú zobrazenie vo federovanej časovej osi.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.home.text": "Príspevky od ľudí ktorých následuješ sa zobrazia na tvojej domovskej nástenke. Môžeš následovať hocikoho na ktoromkoľvek serveri!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"introduction.federation.local.text": "Verejné príspevky od ľudí v rámci toho istého serveru na akom si aj ty, budú zobrazované na miestnej časovej osi.",
"introduction.interactions.action": "Ukonči návod!",
"introduction.interactions.favourite.headline": "Obľúbené",
"introduction.interactions.favourite.text": "Obľúbením si môžeš príspevok uložiť na neskôr, a zároveň dať jeho autorovi vedieť, že sa ti páčil.",
"introduction.interactions.reblog.headline": "Povýš",
"introduction.interactions.reblog.text": "Môžeš zdieľať príspevky iných ľudí s vašimi následovateľmi tým, že ich povýšiš.",
"introduction.interactions.reply.headline": "Odpovedz",
"introduction.interactions.reply.text": "Odpovedať môžeš na príspevky iných ľudí, aj na svoje vlastné, čím sa sspolu prepoja do konverzácie.",
"introduction.welcome.action": "Poďme do toho!",
"introduction.welcome.headline": "Prvé kroky",
"introduction.welcome.text": "Vitaj vo fediverse! Za malú chvíľu budeš môcť posielať správy a rozpovedať sa so svojími priateľmi cez širokú škálu rôznorodých serverov. Ale tento server, {domain}, je špeciálny v tom, že ukladá tvoj profil, takže si jeho názov zapametaj.",
"keyboard_shortcuts.back": "dostať sa naspäť",
"keyboard_shortcuts.blocked": "otvor zoznam blokovaných užívateľov",
"keyboard_shortcuts.boost": "vyzdvihnúť",
@ -242,20 +242,20 @@
"notifications.clear_confirmation": "Naozaj chcete nenávratne prečistiť všetky vaše notifikácie?",
"notifications.column_settings.alert": "Notifikácie na ploche",
"notifications.column_settings.favourite": "Obľúbené:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Zobraz všetky kategórie",
"notifications.column_settings.filter_bar.category": "Rýchle triedenie",
"notifications.column_settings.filter_bar.show": "Ukáž",
"notifications.column_settings.follow": "Noví následujúci:",
"notifications.column_settings.mention": "Zmienenia:",
"notifications.column_settings.push": "Push notifikácie",
"notifications.column_settings.reblog": "Boosty:",
"notifications.column_settings.show": "Zobraziť v stĺpci",
"notifications.column_settings.sound": "Prehrať zvuk",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.all": "Všetky",
"notifications.filter.boosts": "Vyzdvihnutia",
"notifications.filter.favourites": "Obľúbené",
"notifications.filter.follows": "Sledovania",
"notifications.filter.mentions": "Spomenutia",
"notifications.group": "{count} oznámenia",
"privacy.change": "Zmeňiť viditeľnosť statusu",
"privacy.direct.long": "Poslať priamo iba spomenutým používateľom",
@ -319,7 +319,7 @@
"status.reply": "Odpovedať",
"status.replyAll": "Odpovedať na diskusiu",
"status.report": "Nahlásiť @{name}",
"status.sensitive_toggle": "Kliknite pre zobrazenie",
"status.sensitive_toggle": "Klikni pre zobrazenie",
"status.sensitive_warning": "Chúlostivý obsah",
"status.share": "Zdieľať",
"status.show_less": "Zobraz menej",

@ -88,7 +88,7 @@ const deleteStatus = (state, id, accountId, references) => {
};
const clearTimeline = (state, timeline) => {
return state.updateIn([timeline, 'items'], list => list.clear());
return state.set(timeline, initialTimeline);
};
const filterTimelines = (state, relationship, statuses) => {

@ -11,5 +11,5 @@ export default function configureStore() {
loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
errorsMiddleware(),
soundsMiddleware()
), window.devToolsExtension ? window.devToolsExtension() : f => f));
), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f));
};

@ -30,15 +30,21 @@
}
}
&__num {
&__num,
&__text {
text-align: center;
font-weight: 500;
font-size: 24px;
line-height: 21px;
color: $primary-text-color;
font-family: $font-display, sans-serif;
margin-bottom: 20px;
}
&__text {
font-size: 18px;
}
&__label {
font-size: 14px;
color: $darker-text-color;

@ -50,6 +50,8 @@ class ActivityPub::Activity
ActivityPub::Activity::Add
when 'Remove'
ActivityPub::Activity::Remove
when 'Move'
ActivityPub::Activity::Move
end
end
end

@ -4,9 +4,10 @@ class ActivityPub::Activity::Block < ActivityPub::Activity
def perform
target_account = account_from_uri(object_uri)
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.blocking?(target_account)
return if target_account.nil? || !target_account.local? || @account.blocking?(target_account)
UnfollowService.new.call(target_account, @account) if target_account.following?(@account)
@account.block!(target_account, uri: @json['id'])
@account.block!(target_account, uri: @json['id']) unless delete_arrived_first?(@json['id'])
end
end

@ -210,7 +210,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def resolve_thread(status)
return unless status.reply? && status.thread.nil?
return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
end

@ -8,8 +8,6 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
target_statuses_by_account = object_uris.map { |uri| status_from_uri(uri) }.compact.select(&:local?).group_by(&:account_id)
target_accounts.each do |target_account|
next if Report.where(account: @account, target_account: target_account).exists?
target_statuses = target_statuses_by_account[target_account.id]
ReportService.new.call(

@ -6,7 +6,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account)
if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain)
if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved?
reject_follow_request!(target_account)
return
end
@ -28,7 +28,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
end
def reject_follow_request!(target_account)
json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(target_account))
json = ActiveModelSerializers::SerializableResource.new(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).to_json
ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url)
end
end

@ -0,0 +1,43 @@
# frozen_string_literal: true
class ActivityPub::Activity::Move < ActivityPub::Activity
PROCESSING_COOLDOWN = 7.days.seconds
def perform
return if origin_account.uri != object_uri || processed?
mark_as_processing!
target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri)
return if target_account.nil? || !target_account.also_known_as.include?(origin_account.uri)
# In case for some reason we didn't have a redirect for the profile already, set it
origin_account.update(moved_to_account: target_account) if origin_account.moved_to_account_id.nil?
# Initiate a re-follow for each follower
origin_account.followers.local.select(:id).find_in_batches do |follower_accounts|
UnfollowFollowWorker.push_bulk(follower_accounts.map(&:id)) do |follower_account_id|
[follower_account_id, origin_account.id, target_account.id]
end
end
end
private
def origin_account
@account
end
def target_uri
value_or_id(@json['target'])
end
def processed?
redis.exists("move_in_progress:#{@account.id}")
end
def mark_as_processing!
redis.setex("move_in_progress:#{@account.id}", PROCESSING_COOLDOWN, true)
end
end

@ -10,6 +10,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'sensitive' => 'as:sensitive',
'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' },
'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' },
'Hashtag' => 'as:Hashtag',
'ostatus' => 'http://ostatus.org#',
'atomUri' => 'ostatus:atomUri',

@ -57,7 +57,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
save_emojis(status)
end
if thread? && status.thread.nil?
if thread? && status.thread.nil? && Request.valid_url?(thread.second)
Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}"
ThreadResolveWorker.perform_async(status.id, thread.second)
end

@ -66,6 +66,18 @@ class Request
(@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
end
class << self
def valid_url?(url)
begin
parsed_url = Addressable::URI.parse(url)
rescue Addressable::URI::InvalidURIError
return false
end
%w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
end
end
private
def set_common_headers!

@ -66,16 +66,20 @@ class NotificationMailer < ApplicationMailer
end
def digest(recipient, **opts)
return if recipient.user.disabled?
@me = recipient
@since = opts[:since] || @me.user.last_emailed_at || @me.user.current_sign_in_at
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
@notifications_count = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).count
return if @me.user.disabled? || @notifications.empty?
return if @notifications_count.zero?
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).limit(40)
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
locale_for_account(@me) do
mail to: @me.user.email,
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size)
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications_count)
end
end

@ -44,6 +44,7 @@
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
#
class Account < ApplicationRecord
@ -59,6 +60,7 @@ class Account < ApplicationRecord
include Attachmentable
include Paginable
include AccountCounters
include DomainNormalizable
MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i
@ -87,6 +89,7 @@ class Account < ApplicationRecord
scope :silenced, -> { where(silenced: true) }
scope :suspended, -> { where(suspended: true) }
scope :without_suspended, -> { where(suspended: false) }
scope :without_silenced, -> { where(silenced: false) }
scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
@ -94,8 +97,8 @@ class Account < ApplicationRecord
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status }
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status }
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
scope :popular, -> { order('account_stats.followers_count desc') }
@ -142,6 +145,10 @@ class Account < ApplicationRecord
"#{username}@#{Rails.configuration.x.local_domain}"
end
def local_followers_count
Follow.where(target_account_id: id).count
end
def to_webfinger_s
"acct:#{local_username_and_domain}"
end
@ -226,6 +233,10 @@ class Account < ApplicationRecord
end
end
def also_known_as
self[:also_known_as] || []
end
def fields
(self[:fields] || []).map { |f| Field.new(self, f) }
end
@ -459,7 +470,6 @@ class Account < ApplicationRecord
end
before_create :generate_keys
before_validation :normalize_domain
before_validation :prepare_contents, if: :local?
before_destroy :clean_feed_manager
@ -497,7 +507,7 @@ class Account < ApplicationRecord
def normalize_domain
return if local?
self.domain = TagManager.instance.normalize_domain(domain)
super
end
def emojifiable_text

@ -0,0 +1,15 @@
# frozen_string_literal: true
module DomainNormalizable
extend ActiveSupport::Concern
included do
before_validation :normalize_domain
end
private
def normalize_domain
self.domain = TagManager.instance.normalize_domain(domain)
end
end

@ -13,6 +13,8 @@
#
class DomainBlock < ApplicationRecord
include DomainNormalizable
enum severity: [:silence, :suspend, :noop]
attr_accessor :retroactive
@ -25,12 +27,4 @@ class DomainBlock < ApplicationRecord
def self.blocked?(domain)
where(domain: domain, severity: :suspend).exists?
end
before_validation :normalize_domain
private
def normalize_domain
self.domain = TagManager.instance.normalize_domain(domain)
end
end

@ -10,7 +10,7 @@
#
class EmailDomainBlock < ApplicationRecord
before_validation :normalize_domain
include DomainNormalizable
validates :domain, presence: true, uniqueness: true
@ -27,10 +27,4 @@ class EmailDomainBlock < ApplicationRecord
where(domain: domain).exists?
end
private
def normalize_domain
self.domain = TagManager.instance.normalize_domain(domain)
end
end

@ -9,15 +9,33 @@ class Export
end
def to_blocked_accounts_csv
to_csv account.blocking
to_csv account.blocking.select(:username, :domain)
end
def to_muted_accounts_csv
to_csv account.muting
to_csv account.muting.select(:username, :domain)
end
def to_following_accounts_csv
to_csv account.following
to_csv account.following.select(:username, :domain)
end
def to_lists_csv
CSV.generate do |csv|
account.owned_lists.select(:title).each do |list|
list.accounts.select(:username, :domain).each do |account|
csv << [list.title, acct(account)]
end
end
end
end
def to_blocked_domains_csv
CSV.generate do |csv|
account.domain_blocks.pluck(:domain).each do |domain|
csv << [domain]
end
end
end
def total_storage
@ -32,6 +50,10 @@ class Export
account.following_count
end
def total_lists
account.owned_lists.count
end
def total_followers
account.followers_count
end
@ -44,13 +66,21 @@ class Export
account.muting.count
end
def total_domain_blocks
account.domain_blocks.count
end
private
def to_csv(accounts)
CSV.generate do |csv|
accounts.each do |account|
csv << [(account.local? ? account.local_username_and_domain : account.acct)]
csv << [acct(account)]
end
end
end
def acct(account)
account.local? ? account.local_username_and_domain : account.acct
end
end

@ -6,8 +6,6 @@ class Form::StatusBatch
attr_accessor :status_ids, :action, :current_account
ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze
def save
case action
when 'nsfw_on', 'nsfw_off'

@ -15,7 +15,7 @@ class ReportNote < ApplicationRecord
belongs_to :account
belongs_to :report, inverse_of: :notes, touch: true
scope :latest, -> { reorder('created_at ASC') }
scope :latest, -> { reorder(created_at: :desc) }
validates :content, presence: true, length: { maximum: 500 }
end

@ -36,6 +36,7 @@
# invite_id :bigint(8)
# remember_token :string
# chosen_languages :string is an Array
# created_by_application_id :bigint(8)
#
class User < ApplicationRecord
@ -49,7 +50,7 @@ class User < ApplicationRecord
# every day. Raising the duration reduces the amount of expensive
# RegenerationWorker jobs that need to be run when those people come
# to check their feed
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze
devise :two_factor_authenticatable,
otp_secret_encryption_key: Rails.configuration.x.otp_secret
@ -66,6 +67,7 @@ class User < ApplicationRecord
belongs_to :account, inverse_of: :user
belongs_to :invite, counter_cache: :uses, optional: true
belongs_to :created_by_application, class_name: 'Doorkeeper::Application', optional: true
accepts_nested_attributes_for :account
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
@ -74,15 +76,18 @@ class User < ApplicationRecord
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
validates_with BlacklistedEmailValidator, if: :email_changed?
validates_with EmailMxValidator, if: :validate_email_dns?
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
scope :recent, -> { order(id: :desc) }
scope :admins, -> { where(admin: true) }
scope :moderators, -> { where(moderator: true) }
scope :staff, -> { admins.or(moderators) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :enabled, -> { where(disabled: false) }
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
before_validation :sanitize_languages
@ -136,6 +141,10 @@ class User < ApplicationRecord
confirmed_at.present?
end
def invited?
invite_id.present?
end
def staff?
admin? || moderator?
end
@ -294,7 +303,7 @@ class User < ApplicationRecord
end
if resource.blank?
resource = new(email: attributes[:email])
resource = new(email: attributes[:email], agreement: true)
if Devise.check_at_sign && !resource[:email].index('@')
resource[:email] = Rpam2.getenv(resource.find_pam_service, attributes[:email], attributes[:password], 'email', false)
resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" unless resource[:email]
@ -307,7 +316,7 @@ class User < ApplicationRecord
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
if resource.blank?
resource = new(email: attributes[:mail].first, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
resource.ldap_setup(attributes)
end

@ -14,6 +14,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
has_many :virtual_attachments, key: :attachment
attribute :moved_to, if: :moved?
attribute :also_known_as, if: :also_known_as?
class EndpointsSerializer < ActiveModel::Serializer
include RoutingHelper
@ -116,6 +117,10 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
end
def also_known_as?
!object.also_known_as.empty?
end
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
end

@ -22,6 +22,7 @@ class InitialStateSerializer < ActiveModel::Serializer
version: Mastodon::Version.to_s,
invites_enabled: Setting.min_invite_role == 'user',
mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory,
}
if object.current_account

@ -75,6 +75,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.note = @json['summary'] || ''
@account.locked = @json['manuallyApprovesFollowers'] || false
@account.fields = property_values || {}
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
@account.actor_type = actor_type
end

@ -27,7 +27,7 @@ class ActivityPub::ProcessCollectionService < BaseService
private
def different_actor?
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri && @json['signature'].present?
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri
end
def process_items(items)

@ -31,11 +31,11 @@ class AfterBlockDomainFromAccountService < BaseService
return unless follow.account.activitypub?
json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
json = ActiveModelSerializers::SerializableResource.new(
follow,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(@account))
).to_json
ActivityPub::DeliveryWorker.perform_async(json, @account.id, follow.account.inbox_url)
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
class AppSignUpService < BaseService
def call(app, params)
return unless allowed_registrations?
user_params = params.slice(:email, :password, :agreement)
account_params = params.slice(:username)
user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params))
Doorkeeper::AccessToken.create!(application: app,
resource_owner_id: user.id,
scopes: app.scopes,
expires_in: Doorkeeper.configuration.access_token_expires_in,
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?)
end
private
def allowed_registrations?
Setting.open_registrations && !Rails.configuration.x.single_user_mode
end
end

@ -24,11 +24,11 @@ class AuthorizeFollowService < BaseService
end
def build_json(follow_request)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
ActiveModelSerializers::SerializableResource.new(
follow_request,
serializer: ActivityPub::AcceptFollowSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(follow_request.target_account))
).to_json
end
def build_xml(follow_request)

@ -1,8 +1,6 @@
# frozen_string_literal: true
class BlockService < BaseService
include StreamEntryRenderer
def call(account, target_account)
return if account.id == target_account.id
@ -27,11 +25,11 @@ class BlockService < BaseService
end
def build_json(block)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
ActiveModelSerializers::SerializableResource.new(
block,
serializer: ActivityPub::BlockSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(block.account))
).to_json
end
def build_xml(block)

@ -1,8 +1,6 @@
# frozen_string_literal: true
class FollowService < BaseService
include StreamEntryRenderer
# Follow a remote user, notify remote user about the follow
# @param [Account] source_account From which to follow
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
@ -12,7 +10,7 @@ class FollowService < BaseService
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved?
if source_account.following?(target_account)
# We're already following this account, but we'll call follow! again to
@ -82,10 +80,10 @@ class FollowService < BaseService
end
def build_json(follow_request)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
ActiveModelSerializers::SerializableResource.new(
follow_request,
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(follow_request.account))
).to_json
end
end

@ -26,13 +26,16 @@ class PostStatusService < BaseService
text = media.find(&:video?) ? '📹' : '🖼' if media.size > 0
end
visibility = options[:visibility] || account.user&.setting_default_privacy
visibility = :unlisted if visibility == :public && account.silenced
ApplicationRecord.transaction do
status = account.statuses.create!(text: text,
media_attachments: media || [],
thread: in_reply_to,
sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]) || options[:spoiler_text].present?,
spoiler_text: options[:spoiler_text] || '',
visibility: options[:visibility] || account.user&.setting_default_privacy,
visibility: visibility,
language: language_from_option(options[:language]) || account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(text, account),
application: options[:application])
end
@ -46,7 +49,6 @@ class PostStatusService < BaseService
unless status.local_only?
Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
ActivityPub::DistributionWorker.perform_async(status.id)
ActivityPub::ReplyDistributionWorker.perform_async(status.id) if status.reply? && status.thread.account.local?
end
if options[:idempotency].present?

@ -60,11 +60,13 @@ class ProcessMentionsService < BaseService
end
def activitypub_json
@activitypub_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
return @activitypub_json if defined?(@activitypub_json)
payload = ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(@status.account))
).as_json
@activitypub_json = Oj.dump(@status.distributable? ? ActivityPub::LinkedDataSignature.new(payload).sign!(@status.account) : payload)
end
def resolve_account_service

@ -19,11 +19,11 @@ class RejectFollowService < BaseService
end
def build_json(follow_request)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
ActiveModelSerializers::SerializableResource.new(
follow_request,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(follow_request.target_account))
).to_json
end
def build_xml(follow_request)

@ -19,6 +19,7 @@ class ResolveAccountService < BaseService
@account = uri
@username = @account.username
@domain = @account.domain
uri = "#{@username}@#{@domain}"
return @account if @account.local? || !webfinger_update_due?
else

@ -34,6 +34,8 @@ class SearchService < BaseService
.compact
statuses.reject { |status| StatusFilter.new(status, account).filtered? }
rescue Faraday::ConnectionFailed
[]
end
def perform_hashtags_search!

@ -20,11 +20,11 @@ class UnblockService < BaseService
end
def build_json(unblock)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
ActiveModelSerializers::SerializableResource.new(
unblock,
serializer: ActivityPub::UndoBlockSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(unblock.account))
).to_json
end
def build_xml(block)

@ -43,11 +43,11 @@ class UnfollowService < BaseService
end
def build_json(follow)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
ActiveModelSerializers::SerializableResource.new(
follow,
serializer: ActivityPub::UndoFollowSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(follow.account))
).to_json
end
def build_xml(follow)

@ -10,7 +10,6 @@ class VerifyLinkService < BaseService
return unless link_back_present?
field.mark_verified!
field.account.save!
rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
Rails.logger.debug "Error fetching link #{@url}: #{e}"
nil

@ -2,31 +2,32 @@
class BlacklistedEmailValidator < ActiveModel::Validator
def validate(user)
user.errors.add(:email, I18n.t('users.invalid_email')) if blocked_email?(user.email)
@email = user.email
user.errors.add(:email, I18n.t('users.invalid_email')) if blocked_email?
end
private
def blocked_email?(value)
on_blacklist?(value) || not_on_whitelist?(value)
def blocked_email?
on_blacklist? || not_on_whitelist?
end
def on_blacklist?(value)
return true if EmailDomainBlock.block?(value)
def on_blacklist?
return true if EmailDomainBlock.block?(@email)
return false if Rails.configuration.x.email_domains_blacklist.blank?
domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
value =~ regexp
@email =~ regexp
end
def not_on_whitelist?(value)
def not_on_whitelist?
return false if Rails.configuration.x.email_domains_whitelist.blank?
domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.')
regexp = Regexp.new("@(.+\\.)?(#{domains})$", true)
value !~ regexp
@email !~ regexp
end
end

@ -4,14 +4,19 @@ class DisallowedHashtagsValidator < ActiveModel::Validator
def validate(status)
return unless status.local? && !status.reblog?
tags = Extractor.extract_hashtags(status.text)
tags.keep_if { |tag| disallowed_hashtags.include? tag.downcase }
@status = status
tags = select_tags
status.errors.add(:text, I18n.t('statuses.disallowed_hashtags', tags: tags.join(', '), count: tags.size)) unless tags.empty?
end
private
def select_tags
tags = Extractor.extract_hashtags(@status.text)
tags.keep_if { |tag| disallowed_hashtags.include? tag.downcase }
end
def disallowed_hashtags
return @disallowed_hashtags if @disallowed_hashtags

@ -5,27 +5,29 @@ class StatusLengthValidator < ActiveModel::Validator
def validate(status)
return unless status.local? && !status.reblog?
status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if too_long?(status)
@status = status
status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if too_long?
end
private
def too_long?(status)
countable_length(status) > MAX_CHARS
def too_long?
countable_length > MAX_CHARS
end
def countable_length(status)
total_text(status).mb_chars.grapheme_length
def countable_length
total_text.mb_chars.grapheme_length
end
def total_text(status)
[status.spoiler_text, countable_text(status)].join
def total_text
[@status.spoiler_text, countable_text].join
end
def countable_text(status)
return '' if status.text.nil?
def countable_text
return '' if @status.text.nil?
status.text.dup.tap do |new_text|
@status.text.dup.tap do |new_text|
new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23)
new_text.gsub!(Account::MENTION_RE, '@\2')
end

@ -1,53 +1,81 @@
- content_for :page_title do
= @account.acct
= render 'application/card', account: @account
.dashboard__counters{ style: 'margin-top: 10px' }
%div
= link_to admin_account_statuses_path(@account.id) do
.dashboard__counters__num= number_with_delimiter @account.statuses_count
.dashboard__counters__label= t 'admin.accounts.statuses'
%div
= link_to admin_account_statuses_path(@account.id, { media: true }) do
.dashboard__counters__num= number_to_human_size @account.media_attachments.sum('file_file_size')
.dashboard__counters__label= t 'admin.accounts.media_attachments'
%div
= link_to admin_account_followers_path(@account.id) do
.dashboard__counters__num= number_with_delimiter @account.local_followers_count
.dashboard__counters__label= t 'admin.accounts.followers'
%div
= link_to admin_reports_path(account_id: @account.id) do
.dashboard__counters__num= number_with_delimiter @account.reports.count
.dashboard__counters__label= t '.created_reports'
%div
= link_to admin_reports_path(target_account_id: @account.id) do
.dashboard__counters__num= number_with_delimiter @account.targeted_reports.count
.dashboard__counters__label= t '.targeted_reports'
%div
%div
.dashboard__counters__text
- if @account.local? && @account.user.nil?
%span.neutral= t('admin.accounts.deleted')
- elsif @account.suspended?
%span.red= t('admin.accounts.suspended')
- elsif @account.silenced?
%span.red= t('admin.accounts.silenced')
- elsif @account.local? && @account.user&.disabled?
%span.red= t('admin.accounts.disabled')
- elsif @account.local? && !@account.user&.confirmed?
%span.neutral= t('admin.accounts.confirming')
- else
%span.neutral= t('admin.accounts.no_limits_imposed')
.dashboard__counters__label= t 'admin.accounts.login_status'
- unless @account.local? && @account.user.nil?
.table-wrapper
%table.table.inline-table
%tbody
%tr
%th= t('admin.accounts.username')
%td= @account.username
%tr
%th= t('admin.accounts.domain')
%td= @account.domain
%tr
%th= t('admin.accounts.display_name')
%td= @account.display_name
- if @account.local?
- if @account.avatar?
%tr
%th= t('admin.accounts.avatar')
%td= table_link_to 'trash', t('admin.accounts.remove_avatar'), remove_avatar_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_avatar, @account)
%td
= link_to @account.avatar.url(:original) do
= image_tag @account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
- if @account.local? && @account.avatar?
= table_link_to 'trash', t('admin.accounts.remove_avatar'), remove_avatar_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_avatar, @account)
- if @account.header?
%tr
%th= t('admin.accounts.header')
%td= table_link_to 'trash', t('admin.accounts.remove_header'), remove_header_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_header, @account)
%td
= link_to @account.header.url(:original) do
= image_tag @account.header.url(:original), alt: '', width: 128, height: 40, class: 'header'
- if @account.local? && @account.header?
= table_link_to 'trash', t('admin.accounts.remove_header'), remove_header_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:remove_header, @account)
- if @account.local?
%tr
%th= t('admin.accounts.role')
%td= t("admin.accounts.roles.#{@account.user&.role}")
%td
- if @account.user.nil?
= t("admin.accounts.moderation.suspended")
- else
= t("admin.accounts.roles.#{@account.user&.role}")
= table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user)
= table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
%tr
%th= t('admin.accounts.email')
%td
= @account.user_email
= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user)
%td= @account.user_email
%td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user)
- if @account.user_unconfirmed_email.present?
%tr
%th= t('admin.accounts.unconfirmed_email')
%td= @account.user_unconfirmed_email
%td
= @account.user_unconfirmed_email
%tr
%th= t('admin.accounts.email_status')
%td
@ -55,56 +83,60 @@
= t('admin.accounts.confirmed')
- else
= t('admin.accounts.confirming')
= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user)
%td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user)
%tr
%th= t('admin.accounts.login_status')
%td
- if @account.user&.disabled?
= t('admin.accounts.disabled')
= table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user)
- else
= t('admin.accounts.enabled')
%td
- if @account.user&.disabled?
= table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user)
- else
= table_link_to 'lock', t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable') if can?(:disable, @account.user)
%tr
%th= t('simple_form.labels.defaults.locale')
%td= @account.user_locale
%td
%tr
%th= t('admin.accounts.joined')
%td
%time.formatted{ datetime: @account.created_at.iso8601, title: l(@account.created_at) }= l @account.created_at
%td
%tr
%th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip
%td
%tr
%th= t('admin.accounts.most_recent_activity')
%td
- if @account.user_current_sign_in_at
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }
= l @account.user_current_sign_in_at
- else
\-
- else
%tr
%th= t('admin.accounts.profile_url')
%td= link_to @account.url, @account.url
%tr
%th= t('admin.accounts.protocol')
%td= @account.protocol.humanize
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }= l @account.user_current_sign_in_at
- if @account.user&.invited?
%tr
%th= t('admin.accounts.follows')
%td= number_to_human @account.following_count
%tr
%th= t('admin.accounts.followers')
%td= number_to_human @account.followers_count
%tr
%th= t('admin.accounts.statuses')
%td= link_to number_to_human(@account.statuses_count), admin_account_statuses_path(@account.id)
%tr
%th= t('admin.accounts.media_attachments')
%th= t('admin.accounts.invited_by')
%td= admin_account_link_to @account.user.invite.user.account
%td
= link_to number_to_human(@account.media_attachments.count), admin_account_statuses_path(@account.id, { media: true })
= surround '(', ')' do
= number_to_human_size @account.media_attachments.sum('file_file_size')
- else
%tr
%th= t('.created_reports')
%td= link_to pluralize(@account.reports.count, t('.report')), admin_reports_path(account_id: @account.id)
%th= t('admin.accounts.inbox_url')
%td
= @account.inbox_url
= fa_icon DeliveryFailureTracker.unavailable?(@account.inbox_url) ? 'times' : 'check'
%tr
%th= t('.targeted_reports')
%td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id)
%th= t('admin.accounts.shared_inbox_url')
%td
= @account.shared_inbox_url
= fa_icon DeliveryFailureTracker.unavailable?(@account.shared_inbox_url) ? 'times' : 'check'
%div{ style: 'overflow: hidden' }
%div{ style: 'float: right' }
@ -118,6 +150,8 @@
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
%div{ style: 'float: left' }
- if @account.local?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
- if @account.silenced?
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
- else
@ -132,58 +166,9 @@
- else
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
- if !@account.local? && @account.hub_url.present?
%hr.spacer/
%h3 OStatus
.table-wrapper
%table.table.inline-table
%tbody
%tr
%th= t('admin.accounts.feed_url')
%td= link_to @account.remote_url, @account.remote_url
%tr
%th= t('admin.accounts.push_subscription_expires')
%td
- if @account.subscribed?
%time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) }
= l @account.subscription_expires_at
- else
= t('admin.accounts.not_subscribed')
%tr
%th= t('admin.accounts.salmon_url')
%td= link_to @account.salmon_url, @account.salmon_url
%div{ style: 'overflow: hidden' }
%div{ style: 'float: right' }
= link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' if can?(:subscribe, @account)
- if @account.subscribed?
= link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account)
- if !@account.local? && @account.inbox_url.present?
%hr.spacer/
%h3 ActivityPub
.table-wrapper
%table.table.inline-table
%tbody
%tr
%th= t('admin.accounts.inbox_url')
%td= link_to @account.inbox_url, @account.inbox_url
%tr
%th= t('admin.accounts.outbox_url')
%td= link_to @account.outbox_url, @account.outbox_url
%tr
%th= t('admin.accounts.shared_inbox_url')
%td= link_to @account.shared_inbox_url, @account.shared_inbox_url
%tr
%th= t('admin.accounts.followers_url')
%td= link_to @account.followers_url, @account.followers_url
%hr.spacer/
- unless @warnings.empty?
= render @warnings
%hr.spacer/

@ -0,0 +1,28 @@
- content_for :page_title do
= t('admin.followers.title', acct: @account.acct)
.filters
.filter-subset
%strong= t('admin.accounts.location.title')
%ul
%li= link_to t('admin.accounts.location.local'), admin_account_followers_path(@account.id), class: 'selected'
.back-link{ style: 'flex: 1 1 auto; text-align: right' }
= link_to admin_account_path(@account.id) do
= fa_icon 'chevron-left fw'
= t('admin.followers.back_to_account')
%hr.spacer/
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.accounts.username')
%th= t('admin.accounts.role')
%th= t('admin.accounts.most_recent_ip')
%th= t('admin.accounts.most_recent_activity')
%th
%tbody
= render partial: 'admin/accounts/account', collection: @followers.map(&:account)
= paginate @followers

@ -11,7 +11,7 @@
%li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected'
.back-link{ style: 'flex: 1 1 auto; text-align: right' }
= link_to admin_account_path(@account.id) do
%i.fa.fa-chevron-left.fa-fw
= fa_icon 'chevron-left fw'
= t('admin.statuses.back_to_account')
%hr.spacer/

@ -14,7 +14,7 @@
%tr
%td.column-cell.text-center.padded
%h1= t 'notification_mailer.digest.title'
%p.lead= t('notification_mailer.digest.body', since: l(@since.to_date, format: :short), instance: site_hostname)
%p.lead= t('notification_mailer.digest.body', since: l((@me.user_current_sign_in_at || @since).to_date, format: :short), instance: site_hostname)
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
%tbody
%tr

@ -1,6 +1,6 @@
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.digest.body', since: l(@since), instance: root_url) %>
<%= raw t('notification_mailer.digest.body', since: l(@me.user_current_sign_in_at || @since), instance: root_url) %>
<% @notifications.each do |notification| %>
* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.acct) %>

@ -16,6 +16,10 @@
%th= t('exports.follows')
%td= number_with_delimiter @export.total_follows
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%tr
%th= t('exports.lists')
%td= number_with_delimiter @export.total_lists
%td= table_link_to 'download', t('exports.csv'), settings_exports_lists_path(format: :csv)
%tr
%th= t('accounts.followers', count: @export.total_followers)
%td= number_with_delimiter @export.total_followers
@ -28,6 +32,10 @@
%th= t('exports.mutes')
%td= number_with_delimiter @export.total_mutes
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
%tr
%th= t('exports.domain_blocks')
%td= number_with_delimiter @export.total_domain_blocks
%td= table_link_to 'download', t('exports.csv'), settings_exports_domain_blocks_path(format: :csv)
%p.muted-hint= t('exports.archive_takeout.hint_html')

@ -55,6 +55,10 @@
%tbody
%tr
%td.button-primary
- if @resource.created_by_application
= link_to confirmation_url(@resource, confirmation_token: @token, redirect_to_app: 'true') do
%span= t 'devise.mailer.confirmation_instructions.action_with_app', app: @resource.created_by_application.name
- else
= link_to confirmation_url(@resource, confirmation_token: @token) do
%span= t 'devise.mailer.confirmation_instructions.action'

@ -4,7 +4,7 @@
<%= t 'devise.mailer.confirmation_instructions.explanation', host: site_hostname %>
=> <%= confirmation_url(@resource, confirmation_token: @token) %>
=> <%= confirmation_url(@resource, confirmation_token: @token, redirect_to_app: @resource.created_by_application ? 'true' : nil) %>
<%= strip_tags(t('devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: terms_url)) %>

@ -12,7 +12,7 @@ class ActivityPub::DistributionWorker
return if skip_distribution?
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[signed_payload, @account.id, inbox_url]
[payload, @account.id, inbox_url]
end
relay! if relayable?
@ -31,24 +31,35 @@ class ActivityPub::DistributionWorker
end
def inboxes
@inboxes ||= @account.followers.inboxes
# Deliver the status to all followers.
# If the status is a reply to another local status, also forward it to that
# status' authors' followers.
@inboxes ||= if @status.reply? && @status.thread.account.local? && @status.distributable?
@account.followers.or(@status.thread.account.followers).inboxes
else
@account.followers.inboxes
end
end
def signed_payload
@signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
end
def payload
@payload ||= ActiveModelSerializers::SerializableResource.new(
def unsigned_payload
ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).as_json
end
def payload
@payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
end
def relay!
ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
[signed_payload, @account.id, inbox_url]
[payload, @account.id, inbox_url]
end
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save