Merge commit '1483a3ddfe74e4fb81d87447a1781943eab86c60' into glitch-soc/merge-upstream
Conflicts: - `config/initializers/simple_form.rb`: Upstream added a new simple_form component, where we had an extra one. Kept both components.pull/2246/head
commit
d8b0a732aa
|
@ -81,6 +81,15 @@ module.exports = {
|
||||||
{ property: 'substring', message: 'Use .slice instead of .substring.' },
|
{ property: 'substring', message: 'Use .slice instead of .substring.' },
|
||||||
{ property: 'substr', message: 'Use .slice instead of .substr.' },
|
{ property: 'substr', message: 'Use .slice instead of .substr.' },
|
||||||
],
|
],
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
selector: 'Literal[value=/•/], JSXText[value=/•/]',
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
message: "Use '·' (middle dot) instead of '•' (bullet)",
|
||||||
|
},
|
||||||
|
],
|
||||||
'no-self-assign': 'off',
|
'no-self-assign': 'off',
|
||||||
'no-unused-expressions': 'error',
|
'no-unused-expressions': 'error',
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
|
|
@ -4,6 +4,11 @@ exclude:
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
- lib/templates/haml/scaffold/_form.html.haml
|
- lib/templates/haml/scaffold/_form.html.haml
|
||||||
|
|
||||||
|
require:
|
||||||
|
- ./lib/linter/haml_middle_dot.rb
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
AltText:
|
AltText:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
MiddleDot:
|
||||||
|
enabled: true
|
||||||
|
|
|
@ -11,6 +11,7 @@ require:
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-capybara
|
- rubocop-capybara
|
||||||
|
- ./lib/linter/rubocop_middle_dot
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 3.0 # Set to minimum supported version of CI
|
TargetRubyVersion: 3.0 # Set to minimum supported version of CI
|
||||||
|
@ -205,3 +206,6 @@ Style/TrailingCommaInArrayLiteral:
|
||||||
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
|
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
|
||||||
Style/TrailingCommaInHashLiteral:
|
Style/TrailingCommaInHashLiteral:
|
||||||
EnforcedStyleForMultiline: 'comma'
|
EnforcedStyleForMultiline: 'comma'
|
||||||
|
|
||||||
|
Style/MiddleDot:
|
||||||
|
Enabled: true
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -20,7 +20,7 @@ gem 'dotenv-rails', '~> 2.8'
|
||||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||||
gem 'fog-core', '<= 2.4.0'
|
gem 'fog-core', '<= 2.4.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b'
|
gem 'kt-paperclip', '~> 7.2'
|
||||||
gem 'blurhash', '~> 0.1'
|
gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
|
@ -60,7 +60,6 @@ gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
||||||
gem 'nokogiri', '~> 1.15'
|
gem 'nokogiri', '~> 1.15'
|
||||||
gem 'nsa', '~> 0.2'
|
|
||||||
gem 'oj', '~> 3.14'
|
gem 'oj', '~> 3.14'
|
||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
gem 'parslet'
|
gem 'parslet'
|
||||||
|
|
27
Gemfile.lock
27
Gemfile.lock
|
@ -7,18 +7,6 @@ GIT
|
||||||
hkdf (~> 0.2)
|
hkdf (~> 0.2)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
|
|
||||||
GIT
|
|
||||||
remote: https://github.com/kreeti/kt-paperclip.git
|
|
||||||
revision: 11abf222dc31bff71160a1d138b445214f434b2b
|
|
||||||
ref: 11abf222dc31bff71160a1d138b445214f434b2b
|
|
||||||
specs:
|
|
||||||
kt-paperclip (7.1.1)
|
|
||||||
activemodel (>= 4.2.0)
|
|
||||||
activesupport (>= 4.2.0)
|
|
||||||
marcel (~> 1.0.1)
|
|
||||||
mime-types
|
|
||||||
terrapin (~> 0.6.0)
|
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/mastodon/rails-settings-cached.git
|
remote: https://github.com/mastodon/rails-settings-cached.git
|
||||||
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
|
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
|
||||||
|
@ -380,6 +368,12 @@ GEM
|
||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.2)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.2)
|
kaminari-core (1.2.2)
|
||||||
|
kt-paperclip (7.2.0)
|
||||||
|
activemodel (>= 4.2.0)
|
||||||
|
activesupport (>= 4.2.0)
|
||||||
|
marcel (~> 1.0.1)
|
||||||
|
mime-types
|
||||||
|
terrapin (~> 0.6.0)
|
||||||
launchy (2.5.2)
|
launchy (2.5.2)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
letter_opener (1.8.1)
|
||||||
|
@ -442,11 +436,6 @@ GEM
|
||||||
nokogiri (1.15.2)
|
nokogiri (1.15.2)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
|
||||||
activesupport (>= 4.2, < 7)
|
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
||||||
sidekiq (>= 3.5)
|
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
|
||||||
oj (3.14.3)
|
oj (3.14.3)
|
||||||
omniauth (1.9.2)
|
omniauth (1.9.2)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
|
@ -682,7 +671,6 @@ GEM
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
stackprof (0.2.25)
|
stackprof (0.2.25)
|
||||||
statsd-ruby (1.5.0)
|
|
||||||
stoplight (3.0.1)
|
stoplight (3.0.1)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
strong_migrations (0.8.0)
|
strong_migrations (0.8.0)
|
||||||
|
@ -819,7 +807,7 @@ DEPENDENCIES
|
||||||
json-ld-preloaded (~> 3.2)
|
json-ld-preloaded (~> 3.2)
|
||||||
json-schema (~> 4.0)
|
json-schema (~> 4.0)
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kt-paperclip (~> 7.1)!
|
kt-paperclip (~> 7.2)
|
||||||
letter_opener (~> 1.8)
|
letter_opener (~> 1.8)
|
||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
|
@ -831,7 +819,6 @@ DEPENDENCIES
|
||||||
net-http (~> 0.3.2)
|
net-http (~> 0.3.2)
|
||||||
net-ldap (~> 0.18)
|
net-ldap (~> 0.18)
|
||||||
nokogiri (~> 1.15)
|
nokogiri (~> 1.15)
|
||||||
nsa (~> 0.2)
|
|
||||||
oj (~> 3.14)
|
oj (~> 3.14)
|
||||||
omniauth (~> 1.9)
|
omniauth (~> 1.9)
|
||||||
omniauth-cas (~> 2.0)
|
omniauth-cas (~> 2.0)
|
||||||
|
|
|
@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:title, :replies_policy)
|
params.permit(:title, :replies_policy, :exclusive)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,15 +11,15 @@ class BackupsController < ApplicationController
|
||||||
def download
|
def download
|
||||||
case Paperclip::Attachment.default_options[:storage]
|
case Paperclip::Attachment.default_options[:storage]
|
||||||
when :s3
|
when :s3
|
||||||
redirect_to @backup.dump.expiring_url(10)
|
redirect_to @backup.dump.expiring_url(10), allow_other_host: true
|
||||||
when :fog
|
when :fog
|
||||||
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
||||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
|
redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true
|
||||||
else
|
else
|
||||||
redirect_to full_asset_url(@backup.dump.url)
|
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
||||||
end
|
end
|
||||||
when :filesystem
|
when :filesystem
|
||||||
redirect_to full_asset_url(@backup.dump.url)
|
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,7 @@ export function normalizePollOptionTranslation(translation, poll) {
|
||||||
|
|
||||||
export function normalizeAnnouncement(announcement) {
|
export function normalizeAnnouncement(announcement) {
|
||||||
const normalAnnouncement = { ...announcement };
|
const normalAnnouncement = { ...announcement };
|
||||||
const emojiMap = makeEmojiMap.emojis(normalAnnouncement);
|
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
||||||
|
|
||||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
||||||
|
|
||||||
|
|
|
@ -151,10 +151,10 @@ export const createListFail = error => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
|
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
|
||||||
dispatch(updateListRequest(id));
|
dispatch(updateListRequest(id));
|
||||||
|
|
||||||
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
|
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
||||||
dispatch(updateListSuccess(data));
|
dispatch(updateListSuccess(data));
|
||||||
|
|
||||||
if (shouldReset) {
|
if (shouldReset) {
|
||||||
|
|
|
@ -67,7 +67,7 @@ class Explore extends PureComponent {
|
||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='scrollable scrollable--flex'>
|
<div className='scrollable scrollable--flex' data-nosnippet>
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<SearchResults />
|
<SearchResults />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||||
import { fetchList, deleteList, updateList } from 'mastodon/actions/lists';
|
import { fetchList, deleteList, updateList } from 'mastodon/actions/lists';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
@ -145,7 +147,13 @@ class ListTimeline extends PureComponent {
|
||||||
handleRepliesPolicyChange = ({ target }) => {
|
handleRepliesPolicyChange = ({ target }) => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
dispatch(updateList(id, undefined, false, target.value));
|
dispatch(updateList(id, undefined, false, undefined, target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onExclusiveToggle = ({ target }) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { id } = this.props.params;
|
||||||
|
dispatch(updateList(id, undefined, false, target.checked, undefined));
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -154,6 +162,7 @@ class ListTimeline extends PureComponent {
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const title = list ? list.get('title') : id;
|
const title = list ? list.get('title') : id;
|
||||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||||
|
const isExclusive = list ? list.get('exclusive') : undefined;
|
||||||
|
|
||||||
if (typeof list === 'undefined') {
|
if (typeof list === 'undefined') {
|
||||||
return (
|
return (
|
||||||
|
@ -191,6 +200,13 @@ class ListTimeline extends PureComponent {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='setting-toggle'>
|
||||||
|
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||||
|
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||||
|
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{ replies_policy !== undefined && (
|
{ replies_policy !== undefined && (
|
||||||
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
||||||
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
||||||
|
|
|
@ -121,7 +121,7 @@ class Onboarding extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='onboarding__steps'>
|
<div className='onboarding__steps'>
|
||||||
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
||||||
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Follow {count, plural, one {one person} other {# people}}' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own feed. Let's fill it with interesting people." />} />
|
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
|
||||||
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
|
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
|
||||||
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -217,7 +217,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
} else if (this.context.router) {
|
} else if (this.context.router) {
|
||||||
reblogLink = (
|
reblogLink = (
|
||||||
<>
|
<>
|
||||||
·
|
{' · '}
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||||
<Icon id={reblogIcon} />
|
<Icon id={reblogIcon} />
|
||||||
<span className='detailed-status__reblogs'>
|
<span className='detailed-status__reblogs'>
|
||||||
|
@ -229,7 +229,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
} else {
|
} else {
|
||||||
reblogLink = (
|
reblogLink = (
|
||||||
<>
|
<>
|
||||||
·
|
{' · '}
|
||||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
<Icon id={reblogIcon} />
|
<Icon id={reblogIcon} />
|
||||||
<span className='detailed-status__reblogs'>
|
<span className='detailed-status__reblogs'>
|
||||||
|
@ -263,7 +263,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
if (status.get('edited_at')) {
|
if (status.get('edited_at')) {
|
||||||
edited = (
|
edited = (
|
||||||
<>
|
<>
|
||||||
·
|
{' · '}
|
||||||
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -356,6 +356,7 @@
|
||||||
"lists.delete": "Delete list",
|
"lists.delete": "Delete list",
|
||||||
"lists.edit": "Edit list",
|
"lists.edit": "Edit list",
|
||||||
"lists.edit.submit": "Change title",
|
"lists.edit.submit": "Change title",
|
||||||
|
"lists.exclusive": "Hide these posts from home",
|
||||||
"lists.new.create": "Add list",
|
"lists.new.create": "Add list",
|
||||||
"lists.new.title_placeholder": "New list title",
|
"lists.new.title_placeholder": "New list title",
|
||||||
"lists.replies_policy.followed": "Any followed user",
|
"lists.replies_policy.followed": "Any followed user",
|
||||||
|
@ -460,8 +461,8 @@
|
||||||
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
|
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
|
||||||
"onboarding.start.skip": "Want to skip right ahead?",
|
"onboarding.start.skip": "Want to skip right ahead?",
|
||||||
"onboarding.start.title": "You've made it!",
|
"onboarding.start.title": "You've made it!",
|
||||||
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
|
"onboarding.steps.follow_people.body": "You curate your own home feed. Let's fill it with interesting people.",
|
||||||
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
|
"onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow",
|
||||||
"onboarding.steps.publish_status.body": "Say hello to the world.",
|
"onboarding.steps.publish_status.body": "Say hello to the world.",
|
||||||
"onboarding.steps.publish_status.title": "Make your first post",
|
"onboarding.steps.publish_status.title": "Make your first post",
|
||||||
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
|
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
|
||||||
|
|
|
@ -48,6 +48,7 @@ export const IntlProvider: React.FC<
|
||||||
locale={locale}
|
locale={locale}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
onError={onProviderError}
|
onError={onProviderError}
|
||||||
|
textComponent='span'
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -25,6 +25,7 @@ const initialState = ImmutableMap({
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
isChanged: false,
|
isChanged: false,
|
||||||
title: '',
|
title: '',
|
||||||
|
isExclusive: false,
|
||||||
|
|
||||||
accounts: ImmutableMap({
|
accounts: ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
|
@ -46,6 +47,7 @@ export default function listEditorReducer(state = initialState, action) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('listId', action.list.get('id'));
|
map.set('listId', action.list.get('id'));
|
||||||
map.set('title', action.list.get('title'));
|
map.set('title', action.list.get('title'));
|
||||||
|
map.set('isExclusive', action.list.get('is_exclusive'));
|
||||||
map.set('isSubmitting', false);
|
map.set('isSubmitting', false);
|
||||||
});
|
});
|
||||||
case LIST_EDITOR_TITLE_CHANGE:
|
case LIST_EDITOR_TITLE_CHANGE:
|
||||||
|
|
|
@ -3,11 +3,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
@media screen and (max-width: $no-gap-breakpoint) {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
|
@ -22,7 +19,6 @@
|
||||||
height: 130px;
|
height: 130px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: darken($ui-base-color, 12%);
|
background: darken($ui-base-color, 12%);
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -30,7 +26,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 600px) {
|
@media screen and (width <= 600px) {
|
||||||
|
@ -45,11 +40,6 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
|
|
||||||
@media screen and (max-width: $no-gap-breakpoint) {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
@ -137,6 +137,10 @@ code {
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
&.invited-by {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dimension::BaseDimension
|
class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dimension::BaseDimension
|
||||||
|
include Admin::Metrics::Dimension::QueryHelper
|
||||||
include LanguagesHelper
|
include LanguagesHelper
|
||||||
|
|
||||||
def self.with_params?
|
def self.with_params?
|
||||||
|
@ -14,19 +15,23 @@ class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dim
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def perform_query
|
def perform_query
|
||||||
sql = <<-SQL.squish
|
dimension_data_rows.map { |row| { key: row['username'], human_key: row['username'], value: row['value'].to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_array
|
||||||
|
[sql_query_string, { domain: params[:domain], limit: @limit }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_query_string
|
||||||
|
<<~SQL.squish
|
||||||
SELECT accounts.username, count(follows.*) AS value
|
SELECT accounts.username, count(follows.*) AS value
|
||||||
FROM accounts
|
FROM accounts
|
||||||
LEFT JOIN follows ON follows.target_account_id = accounts.id
|
LEFT JOIN follows ON follows.target_account_id = accounts.id
|
||||||
WHERE accounts.domain = $1
|
WHERE accounts.domain = :domain
|
||||||
GROUP BY accounts.id, follows.target_account_id
|
GROUP BY accounts.id, follows.target_account_id
|
||||||
ORDER BY value DESC
|
ORDER BY value DESC
|
||||||
LIMIT $2
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, @limit]])
|
|
||||||
|
|
||||||
rows.map { |row| { key: row['username'], human_key: row['username'], value: row['value'].to_s } }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def params
|
def params
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Dimension::BaseDimension
|
class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Dimension::BaseDimension
|
||||||
|
include Admin::Metrics::Dimension::QueryHelper
|
||||||
include LanguagesHelper
|
include LanguagesHelper
|
||||||
|
|
||||||
def self.with_params?
|
def self.with_params?
|
||||||
|
@ -14,21 +15,33 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def perform_query
|
def perform_query
|
||||||
sql = <<-SQL.squish
|
dimension_data_rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_array
|
||||||
|
[sql_query_string, { domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_query_string
|
||||||
|
<<~SQL.squish
|
||||||
SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value
|
SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value
|
||||||
FROM statuses
|
FROM statuses
|
||||||
INNER JOIN accounts ON accounts.id = statuses.account_id
|
INNER JOIN accounts ON accounts.id = statuses.account_id
|
||||||
WHERE accounts.domain = $1
|
WHERE accounts.domain = :domain
|
||||||
AND statuses.id BETWEEN $2 AND $3
|
AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id
|
||||||
AND statuses.reblog_of_id IS NULL
|
AND statuses.reblog_of_id IS NULL
|
||||||
GROUP BY COALESCE(statuses.language, 'und')
|
GROUP BY COALESCE(statuses.language, 'und')
|
||||||
ORDER BY count(*) DESC
|
ORDER BY count(*) DESC
|
||||||
LIMIT $4
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]])
|
def earliest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@start_at, with_random: false)
|
||||||
|
end
|
||||||
|
|
||||||
rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } }
|
def latest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@end_at, with_random: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def params
|
def params
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension::BaseDimension
|
class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension::BaseDimension
|
||||||
|
include Admin::Metrics::Dimension::QueryHelper
|
||||||
include LanguagesHelper
|
include LanguagesHelper
|
||||||
|
|
||||||
def key
|
def key
|
||||||
|
@ -10,18 +11,22 @@ class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension:
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def perform_query
|
def perform_query
|
||||||
sql = <<-SQL.squish
|
dimension_data_rows.map { |row| { key: row['locale'], human_key: standard_locale_name(row['locale']), value: row['value'].to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_array
|
||||||
|
[sql_query_string, { start_at: @start_at, end_at: @end_at, limit: @limit }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_query_string
|
||||||
|
<<~SQL.squish
|
||||||
SELECT locale, count(*) AS value
|
SELECT locale, count(*) AS value
|
||||||
FROM users
|
FROM users
|
||||||
WHERE current_sign_in_at BETWEEN $1 AND $2
|
WHERE current_sign_in_at BETWEEN :start_at AND :end_at
|
||||||
AND locale IS NOT NULL
|
AND locale IS NOT NULL
|
||||||
GROUP BY locale
|
GROUP BY locale
|
||||||
ORDER BY count(*) DESC
|
ORDER BY count(*) DESC
|
||||||
LIMIT $3
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]])
|
|
||||||
|
|
||||||
rows.map { |row| { key: row['locale'], human_key: standard_locale_name(row['locale']), value: row['value'].to_s } }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::Metrics::Dimension::QueryHelper
|
||||||
|
protected
|
||||||
|
|
||||||
|
def dimension_data_rows
|
||||||
|
ActiveRecord::Base.connection.select_all(sanitized_sql_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitized_sql_string
|
||||||
|
ActiveRecord::Base.sanitize_sql_array(sql_array)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::BaseDimension
|
class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::BaseDimension
|
||||||
|
include Admin::Metrics::Dimension::QueryHelper
|
||||||
|
|
||||||
def key
|
def key
|
||||||
'servers'
|
'servers'
|
||||||
end
|
end
|
||||||
|
@ -8,18 +10,30 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def perform_query
|
def perform_query
|
||||||
sql = <<-SQL.squish
|
dimension_data_rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_array
|
||||||
|
[sql_query_string, { earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_query_string
|
||||||
|
<<~SQL.squish
|
||||||
SELECT accounts.domain, count(*) AS value
|
SELECT accounts.domain, count(*) AS value
|
||||||
FROM statuses
|
FROM statuses
|
||||||
INNER JOIN accounts ON accounts.id = statuses.account_id
|
INNER JOIN accounts ON accounts.id = statuses.account_id
|
||||||
WHERE statuses.id BETWEEN $1 AND $2
|
WHERE statuses.id BETWEEN :earliest_status_id AND :latest_status_id
|
||||||
GROUP BY accounts.domain
|
GROUP BY accounts.domain
|
||||||
ORDER BY count(*) DESC
|
ORDER BY count(*) DESC
|
||||||
LIMIT $3
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, Mastodon::Snowflake.id_at(@start_at)], [nil, Mastodon::Snowflake.id_at(@end_at)], [nil, @limit]])
|
def earliest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@start_at)
|
||||||
|
end
|
||||||
|
|
||||||
rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } }
|
def latest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@end_at)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::BaseDimension
|
class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::BaseDimension
|
||||||
|
include Admin::Metrics::Dimension::QueryHelper
|
||||||
|
|
||||||
def key
|
def key
|
||||||
'sources'
|
'sources'
|
||||||
end
|
end
|
||||||
|
@ -8,18 +10,22 @@ class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::B
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def perform_query
|
def perform_query
|
||||||
sql = <<-SQL.squish
|
dimension_data_rows.map { |row| { key: row['name'] || 'web', human_key: row['name'] || I18n.t('admin.dashboard.website'), value: row['value'].to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_array
|
||||||
|
[sql_query_string, { start_at: @start_at, end_at: @end_at, limit: @limit }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_query_string
|
||||||
|
<<~SQL.squish
|
||||||
SELECT oauth_applications.name, count(*) AS value
|
SELECT oauth_applications.name, count(*) AS value
|
||||||
FROM users
|
FROM users
|
||||||
LEFT JOIN oauth_applications ON oauth_applications.id = users.created_by_application_id
|
LEFT JOIN oauth_applications ON oauth_applications.id = users.created_by_application_id
|
||||||
WHERE users.created_at BETWEEN $1 AND $2
|
WHERE users.created_at BETWEEN :start_at AND :end_at
|
||||||
GROUP BY oauth_applications.name
|
GROUP BY oauth_applications.name
|
||||||
ORDER BY count(*) DESC
|
ORDER BY count(*) DESC
|
||||||
LIMIT $3
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]])
|
|
||||||
|
|
||||||
rows.map { |row| { key: row['name'] || 'web', human_key: row['name'] || I18n.t('admin.dashboard.website'), value: row['value'].to_s } }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimension::BaseDimension
|
class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimension::BaseDimension
|
||||||
|
include Admin::Metrics::Dimension::QueryHelper
|
||||||
include LanguagesHelper
|
include LanguagesHelper
|
||||||
|
|
||||||
def self.with_params?
|
def self.with_params?
|
||||||
|
@ -14,20 +15,36 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def perform_query
|
def perform_query
|
||||||
sql = <<-SQL.squish
|
dimension_data_rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_array
|
||||||
|
[sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_query_string
|
||||||
|
<<~SQL.squish
|
||||||
SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value
|
SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value
|
||||||
FROM statuses
|
FROM statuses
|
||||||
INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id
|
INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id
|
||||||
WHERE statuses_tags.tag_id = $1
|
WHERE statuses_tags.tag_id = :tag_id
|
||||||
AND statuses.id BETWEEN $2 AND $3
|
AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id
|
||||||
GROUP BY COALESCE(statuses.language, 'und')
|
GROUP BY COALESCE(statuses.language, 'und')
|
||||||
ORDER BY count(*) DESC
|
ORDER BY count(*) DESC
|
||||||
LIMIT $4
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]])
|
def tag_id
|
||||||
|
params[:id]
|
||||||
|
end
|
||||||
|
|
||||||
rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } }
|
def earliest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@start_at, with_random: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@end_at, with_random: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def params
|
def params
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension::BaseDimension
|
class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension::BaseDimension
|
||||||
|
include Admin::Metrics::Dimension::QueryHelper
|
||||||
|
|
||||||
def self.with_params?
|
def self.with_params?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -12,21 +14,37 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def perform_query
|
def perform_query
|
||||||
sql = <<-SQL.squish
|
dimension_data_rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_array
|
||||||
|
[sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_query_string
|
||||||
|
<<-SQL.squish
|
||||||
SELECT accounts.domain, count(*) AS value
|
SELECT accounts.domain, count(*) AS value
|
||||||
FROM statuses
|
FROM statuses
|
||||||
INNER JOIN accounts ON accounts.id = statuses.account_id
|
INNER JOIN accounts ON accounts.id = statuses.account_id
|
||||||
INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id
|
INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id
|
||||||
WHERE statuses_tags.tag_id = $1
|
WHERE statuses_tags.tag_id = :tag_id
|
||||||
AND statuses.id BETWEEN $2 AND $3
|
AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id
|
||||||
GROUP BY accounts.domain
|
GROUP BY accounts.domain
|
||||||
ORDER BY count(*) DESC
|
ORDER BY count(*) DESC
|
||||||
LIMIT $4
|
LIMIT :limit
|
||||||
SQL
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]])
|
def tag_id
|
||||||
|
params[:id]
|
||||||
|
end
|
||||||
|
|
||||||
rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } }
|
def earliest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@start_at, with_random: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_status_id
|
||||||
|
Mastodon::Snowflake.id_at(@end_at, with_random: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def params
|
def params
|
||||||
|
|
|
@ -40,9 +40,9 @@ class FeedManager
|
||||||
def filter?(timeline_type, status, receiver)
|
def filter?(timeline_type, status, receiver)
|
||||||
case timeline_type
|
case timeline_type
|
||||||
when :home
|
when :home
|
||||||
filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]))
|
filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]), :home)
|
||||||
when :list
|
when :list
|
||||||
filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]))
|
filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list)
|
||||||
when :mentions
|
when :mentions
|
||||||
filter_from_mentions?(status, receiver.id)
|
filter_from_mentions?(status, receiver.id)
|
||||||
when :direct
|
when :direct
|
||||||
|
@ -401,9 +401,10 @@ class FeedManager
|
||||||
# @param [Integer] receiver_id
|
# @param [Integer] receiver_id
|
||||||
# @param [Hash] crutches
|
# @param [Hash] crutches
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def filter_from_home?(status, receiver_id, crutches)
|
def filter_from_home?(status, receiver_id, crutches, timeline_type = :home)
|
||||||
return false if receiver_id == status.account_id
|
return false if receiver_id == status.account_id
|
||||||
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||||
|
return true if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present?
|
||||||
return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
|
return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
|
||||||
|
|
||||||
check_for_blocks = crutches[:active_mentions][status.id] || []
|
check_for_blocks = crutches[:active_mentions][status.id] || []
|
||||||
|
@ -603,6 +604,8 @@ class FeedManager
|
||||||
arr
|
arr
|
||||||
end
|
end
|
||||||
|
|
||||||
|
lists = List.where(account_id: receiver_id, exclusive: true)
|
||||||
|
|
||||||
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true)
|
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true)
|
||||||
crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
|
crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
|
||||||
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
|
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
|
||||||
|
@ -610,6 +613,7 @@ class FeedManager
|
||||||
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
|
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
|
||||||
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
|
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
|
||||||
crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
|
crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
|
||||||
|
crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
|
||||||
|
|
||||||
crutches
|
crutches
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# replies_policy :integer default("list"), not null
|
# replies_policy :integer default("list"), not null
|
||||||
|
# exclusive :boolean default(FALSE)
|
||||||
#
|
#
|
||||||
|
|
||||||
class List < ApplicationRecord
|
class List < ApplicationRecord
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::ListSerializer < ActiveModel::Serializer
|
class REST::ListSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :title, :replies_policy
|
attributes :id, :title, :replies_policy, :exclusive
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
|
|
@ -9,6 +9,6 @@
|
||||||
|
|
||||||
- if email_domain_block.parent.present?
|
- if email_domain_block.parent.present?
|
||||||
= t('admin.email_domain_blocks.resolved_through_html', domain: content_tag(:samp, email_domain_block.parent.domain))
|
= t('admin.email_domain_blocks.resolved_through_html', domain: content_tag(:samp, email_domain_block.parent.domain))
|
||||||
•
|
·
|
||||||
|
|
||||||
= t('admin.email_domain_blocks.attempts_over_week', count: email_domain_block.history.reduce(0) { |sum, day| sum + day.accounts })
|
= t('admin.email_domain_blocks.attempts_over_week', count: email_domain_block.history.reduce(0) { |sum, day| sum + day.accounts })
|
||||||
|
|
|
@ -17,11 +17,11 @@
|
||||||
|
|
||||||
%br/
|
%br/
|
||||||
|
|
||||||
= f.object.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ')
|
= f.object.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ')
|
||||||
- if f.object.public_comment.present?
|
- if f.object.public_comment.present?
|
||||||
•
|
·
|
||||||
= f.object.public_comment
|
= f.object.public_comment
|
||||||
- if existing_relationships
|
- if existing_relationships
|
||||||
•
|
·
|
||||||
= fa_icon 'warning fw'
|
= fa_icon 'warning fw'
|
||||||
= t('admin.export_domain_blocks.import.existing_relationships_warning')
|
= t('admin.export_domain_blocks.import.existing_relationships_warning')
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
%small
|
%small
|
||||||
- if instance.domain_block
|
- if instance.domain_block
|
||||||
= instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ')
|
= instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ')
|
||||||
- elsif instance.domain_allow
|
- elsif instance.domain_allow
|
||||||
= t('admin.accounts.whitelisted')
|
= t('admin.accounts.whitelisted')
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
%td= @instance.domain_block.public_comment
|
%td= @instance.domain_block.public_comment
|
||||||
%tr
|
%tr
|
||||||
%th= t('admin.instances.content_policies.policy')
|
%th= t('admin.instances.content_policies.policy')
|
||||||
%td= @instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ')
|
%td= @instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ')
|
||||||
|
|
||||||
= link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@instance.domain_block), class: 'button'
|
= link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@instance.domain_block), class: 'button'
|
||||||
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
|
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
.pending-account__header
|
.pending-account__header
|
||||||
%samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}")
|
%samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}")
|
||||||
- if ip_block.comment.present?
|
- if ip_block.comment.present?
|
||||||
•
|
·
|
||||||
= ip_block.comment
|
= ip_block.comment
|
||||||
%br/
|
%br/
|
||||||
= t("simple_form.labels.ip_block.severities.#{ip_block.severity}")
|
= t("simple_form.labels.ip_block.severities.#{ip_block.severity}")
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
= t('admin.roles.everyone_full_description_html')
|
= t('admin.roles.everyone_full_description_html')
|
||||||
- else
|
- else
|
||||||
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id)
|
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id)
|
||||||
•
|
·
|
||||||
%abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
|
%abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
|
||||||
%div
|
%div
|
||||||
= table_link_to 'pencil', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role)
|
= table_link_to 'pencil', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role)
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
|
= f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
|
||||||
= f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
|
= f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }, hint: false, warning_hint: t('simple_form.hints.form_admin_settings.content_cache_retention_period')
|
||||||
= f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
|
= f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
|
|
|
@ -10,21 +10,21 @@
|
||||||
|
|
||||||
- if preview_card.provider_name.present?
|
- if preview_card.provider_name.present?
|
||||||
= preview_card.provider_name
|
= preview_card.provider_name
|
||||||
•
|
·
|
||||||
|
|
||||||
- if preview_card.language.present?
|
- if preview_card.language.present?
|
||||||
= standard_locale_name(preview_card.language)
|
= standard_locale_name(preview_card.language)
|
||||||
•
|
·
|
||||||
|
|
||||||
= t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts })
|
= t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts })
|
||||||
|
|
||||||
- if preview_card.trend.allowed?
|
- if preview_card.trend.allowed?
|
||||||
•
|
·
|
||||||
%abbr{ title: t('admin.trends.tags.current_score', score: preview_card.trend.score) }= t('admin.trends.tags.trending_rank', rank: preview_card.trend.rank)
|
%abbr{ title: t('admin.trends.tags.current_score', score: preview_card.trend.score) }= t('admin.trends.tags.trending_rank', rank: preview_card.trend.rank)
|
||||||
|
|
||||||
- if preview_card.decaying?
|
- if preview_card.decaying?
|
||||||
•
|
·
|
||||||
= t('admin.trends.tags.peaked_on_and_decaying', date: l(preview_card.max_score_at.to_date, format: :short))
|
= t('admin.trends.tags.peaked_on_and_decaying', date: l(preview_card.max_score_at.to_date, format: :short))
|
||||||
- elsif preview_card.requires_review?
|
- elsif preview_card.requires_review?
|
||||||
•
|
·
|
||||||
= t('admin.trends.pending_review')
|
= t('admin.trends.pending_review')
|
||||||
|
|
|
@ -17,17 +17,17 @@
|
||||||
= t('admin.trends.statuses.shared_by', count: status.reblogs_count + status.favourites_count, friendly_count: friendly_number_to_human(status.reblogs_count + status.favourites_count))
|
= t('admin.trends.statuses.shared_by', count: status.reblogs_count + status.favourites_count, friendly_count: friendly_number_to_human(status.reblogs_count + status.favourites_count))
|
||||||
|
|
||||||
- if status.account.domain.present?
|
- if status.account.domain.present?
|
||||||
•
|
·
|
||||||
= status.account.domain
|
= status.account.domain
|
||||||
- if status.language.present?
|
- if status.language.present?
|
||||||
•
|
·
|
||||||
= standard_locale_name(status.language)
|
= standard_locale_name(status.language)
|
||||||
- if status.trendable? && !status.account.discoverable?
|
- if status.trendable? && !status.account.discoverable?
|
||||||
•
|
·
|
||||||
= t('admin.trends.statuses.not_discoverable')
|
= t('admin.trends.statuses.not_discoverable')
|
||||||
- if status.trend.allowed?
|
- if status.trend.allowed?
|
||||||
•
|
·
|
||||||
%abbr{ title: t('admin.trends.tags.current_score', score: status.trend.score) }= t('admin.trends.tags.trending_rank', rank: status.trend.rank)
|
%abbr{ title: t('admin.trends.tags.current_score', score: status.trend.score) }= t('admin.trends.tags.trending_rank', rank: status.trend.rank)
|
||||||
- elsif status.requires_review?
|
- elsif status.requires_review?
|
||||||
•
|
·
|
||||||
= t('admin.trends.pending_review')
|
= t('admin.trends.pending_review')
|
||||||
|
|
|
@ -13,12 +13,12 @@
|
||||||
= t('admin.trends.tags.used_by_over_week', count: tag.history.reduce(0) { |sum, day| sum + day.accounts })
|
= t('admin.trends.tags.used_by_over_week', count: tag.history.reduce(0) { |sum, day| sum + day.accounts })
|
||||||
|
|
||||||
- if tag.trendable? && (rank = Trends.tags.rank(tag.id))
|
- if tag.trendable? && (rank = Trends.tags.rank(tag.id))
|
||||||
•
|
·
|
||||||
%abbr{ title: t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id)) }= t('admin.trends.tags.trending_rank', rank: rank + 1)
|
%abbr{ title: t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id)) }= t('admin.trends.tags.trending_rank', rank: rank + 1)
|
||||||
|
|
||||||
- if tag.decaying?
|
- if tag.decaying?
|
||||||
•
|
·
|
||||||
= t('admin.trends.tags.peaked_on_and_decaying', date: l(tag.max_score_at.to_date, format: :short))
|
= t('admin.trends.tags.peaked_on_and_decaying', date: l(tag.max_score_at.to_date, format: :short))
|
||||||
- elsif tag.requires_review?
|
- elsif tag.requires_review?
|
||||||
•
|
·
|
||||||
= t('admin.trends.pending_review')
|
= t('admin.trends.pending_review')
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
- else
|
- else
|
||||||
%span.negative-hint= t('admin.webhooks.disabled')
|
%span.negative-hint= t('admin.webhooks.disabled')
|
||||||
|
|
||||||
•
|
·
|
||||||
|
|
||||||
%abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size)
|
%abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<%= raw t('admin_mailer.new_trends.new_trending_links.title') %>
|
<%= raw t('admin_mailer.new_trends.new_trending_links.title') %>
|
||||||
|
|
||||||
<% @links.each do |link| %>
|
<% @links.each do |link| %>
|
||||||
- <%= link.title %> • <%= link.url %>
|
- <%= link.title %> · <%= link.url %>
|
||||||
<%= standard_locale_name(link.language) %> • <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: link.trend.score.round(2)) %>
|
<%= standard_locale_name(link.language) %> · <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> · <%= t('admin.trends.tags.current_score', score: link.trend.score.round(2)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= raw t('application_mailer.view')%> <%= admin_trends_links_url %>
|
<%= raw t('application_mailer.view')%> <%= admin_trends_links_url %>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<% @statuses.each do |status| %>
|
<% @statuses.each do |status| %>
|
||||||
- <%= ActivityPub::TagManager.instance.url_for(status) %>
|
- <%= ActivityPub::TagManager.instance.url_for(status) %>
|
||||||
<%= standard_locale_name(status.language) %> • <%= raw t('admin.trends.tags.current_score', score: status.trend.score.round(2)) %>
|
<%= standard_locale_name(status.language) %> · <%= raw t('admin.trends.tags.current_score', score: status.trend.score.round(2)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %>
|
<%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<% @tags.each do |tag| %>
|
<% @tags.each do |tag| %>
|
||||||
- #<%= tag.display_name %>
|
- #<%= tag.display_name %>
|
||||||
<%= raw t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %>
|
<%= raw t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> · <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if @lowest_trending_tag %>
|
<% if @lowest_trending_tag %>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account)
|
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account)
|
||||||
|
- compact ||= false
|
||||||
|
|
||||||
.card.h-card
|
.card.h-card
|
||||||
= link_to account_url, target: '_blank', rel: 'noopener noreferrer' do
|
= link_to account_url, target: '_blank', rel: 'noopener noreferrer' do
|
||||||
|
- unless compact
|
||||||
.card__img
|
.card__img
|
||||||
= image_tag account.header.url, alt: ''
|
= image_tag account.header.url, alt: ''
|
||||||
.card__bar
|
.card__bar
|
||||||
|
|
|
@ -7,6 +7,12 @@
|
||||||
.simple_form
|
.simple_form
|
||||||
= render 'auth/shared/progress', stage: 'rules'
|
= render 'auth/shared/progress', stage: 'rules'
|
||||||
|
|
||||||
|
- if @invite.present? && @invite.autofollow?
|
||||||
|
%h1.title= t('auth.rules.title_invited')
|
||||||
|
%p.lead.invited-by= t('auth.rules.invited_by', domain: site_hostname)
|
||||||
|
= render 'application/card', account: @invite.user.account, compact: true
|
||||||
|
%p.lead= t('auth.rules.preamble_invited', domain: site_hostname)
|
||||||
|
- else
|
||||||
%h1.title= t('auth.rules.title')
|
%h1.title= t('auth.rules.title')
|
||||||
%p.lead= t('auth.rules.preamble', domain: site_hostname)
|
%p.lead= t('auth.rules.preamble', domain: site_hostname)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
- else
|
- else
|
||||||
= t('doorkeeper.authorized_applications.index.never_used')
|
= t('doorkeeper.authorized_applications.index.never_used')
|
||||||
|
|
||||||
•
|
·
|
||||||
|
|
||||||
= t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date))
|
= t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date))
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,14 @@ module RecommendedComponent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
module WarningHintComponent
|
||||||
|
def warning_hint(_wrapper_options = nil)
|
||||||
|
@warning_hint ||= begin
|
||||||
|
options[:warning_hint].to_s.html_safe if options[:warning_hint].present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
module GlitchOnlyComponent
|
module GlitchOnlyComponent
|
||||||
def glitch_only(_wrapper_options = nil)
|
def glitch_only(_wrapper_options = nil)
|
||||||
return unless options[:glitch_only]
|
return unless options[:glitch_only]
|
||||||
|
@ -30,6 +38,7 @@ end
|
||||||
|
|
||||||
SimpleForm.include_component(AppendComponent)
|
SimpleForm.include_component(AppendComponent)
|
||||||
SimpleForm.include_component(RecommendedComponent)
|
SimpleForm.include_component(RecommendedComponent)
|
||||||
|
SimpleForm.include_component(WarningHintComponent)
|
||||||
SimpleForm.include_component(GlitchOnlyComponent)
|
SimpleForm.include_component(GlitchOnlyComponent)
|
||||||
|
|
||||||
SimpleForm.setup do |config|
|
SimpleForm.setup do |config|
|
||||||
|
@ -112,6 +121,7 @@ SimpleForm.setup do |config|
|
||||||
b.use :html5
|
b.use :html5
|
||||||
b.use :label
|
b.use :label
|
||||||
b.use :hint, wrap_with: { tag: :span, class: :hint }
|
b.use :hint, wrap_with: { tag: :span, class: :hint }
|
||||||
|
b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] }
|
||||||
b.use :input, wrap_with: { tag: :div, class: :label_input }
|
b.use :input, wrap_with: { tag: :div, class: :label_input }
|
||||||
b.use :error, wrap_with: { tag: :span, class: :error }
|
b.use :error, wrap_with: { tag: :span, class: :error }
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
if ENV['STATSD_ADDR'].present?
|
|
||||||
host, port = ENV['STATSD_ADDR'].split(':')
|
|
||||||
|
|
||||||
$statsd = ::Statsd.new(host, port)
|
|
||||||
$statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') }
|
|
||||||
|
|
||||||
::NSA.inform_statsd($statsd) do |informant|
|
|
||||||
informant.collect(:action_controller, :web)
|
|
||||||
informant.collect(:active_record, :db)
|
|
||||||
informant.collect(:active_support_cache, :cache)
|
|
||||||
informant.collect(:sidekiq, :sidekiq)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1031,8 +1031,11 @@ en:
|
||||||
rules:
|
rules:
|
||||||
accept: Accept
|
accept: Accept
|
||||||
back: Back
|
back: Back
|
||||||
|
invited_by: 'You can join %{domain} thanks to the invitation you have received from:'
|
||||||
preamble: These are set and enforced by the %{domain} moderators.
|
preamble: These are set and enforced by the %{domain} moderators.
|
||||||
|
preamble_invited: Before you proceed, please consider the ground rules set by the moderators of %{domain}.
|
||||||
title: Some ground rules.
|
title: Some ground rules.
|
||||||
|
title_invited: You've been invited.
|
||||||
security: Security
|
security: Security
|
||||||
set_new_password: Set new password
|
set_new_password: Set new password
|
||||||
setup:
|
setup:
|
||||||
|
|
|
@ -78,7 +78,7 @@ en:
|
||||||
backups_retention_period: Keep generated user archives for the specified number of days.
|
backups_retention_period: Keep generated user archives for the specified number of days.
|
||||||
bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations.
|
bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations.
|
||||||
closed_registrations_message: Displayed when sign-ups are closed
|
closed_registrations_message: Displayed when sign-ups are closed
|
||||||
content_cache_retention_period: Posts from other servers will be deleted after the specified number of days when set to a positive value. This may be irreversible.
|
content_cache_retention_period: All posts and boosts from other servers will be deleted after the specified number of days. Some posts may not be recoverable. All related bookmarks, favourites and boosts will also be lost and impossible to undo.
|
||||||
custom_css: You can apply custom styles on the web version of Mastodon.
|
custom_css: You can apply custom styles on the web version of Mastodon.
|
||||||
mascot: Overrides the illustration in the advanced web interface.
|
mascot: Overrides the illustration in the advanced web interface.
|
||||||
media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand.
|
media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand.
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
console.error("The localisation functionality has been refactored, please see the Localisation section in the development documentation (https://docs.joinmastodon.org/dev/code/#localizations)");
|
|
||||||
|
|
||||||
process.exit(1);
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddExclusiveToLists < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :lists, :exclusive, :boolean, null: false, default: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2023_05_31_154811) do
|
ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -567,6 +567,7 @@ ActiveRecord::Schema.define(version: 2023_05_31_154811) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "replies_policy", default: 0, null: false
|
t.integer "replies_policy", default: 0, null: false
|
||||||
|
t.boolean "exclusive", default: false
|
||||||
t.index ["account_id"], name: "index_lists_on_account_id"
|
t.index ["account_id"], name: "index_lists_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module HamlLint
|
||||||
|
# Bans the usage of “•” (bullet) in HTML/HAML in favor of “·” (middle dot) in anything that will end up as a text node. (including string literals in Ruby code)
|
||||||
|
class Linter::MiddleDot < Linter
|
||||||
|
include LinterRegistry
|
||||||
|
|
||||||
|
# rubocop:disable Style/MiddleDot
|
||||||
|
BULLET = '•'
|
||||||
|
# rubocop:enable Style/MiddleDot
|
||||||
|
MIDDLE_DOT = '·'
|
||||||
|
MESSAGE = "Use '#{MIDDLE_DOT}' (middle dot) instead of '#{BULLET}' (bullet)".freeze
|
||||||
|
|
||||||
|
def visit_plain(node)
|
||||||
|
return unless node.text.include?(BULLET)
|
||||||
|
|
||||||
|
record_lint(node, MESSAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit_script(node)
|
||||||
|
return unless node.script.include?(BULLET)
|
||||||
|
|
||||||
|
record_lint(node, MESSAGE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module RuboCop
|
||||||
|
module Cop
|
||||||
|
module Style
|
||||||
|
# Bans the usage of “•” (bullet) in HTML/HAML in favor of “·” (middle dot) in string literals
|
||||||
|
class MiddleDot < Base
|
||||||
|
extend AutoCorrector
|
||||||
|
extend Util
|
||||||
|
|
||||||
|
# rubocop:disable Style/MiddleDot
|
||||||
|
BULLET = '•'
|
||||||
|
# rubocop:enable Style/MiddleDot
|
||||||
|
MIDDLE_DOT = '·'
|
||||||
|
MESSAGE = "Use '#{MIDDLE_DOT}' (middle dot) instead of '#{BULLET}' (bullet)".freeze
|
||||||
|
|
||||||
|
def on_str(node)
|
||||||
|
# Constants like __FILE__ are handled as strings,
|
||||||
|
# but don't respond to begin.
|
||||||
|
return unless node.loc.respond_to?(:begin) && node.loc.begin
|
||||||
|
|
||||||
|
return unless node.value.include?(BULLET)
|
||||||
|
|
||||||
|
add_offense(node, message: MESSAGE) do |corrector|
|
||||||
|
corrector.replace(node, node.source.gsub(BULLET, MIDDLE_DOT))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,7 +21,6 @@
|
||||||
"lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"",
|
"lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"",
|
||||||
"lint:yml": "prettier --check \"**/*.{yaml,yml}\"",
|
"lint:yml": "prettier --check \"**/*.{yaml,yml}\"",
|
||||||
"lint": "yarn lint:js && yarn lint:json && yarn lint:sass && yarn lint:yml",
|
"lint": "yarn lint:js && yarn lint:json && yarn lint:sass && yarn lint:yml",
|
||||||
"manage:translations": "node ./config/webpack/translationRunner.js",
|
|
||||||
"postversion": "git push --tags",
|
"postversion": "git push --tags",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"start": "node ./streaming/index.js",
|
"start": "node ./streaming/index.js",
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::InstanceAccountsDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::InstanceLanguagesDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::LanguagesDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::ServersDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::SoftwareVersionsDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::SourcesDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::SpaceUsageDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::TagLanguagesDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::Metrics::Dimension::TagServersDimension do
|
||||||
|
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
|
let(:start_at) { 2.days.ago }
|
||||||
|
let(:end_at) { Time.now.utc }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
|
describe '#data' do
|
||||||
|
it 'runs data query without error' do
|
||||||
|
expect { dimension.data }.to_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,6 +26,7 @@ RSpec.describe FeedManager do
|
||||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
|
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
|
||||||
let(:jeff) { Fabricate(:account, username: 'jeff') }
|
let(:jeff) { Fabricate(:account, username: 'jeff') }
|
||||||
|
let(:list) { Fabricate(:list, account: alice) }
|
||||||
|
|
||||||
context 'with home feed' do
|
context 'with home feed' do
|
||||||
it 'returns false for followee\'s status' do
|
it 'returns false for followee\'s status' do
|
||||||
|
@ -160,6 +161,42 @@ RSpec.describe FeedManager do
|
||||||
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
||||||
expect(FeedManager.instance.filter?(:home, status, alice)).to be false
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns true for post from followee on exclusive list' do
|
||||||
|
list.exclusive = true
|
||||||
|
alice.follow!(bob)
|
||||||
|
list.accounts << bob
|
||||||
|
allow(List).to receive(:where).and_return(list)
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for reblog from followee on exclusive list' do
|
||||||
|
list.exclusive = true
|
||||||
|
alice.follow!(jeff)
|
||||||
|
list.accounts << jeff
|
||||||
|
allow(List).to receive(:where).and_return(list)
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
|
expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for post from followee on non-exclusive list' do
|
||||||
|
list.exclusive = false
|
||||||
|
alice.follow!(bob)
|
||||||
|
list.accounts << bob
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for reblog from followee on non-exclusive list' do
|
||||||
|
list.exclusive = false
|
||||||
|
alice.follow!(jeff)
|
||||||
|
list.accounts << jeff
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
|
expect(FeedManager.instance.filter?(:home, reblog, alice)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with mentions feed' do
|
context 'with mentions feed' do
|
||||||
|
|
|
@ -998,4 +998,254 @@ describe Mastodon::CLI::Accounts do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#merge' do
|
||||||
|
shared_examples 'an account not found' do |acct|
|
||||||
|
it 'exits with an error message indicating that there is no such account' do
|
||||||
|
expect { cli.invoke(:merge, arguments) }.to output(
|
||||||
|
a_string_including("No such account (#{acct})")
|
||||||
|
).to_stdout
|
||||||
|
.and raise_error(SystemExit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when "from_account" is not found' do
|
||||||
|
let(:to_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:arguments) { ['non_existent_username@domain.com', "#{to_account.username}@#{to_account.domain}"] }
|
||||||
|
|
||||||
|
it_behaves_like 'an account not found', 'non_existent_username@domain.com'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when "from_account" is a local account' do
|
||||||
|
let(:from_account) { Fabricate(:account, domain: nil, username: 'bob') }
|
||||||
|
let(:to_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:arguments) { [from_account.username, "#{to_account.username}@#{to_account.domain}"] }
|
||||||
|
|
||||||
|
it_behaves_like 'an account not found', 'bob'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when "to_account" is not found' do
|
||||||
|
let(:from_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:arguments) { ["#{from_account.username}@#{from_account.domain}", 'non_existent_username'] }
|
||||||
|
|
||||||
|
it_behaves_like 'an account not found', 'non_existent_username'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when "to_account" is local' do
|
||||||
|
let(:from_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:to_account) { Fabricate(:account, domain: nil, username: 'bob') }
|
||||||
|
let(:arguments) do
|
||||||
|
["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"]
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'an account not found', 'bob@'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when "from_account" and "to_account" public keys do not match' do
|
||||||
|
let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'from_account') }
|
||||||
|
let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'to_account') }
|
||||||
|
let(:arguments) do
|
||||||
|
["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"]
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account)
|
||||||
|
allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'exits with an error message indicating that the accounts do not have the same pub key' do
|
||||||
|
expect { cli.invoke(:merge, arguments) }.to output(
|
||||||
|
a_string_including("Accounts don't have the same public key, might not be duplicates!\nOverride with --force")
|
||||||
|
).to_stdout
|
||||||
|
.and raise_error(SystemExit)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with --force option' do
|
||||||
|
let(:options) { { force: true } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(to_account).to receive(:merge_with!)
|
||||||
|
allow(from_account).to receive(:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'merges "from_account" into "to_account"' do
|
||||||
|
cli.invoke(:merge, arguments, options)
|
||||||
|
|
||||||
|
expect(to_account).to have_received(:merge_with!).with(from_account).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes "from_account"' do
|
||||||
|
cli.invoke(:merge, arguments, options)
|
||||||
|
|
||||||
|
expect(from_account).to have_received(:destroy).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when "from_account" and "to_account" public keys match' do
|
||||||
|
let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'pub_key') }
|
||||||
|
let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'pub_key') }
|
||||||
|
let(:arguments) do
|
||||||
|
["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"]
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account)
|
||||||
|
allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account)
|
||||||
|
allow(to_account).to receive(:merge_with!)
|
||||||
|
allow(from_account).to receive(:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'merges "from_account" into "to_account"' do
|
||||||
|
cli.invoke(:merge, arguments)
|
||||||
|
|
||||||
|
expect(to_account).to have_received(:merge_with!).with(from_account).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes "from_account"' do
|
||||||
|
cli.invoke(:merge, arguments)
|
||||||
|
|
||||||
|
expect(from_account).to have_received(:destroy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#cull' do
|
||||||
|
let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) }
|
||||||
|
let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com') }
|
||||||
|
let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org') }
|
||||||
|
let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net') }
|
||||||
|
let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com') }
|
||||||
|
let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(DeleteAccountService).to receive(:new).and_return(delete_account_service)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no domain is specified' do
|
||||||
|
let(:scope) { Account.remote.where(protocol: :activitypub).partitioned }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(cli).to receive(:parallelize_with_progress).and_yield(tom)
|
||||||
|
.and_yield(bob)
|
||||||
|
.and_yield(gon)
|
||||||
|
.and_yield(ana)
|
||||||
|
.and_yield(tales)
|
||||||
|
.and_return([5, 3])
|
||||||
|
stub_request(:head, 'https://example.org/users/bob').to_return(status: 404)
|
||||||
|
stub_request(:head, 'https://example.net/users/gon').to_return(status: 410)
|
||||||
|
stub_request(:head, 'https://example.net/users/tales').to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes all inactive remote accounts that longer exist in the origin server' do
|
||||||
|
cli.cull
|
||||||
|
|
||||||
|
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
|
||||||
|
expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once
|
||||||
|
expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not delete any active remote account that still exists in the origin server' do
|
||||||
|
cli.cull
|
||||||
|
|
||||||
|
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
|
||||||
|
expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false)
|
||||||
|
expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false)
|
||||||
|
expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'touches inactive remote accounts that have not been deleted' do
|
||||||
|
allow(tales).to receive(:touch)
|
||||||
|
|
||||||
|
cli.cull
|
||||||
|
|
||||||
|
expect(tales).to have_received(:touch).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays the summary correctly' do
|
||||||
|
expect { cli.cull }.to output(
|
||||||
|
a_string_including('Visited 5 accounts, removed 3')
|
||||||
|
).to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a domain is specified' do
|
||||||
|
let(:domain) { 'example.net' }
|
||||||
|
let(:scope) { Account.remote.where(protocol: :activitypub, domain: domain).partitioned }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(cli).to receive(:parallelize_with_progress).and_yield(gon)
|
||||||
|
.and_yield(tales)
|
||||||
|
.and_return([2, 2])
|
||||||
|
stub_request(:head, 'https://example.net/users/gon').to_return(status: 410)
|
||||||
|
stub_request(:head, 'https://example.net/users/tales').to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes inactive remote accounts that longer exist in the specified domain' do
|
||||||
|
cli.cull(domain)
|
||||||
|
|
||||||
|
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
|
||||||
|
expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
|
||||||
|
expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays the summary correctly' do
|
||||||
|
expect { cli.cull }.to output(
|
||||||
|
a_string_including('Visited 2 accounts, removed 2')
|
||||||
|
).to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a domain is unavailable' do
|
||||||
|
shared_examples 'an unavailable domain' do
|
||||||
|
before do
|
||||||
|
allow(cli).to receive(:parallelize_with_progress).and_yield(tales).and_return([1, 0])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skips accounts from the unavailable domain' do
|
||||||
|
cli.cull
|
||||||
|
|
||||||
|
expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays the summary correctly' do
|
||||||
|
expect { cli.cull }.to output(
|
||||||
|
a_string_including("Visited 1 accounts, removed 0\nThe following domains were not available during the check:\n example.net")
|
||||||
|
).to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a connection timeout occurs' do
|
||||||
|
before do
|
||||||
|
stub_request(:head, 'https://example.net/users/tales').to_timeout
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'an unavailable domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a connection error occurs' do
|
||||||
|
before do
|
||||||
|
stub_request(:head, 'https://example.net/users/tales').to_raise(HTTP::ConnectionError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'an unavailable domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when an ssl error occurs' do
|
||||||
|
before do
|
||||||
|
stub_request(:head, 'https://example.net/users/tales').to_raise(OpenSSL::SSL::SSLError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'an unavailable domain'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a private network address error occurs' do
|
||||||
|
before do
|
||||||
|
stub_request(:head, 'https://example.net/users/tales').to_raise(Mastodon::PrivateNetworkAddressError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'an unavailable domain'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,9 +4,57 @@ require 'rails_helper'
|
||||||
require 'mastodon/cli/canonical_email_blocks'
|
require 'mastodon/cli/canonical_email_blocks'
|
||||||
|
|
||||||
describe Mastodon::CLI::CanonicalEmailBlocks do
|
describe Mastodon::CLI::CanonicalEmailBlocks do
|
||||||
|
let(:cli) { described_class.new }
|
||||||
|
|
||||||
describe '.exit_on_failure?' do
|
describe '.exit_on_failure?' do
|
||||||
it 'returns true' do
|
it 'returns true' do
|
||||||
expect(described_class.exit_on_failure?).to be true
|
expect(described_class.exit_on_failure?).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#find' do
|
||||||
|
let(:arguments) { ['user@example.com'] }
|
||||||
|
|
||||||
|
context 'when a block is present' do
|
||||||
|
before { Fabricate(:canonical_email_block, email: 'user@example.com') }
|
||||||
|
|
||||||
|
it 'announces the presence of the block' do
|
||||||
|
expect { cli.invoke(:find, arguments) }.to output(
|
||||||
|
a_string_including('user@example.com is blocked')
|
||||||
|
).to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a block is not present' do
|
||||||
|
it 'announces the absence of the block' do
|
||||||
|
expect { cli.invoke(:find, arguments) }.to output(
|
||||||
|
a_string_including('user@example.com is not blocked')
|
||||||
|
).to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#remove' do
|
||||||
|
let(:arguments) { ['user@example.com'] }
|
||||||
|
|
||||||
|
context 'when a block is present' do
|
||||||
|
before { Fabricate(:canonical_email_block, email: 'user@example.com') }
|
||||||
|
|
||||||
|
it 'removes the block' do
|
||||||
|
expect { cli.invoke(:remove, arguments) }.to output(
|
||||||
|
a_string_including('Unblocked user@example.com')
|
||||||
|
).to_stdout
|
||||||
|
|
||||||
|
expect(CanonicalEmailBlock.matching_email('user@example.com')).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a block is not present' do
|
||||||
|
it 'announces the absence of the block' do
|
||||||
|
expect { cli.invoke(:remove, arguments) }.to output(
|
||||||
|
a_string_including('user@example.com is not blocked')
|
||||||
|
).to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue