mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-15 08:48:15 +00:00
Merge branch 'main' into mute-prefill
This commit is contained in:
commit
ff7e62f3c7
|
@ -23,5 +23,6 @@ RSpec/SpecFilePathFormat:
|
||||||
ActivityPub: activitypub
|
ActivityPub: activitypub
|
||||||
DeepL: deepl
|
DeepL: deepl
|
||||||
FetchOEmbedService: fetch_oembed_service
|
FetchOEmbedService: fetch_oembed_service
|
||||||
|
OAuth: oauth
|
||||||
OEmbedController: oembed_controller
|
OEmbedController: oembed_controller
|
||||||
OStatus: ostatus
|
OStatus: ostatus
|
||||||
|
|
|
@ -11,7 +11,21 @@ const config: StorybookConfig = {
|
||||||
name: '@storybook/react-vite',
|
name: '@storybook/react-vite',
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
staticDirs: ['./static'],
|
staticDirs: [
|
||||||
|
'./static',
|
||||||
|
// We need to manually specify the assets because of the symlink in public/sw.js
|
||||||
|
...[
|
||||||
|
'avatars',
|
||||||
|
'emoji',
|
||||||
|
'headers',
|
||||||
|
'sounds',
|
||||||
|
'badge.png',
|
||||||
|
'loading.gif',
|
||||||
|
'loading.png',
|
||||||
|
'oops.gif',
|
||||||
|
'oops.png',
|
||||||
|
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -2,16 +2,19 @@ import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
|
import { MemoryRouter, Route } from 'react-router';
|
||||||
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import type { Preview } from '@storybook/react-vite';
|
import type { Preview } from '@storybook/react-vite';
|
||||||
import { http, passthrough } from 'msw';
|
|
||||||
import { initialize, mswLoader } from 'msw-storybook-addon';
|
import { initialize, mswLoader } from 'msw-storybook-addon';
|
||||||
|
import { action } from 'storybook/actions';
|
||||||
|
|
||||||
import type { LocaleData } from '@/mastodon/locales';
|
import type { LocaleData } from '@/mastodon/locales';
|
||||||
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
|
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
|
||||||
import { defaultMiddleware } from '@/mastodon/store/store';
|
import { defaultMiddleware } from '@/mastodon/store/store';
|
||||||
|
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
|
||||||
|
|
||||||
// If you want to run the dark theme during development,
|
// If you want to run the dark theme during development,
|
||||||
// you can change the below to `/application.scss`
|
// you can change the below to `/application.scss`
|
||||||
|
@ -22,7 +25,9 @@ const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize MSW
|
// Initialize MSW
|
||||||
initialize();
|
initialize({
|
||||||
|
onUnhandledRequest: unhandledRequestHandler,
|
||||||
|
});
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
|
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
|
||||||
|
@ -94,6 +99,21 @@ const preview: Preview = {
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
(Story) => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<Story />
|
||||||
|
<Route
|
||||||
|
path='*'
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
render={({ location }) => {
|
||||||
|
if (location.pathname !== '/') {
|
||||||
|
action(`route change to ${location.pathname}`)(location);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
),
|
||||||
],
|
],
|
||||||
loaders: [mswLoader],
|
loaders: [mswLoader],
|
||||||
parameters: {
|
parameters: {
|
||||||
|
@ -115,20 +135,10 @@ const preview: Preview = {
|
||||||
|
|
||||||
state: {},
|
state: {},
|
||||||
|
|
||||||
// Force docs to use an iframe as it breaks MSW handlers.
|
docs: {},
|
||||||
// See: https://github.com/mswjs/msw-storybook-addon/issues/83
|
|
||||||
docs: {
|
|
||||||
story: {
|
|
||||||
inline: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: mockHandlers,
|
||||||
http.get('/index.json', passthrough),
|
|
||||||
http.get('/packs-dev/*', passthrough),
|
|
||||||
http.get('/sounds/*', passthrough),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Instances::ModerationNotesController < Admin::BaseController
|
||||||
|
before_action :set_instance, only: [:create]
|
||||||
|
before_action :set_instance_note, only: [:destroy]
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :instance_moderation_note, :create?
|
||||||
|
|
||||||
|
@instance_moderation_note = current_account.instance_moderation_notes.new(content: resource_params[:content], domain: @instance.domain)
|
||||||
|
|
||||||
|
if @instance_moderation_note.save
|
||||||
|
redirect_to admin_instance_path(@instance.domain, anchor: helpers.dom_id(@instance_moderation_note)), notice: I18n.t('admin.instances.moderation_notes.created_msg')
|
||||||
|
else
|
||||||
|
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
|
||||||
|
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||||
|
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
|
||||||
|
|
||||||
|
render 'admin/instances/show'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @instance_moderation_note, :destroy?
|
||||||
|
@instance_moderation_note.destroy!
|
||||||
|
redirect_to admin_instance_path(@instance_moderation_note.domain, anchor: 'instance-notes'), notice: I18n.t('admin.instances.moderation_notes.destroyed_msg')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params
|
||||||
|
.expect(instance_moderation_note: [:content])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_instance
|
||||||
|
domain = params[:instance_id]&.strip
|
||||||
|
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_instance_note
|
||||||
|
@instance_moderation_note = InstanceModerationNote.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,6 +14,9 @@ module Admin
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize :instance, :show?
|
authorize :instance, :show?
|
||||||
|
|
||||||
|
@instance_moderation_note = @instance.moderation_notes.new
|
||||||
|
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
|
||||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||||
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
|
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
|
||||||
end
|
end
|
||||||
|
@ -52,7 +55,8 @@ module Admin
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_instance
|
def set_instance
|
||||||
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(params[:id]&.strip))
|
domain = params[:id]&.strip
|
||||||
|
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instances
|
def set_instances
|
||||||
|
|
|
@ -17,6 +17,9 @@ module Admin
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize @rule, :update?
|
authorize @rule, :update?
|
||||||
|
|
||||||
|
missing_languages = RuleTranslation.languages - @rule.translations.pluck(:language)
|
||||||
|
missing_languages.each { |lang| @rule.translations.build(language: lang) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||||
skip_before_action :authenticate_resource_owner!
|
skip_before_action :authenticate_resource_owner!
|
||||||
|
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
class OAuth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||||
skip_before_action :authenticate_resource_owner!
|
skip_before_action :authenticate_resource_owner!
|
||||||
|
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Oauth::TokensController < Doorkeeper::TokensController
|
class OAuth::TokensController < Doorkeeper::TokensController
|
||||||
def revoke
|
def revoke
|
||||||
unsubscribe_for_token if token.present? && authorized? && token.accessible?
|
unsubscribe_for_token if token.present? && authorized? && token.accessible?
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Oauth::UserinfoController < Api::BaseController
|
class OAuth::UserinfoController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :profile }, only: [:show]
|
before_action -> { doorkeeper_authorize! :profile }, only: [:show]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@account = current_account
|
@account = current_account
|
||||||
render json: @account, serializer: OauthUserinfoSerializer
|
render json: @account, serializer: OAuthUserinfoSerializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WellKnown
|
module WellKnown
|
||||||
class OauthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
class OAuthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
||||||
include CacheConcern
|
include CacheConcern
|
||||||
|
|
||||||
# Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
|
# Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
|
||||||
|
@ -13,8 +13,8 @@ module WellKnown
|
||||||
# new OAuth scopes are added), we don't use expires_in to cache upstream,
|
# new OAuth scopes are added), we don't use expires_in to cache upstream,
|
||||||
# instead just caching in the rails cache:
|
# instead just caching in the rails cache:
|
||||||
render_with_cache(
|
render_with_cache(
|
||||||
json: ::OauthMetadataPresenter.new,
|
json: ::OAuthMetadataPresenter.new,
|
||||||
serializer: ::OauthMetadataSerializer,
|
serializer: ::OAuthMetadataSerializer,
|
||||||
content_type: 'application/json',
|
content_type: 'application/json',
|
||||||
expires_in: 15.minutes
|
expires_in: 15.minutes
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
import { render, fireEvent, screen } from 'mastodon/test_helpers';
|
import { render, fireEvent, screen } from '@/testing/rendering';
|
||||||
|
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
|
|
||||||
|
|
120
app/javascript/mastodon/components/account/account.stories.tsx
Normal file
120
app/javascript/mastodon/components/account/account.stories.tsx
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
|
||||||
|
|
||||||
|
import { Account } from './index';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Account',
|
||||||
|
component: Account,
|
||||||
|
argTypes: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the account to display',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Size of the avatar in pixels',
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether the account is hidden or not',
|
||||||
|
},
|
||||||
|
minimal: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether to display a minimal version of the account',
|
||||||
|
},
|
||||||
|
defaultAction: {
|
||||||
|
type: 'string',
|
||||||
|
control: 'select',
|
||||||
|
options: ['block', 'mute'],
|
||||||
|
description: 'Default action to take on the account',
|
||||||
|
},
|
||||||
|
withBio: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether to display the account bio or not',
|
||||||
|
},
|
||||||
|
withMenu: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether to display the account menu or not',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
id: '1',
|
||||||
|
size: 46,
|
||||||
|
hidden: false,
|
||||||
|
minimal: false,
|
||||||
|
defaultAction: 'mute',
|
||||||
|
withBio: false,
|
||||||
|
withMenu: true,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
state: {
|
||||||
|
accounts: {
|
||||||
|
'1': accountFactoryState(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Account>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
id: '1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Hidden: Story = {
|
||||||
|
args: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Minimal: Story = {
|
||||||
|
args: {
|
||||||
|
minimal: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithBio: Story = {
|
||||||
|
args: {
|
||||||
|
withBio: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoMenu: Story = {
|
||||||
|
args: {
|
||||||
|
withMenu: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Blocked: Story = {
|
||||||
|
args: {
|
||||||
|
defaultAction: 'block',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
state: {
|
||||||
|
relationships: {
|
||||||
|
'1': relationshipsFactory({
|
||||||
|
blocking: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Muted: Story = {
|
||||||
|
args: {},
|
||||||
|
parameters: {
|
||||||
|
state: {
|
||||||
|
relationships: {
|
||||||
|
'1': relationshipsFactory({
|
||||||
|
muting: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { render, fireEvent, screen } from 'mastodon/test_helpers';
|
import { render, fireEvent, screen } from '@/testing/rendering';
|
||||||
|
|
||||||
import Column from '../column';
|
import Column from '../column';
|
||||||
|
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Porušení pravidla",
|
"report_notification.categories.violation": "Porušení pravidla",
|
||||||
"report_notification.categories.violation_sentence": "porušení pravidel",
|
"report_notification.categories.violation_sentence": "porušení pravidel",
|
||||||
"report_notification.open": "Otevřít hlášení",
|
"report_notification.open": "Otevřít hlášení",
|
||||||
|
"search.clear": "Vymazat hledání",
|
||||||
"search.no_recent_searches": "Žádná nedávná vyhledávání",
|
"search.no_recent_searches": "Žádná nedávná vyhledávání",
|
||||||
"search.placeholder": "Hledat",
|
"search.placeholder": "Hledat",
|
||||||
"search.quick_action.account_search": "Profily odpovídající {x}",
|
"search.quick_action.account_search": "Profily odpovídající {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Regelverstoß",
|
"report_notification.categories.violation": "Regelverstoß",
|
||||||
"report_notification.categories.violation_sentence": "Regelverletzung",
|
"report_notification.categories.violation_sentence": "Regelverletzung",
|
||||||
"report_notification.open": "Meldung öffnen",
|
"report_notification.open": "Meldung öffnen",
|
||||||
|
"search.clear": "Suchanfrage löschen",
|
||||||
"search.no_recent_searches": "Keine früheren Suchanfragen",
|
"search.no_recent_searches": "Keine früheren Suchanfragen",
|
||||||
"search.placeholder": "Suchen",
|
"search.placeholder": "Suchen",
|
||||||
"search.quick_action.account_search": "Profile passend zu {x}",
|
"search.quick_action.account_search": "Profile passend zu {x}",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"about.blocks": "Reguligitaj serviloj",
|
"about.blocks": "Reguligitaj serviloj",
|
||||||
"about.contact": "Kontakto:",
|
"about.contact": "Kontakto:",
|
||||||
|
"about.default_locale": "기본",
|
||||||
"about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.",
|
"about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.",
|
||||||
"about.domain_blocks.no_reason_available": "Kialo ne disponeblas",
|
"about.domain_blocks.no_reason_available": "Kialo ne disponeblas",
|
||||||
"about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.",
|
"about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.",
|
||||||
|
@ -8,6 +9,7 @@
|
||||||
"about.domain_blocks.silenced.title": "Limigita",
|
"about.domain_blocks.silenced.title": "Limigita",
|
||||||
"about.domain_blocks.suspended.explanation": "Neniuj datumoj el tiu servilo estos prilaboritaj, konservitaj, aŭ interŝanĝitaj, do neeblas interagi aŭ komuniki kun uzantoj de tiu servilo.",
|
"about.domain_blocks.suspended.explanation": "Neniuj datumoj el tiu servilo estos prilaboritaj, konservitaj, aŭ interŝanĝitaj, do neeblas interagi aŭ komuniki kun uzantoj de tiu servilo.",
|
||||||
"about.domain_blocks.suspended.title": "Suspendita",
|
"about.domain_blocks.suspended.title": "Suspendita",
|
||||||
|
"about.language_label": "Lingvo",
|
||||||
"about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.",
|
"about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.",
|
||||||
"about.powered_by": "Malcentrigita socia retejo pere de {mastodon}",
|
"about.powered_by": "Malcentrigita socia retejo pere de {mastodon}",
|
||||||
"about.rules": "Reguloj de la servilo",
|
"about.rules": "Reguloj de la servilo",
|
||||||
|
@ -328,9 +330,13 @@
|
||||||
"errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo",
|
"errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo",
|
||||||
"errors.unexpected_crash.report_issue": "Raporti problemon",
|
"errors.unexpected_crash.report_issue": "Raporti problemon",
|
||||||
"explore.suggested_follows": "Homoj",
|
"explore.suggested_follows": "Homoj",
|
||||||
|
"explore.title": "Popularaĵoj",
|
||||||
"explore.trending_links": "Novaĵoj",
|
"explore.trending_links": "Novaĵoj",
|
||||||
"explore.trending_statuses": "Afiŝoj",
|
"explore.trending_statuses": "Afiŝoj",
|
||||||
"explore.trending_tags": "Kradvortoj",
|
"explore.trending_tags": "Kradvortoj",
|
||||||
|
"featured_carousel.next": "Antaŭen",
|
||||||
|
"featured_carousel.post": "Afiŝi",
|
||||||
|
"featured_carousel.previous": "Malantaŭen",
|
||||||
"filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.",
|
"filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.",
|
||||||
"filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!",
|
"filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!",
|
||||||
"filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.",
|
"filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.",
|
||||||
|
@ -547,6 +553,7 @@
|
||||||
"navigation_bar.lists": "Listoj",
|
"navigation_bar.lists": "Listoj",
|
||||||
"navigation_bar.logout": "Elsaluti",
|
"navigation_bar.logout": "Elsaluti",
|
||||||
"navigation_bar.moderation": "Modereco",
|
"navigation_bar.moderation": "Modereco",
|
||||||
|
"navigation_bar.more": "Pli",
|
||||||
"navigation_bar.mutes": "Silentigitaj uzantoj",
|
"navigation_bar.mutes": "Silentigitaj uzantoj",
|
||||||
"navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.",
|
"navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.",
|
||||||
"navigation_bar.preferences": "Preferoj",
|
"navigation_bar.preferences": "Preferoj",
|
||||||
|
@ -777,6 +784,7 @@
|
||||||
"report_notification.categories.violation": "Malobservo de la regulo",
|
"report_notification.categories.violation": "Malobservo de la regulo",
|
||||||
"report_notification.categories.violation_sentence": "malobservo de la regulo",
|
"report_notification.categories.violation_sentence": "malobservo de la regulo",
|
||||||
"report_notification.open": "Malfermi la raporton",
|
"report_notification.open": "Malfermi la raporton",
|
||||||
|
"search.clear": "검색어 지우기",
|
||||||
"search.no_recent_searches": "Neniuj lastaj serĉoj",
|
"search.no_recent_searches": "Neniuj lastaj serĉoj",
|
||||||
"search.placeholder": "Serĉi",
|
"search.placeholder": "Serĉi",
|
||||||
"search.quick_action.account_search": "Profiloj kiuj kongruas kun {x}",
|
"search.quick_action.account_search": "Profiloj kiuj kongruas kun {x}",
|
||||||
|
@ -872,7 +880,9 @@
|
||||||
"subscribed_languages.save": "Konservi ŝanĝojn",
|
"subscribed_languages.save": "Konservi ŝanĝojn",
|
||||||
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
|
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
|
||||||
"tabs_bar.home": "Hejmo",
|
"tabs_bar.home": "Hejmo",
|
||||||
|
"tabs_bar.menu": "Menuo",
|
||||||
"tabs_bar.notifications": "Sciigoj",
|
"tabs_bar.notifications": "Sciigoj",
|
||||||
|
"tabs_bar.search": "Serĉi",
|
||||||
"terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
|
"terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
|
||||||
"terms_of_service.title": "Kondiĉoj de uzado",
|
"terms_of_service.title": "Kondiĉoj de uzado",
|
||||||
"terms_of_service.upcoming_changes_on": "Venontaj ŝanĝoj el {date}",
|
"terms_of_service.upcoming_changes_on": "Venontaj ŝanĝoj el {date}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Violación de regla",
|
"report_notification.categories.violation": "Violación de regla",
|
||||||
"report_notification.categories.violation_sentence": "violación de regla",
|
"report_notification.categories.violation_sentence": "violación de regla",
|
||||||
"report_notification.open": "Abrir denuncia",
|
"report_notification.open": "Abrir denuncia",
|
||||||
|
"search.clear": "Limpiar búsqueda",
|
||||||
"search.no_recent_searches": "Sin búsquedas recientes",
|
"search.no_recent_searches": "Sin búsquedas recientes",
|
||||||
"search.placeholder": "Buscar",
|
"search.placeholder": "Buscar",
|
||||||
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Infracción de regla",
|
"report_notification.categories.violation": "Infracción de regla",
|
||||||
"report_notification.categories.violation_sentence": "infracción de regla",
|
"report_notification.categories.violation_sentence": "infracción de regla",
|
||||||
"report_notification.open": "Abrir denuncia",
|
"report_notification.open": "Abrir denuncia",
|
||||||
|
"search.clear": "Limpiar búsqueda",
|
||||||
"search.no_recent_searches": "Sin búsquedas recientes",
|
"search.no_recent_searches": "Sin búsquedas recientes",
|
||||||
"search.placeholder": "Buscar",
|
"search.placeholder": "Buscar",
|
||||||
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Infracción de regla",
|
"report_notification.categories.violation": "Infracción de regla",
|
||||||
"report_notification.categories.violation_sentence": "infracción de regla",
|
"report_notification.categories.violation_sentence": "infracción de regla",
|
||||||
"report_notification.open": "Abrir informe",
|
"report_notification.open": "Abrir informe",
|
||||||
|
"search.clear": "Limpiar búsqueda",
|
||||||
"search.no_recent_searches": "No hay búsquedas recientes",
|
"search.no_recent_searches": "No hay búsquedas recientes",
|
||||||
"search.placeholder": "Buscar",
|
"search.placeholder": "Buscar",
|
||||||
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Brotin regla",
|
"report_notification.categories.violation": "Brotin regla",
|
||||||
"report_notification.categories.violation_sentence": "brot á reglu",
|
"report_notification.categories.violation_sentence": "brot á reglu",
|
||||||
"report_notification.open": "Opna melding",
|
"report_notification.open": "Opna melding",
|
||||||
|
"search.clear": "Nullstilla leiting",
|
||||||
"search.no_recent_searches": "Ongar nýggjar leitingar",
|
"search.no_recent_searches": "Ongar nýggjar leitingar",
|
||||||
"search.placeholder": "Leita",
|
"search.placeholder": "Leita",
|
||||||
"search.quick_action.account_search": "Vangar, ið samsvara {x}",
|
"search.quick_action.account_search": "Vangar, ið samsvara {x}",
|
||||||
|
|
|
@ -563,6 +563,8 @@
|
||||||
"navigation_bar.follows_and_followers": "Seguindo e seguidoras",
|
"navigation_bar.follows_and_followers": "Seguindo e seguidoras",
|
||||||
"navigation_bar.import_export": "Importar e exportar",
|
"navigation_bar.import_export": "Importar e exportar",
|
||||||
"navigation_bar.lists": "Listaxes",
|
"navigation_bar.lists": "Listaxes",
|
||||||
|
"navigation_bar.live_feed_local": "En directo (local)",
|
||||||
|
"navigation_bar.live_feed_public": "En directo (federada)",
|
||||||
"navigation_bar.logout": "Pechar sesión",
|
"navigation_bar.logout": "Pechar sesión",
|
||||||
"navigation_bar.moderation": "Moderación",
|
"navigation_bar.moderation": "Moderación",
|
||||||
"navigation_bar.more": "Máis",
|
"navigation_bar.more": "Máis",
|
||||||
|
@ -802,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Faltou ás regras",
|
"report_notification.categories.violation": "Faltou ás regras",
|
||||||
"report_notification.categories.violation_sentence": "violación das regras",
|
"report_notification.categories.violation_sentence": "violación das regras",
|
||||||
"report_notification.open": "Abrir a denuncia",
|
"report_notification.open": "Abrir a denuncia",
|
||||||
|
"search.clear": "Limpar a busca",
|
||||||
"search.no_recent_searches": "Non hai buscas recentes",
|
"search.no_recent_searches": "Non hai buscas recentes",
|
||||||
"search.placeholder": "Procurar",
|
"search.placeholder": "Procurar",
|
||||||
"search.quick_action.account_search": "Perfís coincidentes {x}",
|
"search.quick_action.account_search": "Perfís coincidentes {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "הפרת כלל",
|
"report_notification.categories.violation": "הפרת כלל",
|
||||||
"report_notification.categories.violation_sentence": "הפרת כלל",
|
"report_notification.categories.violation_sentence": "הפרת כלל",
|
||||||
"report_notification.open": "פתח דו\"ח",
|
"report_notification.open": "פתח דו\"ח",
|
||||||
|
"search.clear": "ניקוי חיפוש",
|
||||||
"search.no_recent_searches": "לא נמצאו חיפושים אחרונים",
|
"search.no_recent_searches": "לא נמצאו חיפושים אחרונים",
|
||||||
"search.placeholder": "חיפוש",
|
"search.placeholder": "חיפוש",
|
||||||
"search.quick_action.account_search": "פרופילים המכילים {x}",
|
"search.quick_action.account_search": "פרופילים המכילים {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Szabálysértés",
|
"report_notification.categories.violation": "Szabálysértés",
|
||||||
"report_notification.categories.violation_sentence": "szabálysértés",
|
"report_notification.categories.violation_sentence": "szabálysértés",
|
||||||
"report_notification.open": "Bejelentés megnyitása",
|
"report_notification.open": "Bejelentés megnyitása",
|
||||||
|
"search.clear": "Keresés törlése",
|
||||||
"search.no_recent_searches": "Nincsenek keresési előzmények",
|
"search.no_recent_searches": "Nincsenek keresési előzmények",
|
||||||
"search.placeholder": "Keresés",
|
"search.placeholder": "Keresés",
|
||||||
"search.quick_action.account_search": "Profilok a következő keresésre: {x}",
|
"search.quick_action.account_search": "Profilok a következő keresésre: {x}",
|
||||||
|
|
|
@ -563,6 +563,8 @@
|
||||||
"navigation_bar.follows_and_followers": "팔로우와 팔로워",
|
"navigation_bar.follows_and_followers": "팔로우와 팔로워",
|
||||||
"navigation_bar.import_export": "가져오기 & 내보내기",
|
"navigation_bar.import_export": "가져오기 & 내보내기",
|
||||||
"navigation_bar.lists": "리스트",
|
"navigation_bar.lists": "리스트",
|
||||||
|
"navigation_bar.live_feed_local": "라이브 피드 (로컬)",
|
||||||
|
"navigation_bar.live_feed_public": "라이브 피드 (공개)",
|
||||||
"navigation_bar.logout": "로그아웃",
|
"navigation_bar.logout": "로그아웃",
|
||||||
"navigation_bar.moderation": "중재",
|
"navigation_bar.moderation": "중재",
|
||||||
"navigation_bar.more": "더 보기",
|
"navigation_bar.more": "더 보기",
|
||||||
|
@ -802,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "규칙 위반",
|
"report_notification.categories.violation": "규칙 위반",
|
||||||
"report_notification.categories.violation_sentence": "규칙 위반",
|
"report_notification.categories.violation_sentence": "규칙 위반",
|
||||||
"report_notification.open": "신고 열기",
|
"report_notification.open": "신고 열기",
|
||||||
|
"search.clear": "검색 초기화",
|
||||||
"search.no_recent_searches": "최근 검색 기록이 없습니다",
|
"search.no_recent_searches": "최근 검색 기록이 없습니다",
|
||||||
"search.placeholder": "검색",
|
"search.placeholder": "검색",
|
||||||
"search.quick_action.account_search": "{x}에 맞는 프로필",
|
"search.quick_action.account_search": "{x}에 맞는 프로필",
|
||||||
|
|
|
@ -563,6 +563,8 @@
|
||||||
"navigation_bar.follows_and_followers": "Volgers en gevolgde accounts",
|
"navigation_bar.follows_and_followers": "Volgers en gevolgde accounts",
|
||||||
"navigation_bar.import_export": "Importeren en exporteren",
|
"navigation_bar.import_export": "Importeren en exporteren",
|
||||||
"navigation_bar.lists": "Lijsten",
|
"navigation_bar.lists": "Lijsten",
|
||||||
|
"navigation_bar.live_feed_local": "Openbare tijdlijn (deze server)",
|
||||||
|
"navigation_bar.live_feed_public": "Openbare tijdlijn (alles)",
|
||||||
"navigation_bar.logout": "Uitloggen",
|
"navigation_bar.logout": "Uitloggen",
|
||||||
"navigation_bar.moderation": "Moderatie",
|
"navigation_bar.moderation": "Moderatie",
|
||||||
"navigation_bar.more": "Meer",
|
"navigation_bar.more": "Meer",
|
||||||
|
@ -802,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Overtreden regel(s)",
|
"report_notification.categories.violation": "Overtreden regel(s)",
|
||||||
"report_notification.categories.violation_sentence": "serverregel overtreden",
|
"report_notification.categories.violation_sentence": "serverregel overtreden",
|
||||||
"report_notification.open": "Rapportage openen",
|
"report_notification.open": "Rapportage openen",
|
||||||
|
"search.clear": "Zoekopdracht wissen",
|
||||||
"search.no_recent_searches": "Geen recente zoekopdrachten",
|
"search.no_recent_searches": "Geen recente zoekopdrachten",
|
||||||
"search.placeholder": "Zoeken",
|
"search.placeholder": "Zoeken",
|
||||||
"search.quick_action.account_search": "Accounts die overeenkomen met {x}",
|
"search.quick_action.account_search": "Accounts die overeenkomen met {x}",
|
||||||
|
|
|
@ -563,6 +563,8 @@
|
||||||
"navigation_bar.follows_and_followers": "Seguindo e seguidores",
|
"navigation_bar.follows_and_followers": "Seguindo e seguidores",
|
||||||
"navigation_bar.import_export": "Importar e exportar",
|
"navigation_bar.import_export": "Importar e exportar",
|
||||||
"navigation_bar.lists": "Listas",
|
"navigation_bar.lists": "Listas",
|
||||||
|
"navigation_bar.live_feed_local": "Cronologia local",
|
||||||
|
"navigation_bar.live_feed_public": "Cronologia federada",
|
||||||
"navigation_bar.logout": "Sair",
|
"navigation_bar.logout": "Sair",
|
||||||
"navigation_bar.moderation": "Moderação",
|
"navigation_bar.moderation": "Moderação",
|
||||||
"navigation_bar.more": "Mais",
|
"navigation_bar.more": "Mais",
|
||||||
|
@ -799,6 +801,7 @@
|
||||||
"report_notification.categories.violation": "Violação de regra",
|
"report_notification.categories.violation": "Violação de regra",
|
||||||
"report_notification.categories.violation_sentence": "violação de regra",
|
"report_notification.categories.violation_sentence": "violação de regra",
|
||||||
"report_notification.open": "Abrir denúncia",
|
"report_notification.open": "Abrir denúncia",
|
||||||
|
"search.clear": "Limpar pesquisa",
|
||||||
"search.no_recent_searches": "Nenhuma pesquisa recente",
|
"search.no_recent_searches": "Nenhuma pesquisa recente",
|
||||||
"search.placeholder": "Pesquisar",
|
"search.placeholder": "Pesquisar",
|
||||||
"search.quick_action.account_search": "Perfis com correspondência a {x}",
|
"search.quick_action.account_search": "Perfis com correspondência a {x}",
|
||||||
|
|
|
@ -563,6 +563,8 @@
|
||||||
"navigation_bar.follows_and_followers": "Подписки и подписчики",
|
"navigation_bar.follows_and_followers": "Подписки и подписчики",
|
||||||
"navigation_bar.import_export": "Импорт и экспорт",
|
"navigation_bar.import_export": "Импорт и экспорт",
|
||||||
"navigation_bar.lists": "Списки",
|
"navigation_bar.lists": "Списки",
|
||||||
|
"navigation_bar.live_feed_local": "Живая лента (локальная)",
|
||||||
|
"navigation_bar.live_feed_public": "Живая лента (глобальная)",
|
||||||
"navigation_bar.logout": "Выйти",
|
"navigation_bar.logout": "Выйти",
|
||||||
"navigation_bar.moderation": "Модерирование",
|
"navigation_bar.moderation": "Модерирование",
|
||||||
"navigation_bar.more": "Ещё",
|
"navigation_bar.more": "Ещё",
|
||||||
|
@ -578,9 +580,9 @@
|
||||||
"navigation_panel.expand_lists": "Развернуть меню списков",
|
"navigation_panel.expand_lists": "Развернуть меню списков",
|
||||||
"not_signed_in_indicator.not_signed_in": "Эта страница доступна только авторизованным пользователям.",
|
"not_signed_in_indicator.not_signed_in": "Эта страница доступна только авторизованным пользователям.",
|
||||||
"notification.admin.report": "{name} пожаловался (-лась) на {target}",
|
"notification.admin.report": "{name} пожаловался (-лась) на {target}",
|
||||||
"notification.admin.report_account": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target} по причине «{category}»",
|
"notification.admin.report_account": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target}, выбрав категорию «{category}»",
|
||||||
"notification.admin.report_account_other": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target}",
|
"notification.admin.report_account_other": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target}",
|
||||||
"notification.admin.report_statuses": "{name} пожаловался (-лась) на {target} по причине «{category}»",
|
"notification.admin.report_statuses": "{name} пожаловался (-лась) на {target}, выбрав категорию «{category}»",
|
||||||
"notification.admin.report_statuses_other": "{name} пожаловался (-лась) на {target}",
|
"notification.admin.report_statuses_other": "{name} пожаловался (-лась) на {target}",
|
||||||
"notification.admin.sign_up": "{name} зарегистрировался (-лась) на сервере",
|
"notification.admin.sign_up": "{name} зарегистрировался (-лась) на сервере",
|
||||||
"notification.admin.sign_up.name_and_others": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} зарегистрировались на сервере",
|
"notification.admin.sign_up.name_and_others": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} зарегистрировались на сервере",
|
||||||
|
@ -602,13 +604,13 @@
|
||||||
"notification.mentioned_you": "{name} упомянул(а) вас",
|
"notification.mentioned_you": "{name} упомянул(а) вас",
|
||||||
"notification.moderation-warning.learn_more": "Узнать больше",
|
"notification.moderation-warning.learn_more": "Узнать больше",
|
||||||
"notification.moderation_warning": "Модераторы вынесли вам предупреждение",
|
"notification.moderation_warning": "Модераторы вынесли вам предупреждение",
|
||||||
"notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.",
|
"notification.moderation_warning.action_delete_statuses": "Некоторые ваши посты были удалены.",
|
||||||
"notification.moderation_warning.action_disable": "Ваша учётная запись была отключена.",
|
"notification.moderation_warning.action_disable": "Ваша учётная запись была отключена.",
|
||||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые из ваших сообщений были отмечены как деликатные.",
|
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые ваши посты получили отметку деликатного содержания.",
|
||||||
"notification.moderation_warning.action_none": "Ваша учётная запись получила предупреждение от модерации.",
|
"notification.moderation_warning.action_none": "Модераторы вынесли вам предупреждение.",
|
||||||
"notification.moderation_warning.action_sensitive": "С этого момента ваши сообщения будут помечены как деликатные.",
|
"notification.moderation_warning.action_sensitive": "С этого момента ваши посты будут иметь отметку деликатного содержания.",
|
||||||
"notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
|
"notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
|
||||||
"notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.",
|
"notification.moderation_warning.action_suspend": "Ваша учётная запись была заблокирована.",
|
||||||
"notification.own_poll": "Ваш опрос завершился",
|
"notification.own_poll": "Ваш опрос завершился",
|
||||||
"notification.poll": "Опрос, в котором вы приняли участие, завершился",
|
"notification.poll": "Опрос, в котором вы приняли участие, завершился",
|
||||||
"notification.reblog": "{name} продвинул(а) ваш пост",
|
"notification.reblog": "{name} продвинул(а) ваш пост",
|
||||||
|
@ -792,16 +794,17 @@
|
||||||
"report.thanks.title_actionable": "Спасибо, что сообщили о проблеме, мы рассмотрим вашу жалобу.",
|
"report.thanks.title_actionable": "Спасибо, что сообщили о проблеме, мы рассмотрим вашу жалобу.",
|
||||||
"report.unfollow": "Отписаться от @{name}",
|
"report.unfollow": "Отписаться от @{name}",
|
||||||
"report.unfollow_explanation": "Вы подписаны на этого пользователя. Отпишитесь от пользователя, чтобы перестать видеть посты этого человека в домашней ленте.",
|
"report.unfollow_explanation": "Вы подписаны на этого пользователя. Отпишитесь от пользователя, чтобы перестать видеть посты этого человека в домашней ленте.",
|
||||||
"report_notification.attached_statuses": "{count, plural, one {{count} сообщение} few {{count} сообщения} many {{count} сообщений} other {{count} сообщений}} вложено",
|
"report_notification.attached_statuses": "{count, plural, one {{count} пост прикреплён} few {{count} поста прикреплено} other {{count} постов прикреплено}}",
|
||||||
"report_notification.categories.legal": "Нарушение закона",
|
"report_notification.categories.legal": "Нарушение закона",
|
||||||
"report_notification.categories.legal_sentence": "запрещённый контент",
|
"report_notification.categories.legal_sentence": "Нарушение закона",
|
||||||
"report_notification.categories.other": "Другое",
|
"report_notification.categories.other": "Другое",
|
||||||
"report_notification.categories.other_sentence": "другое",
|
"report_notification.categories.other_sentence": "Другое",
|
||||||
"report_notification.categories.spam": "Спам",
|
"report_notification.categories.spam": "Спам",
|
||||||
"report_notification.categories.spam_sentence": "спам",
|
"report_notification.categories.spam_sentence": "Спам",
|
||||||
"report_notification.categories.violation": "Нарушение правил",
|
"report_notification.categories.violation": "Нарушение правил",
|
||||||
"report_notification.categories.violation_sentence": "нарушение правила",
|
"report_notification.categories.violation_sentence": "Нарушение правил",
|
||||||
"report_notification.open": "Открыть жалобу",
|
"report_notification.open": "Перейти к жалобе",
|
||||||
|
"search.clear": "Очистить поисковый запрос",
|
||||||
"search.no_recent_searches": "Недавние запросы отсутствуют",
|
"search.no_recent_searches": "Недавние запросы отсутствуют",
|
||||||
"search.placeholder": "Поиск",
|
"search.placeholder": "Поиск",
|
||||||
"search.quick_action.account_search": "Профили, соответствующие {x}",
|
"search.quick_action.account_search": "Профили, соответствующие {x}",
|
||||||
|
@ -868,6 +871,7 @@
|
||||||
"status.mute_conversation": "Игнорировать обсуждение",
|
"status.mute_conversation": "Игнорировать обсуждение",
|
||||||
"status.open": "Открыть пост",
|
"status.open": "Открыть пост",
|
||||||
"status.pin": "Закрепить в профиле",
|
"status.pin": "Закрепить в профиле",
|
||||||
|
"status.quote_error.filtered": "Скрыто одним из ваших фильтров",
|
||||||
"status.quote_error.removed": "Этот пост был удалён его автором.",
|
"status.quote_error.removed": "Этот пост был удалён его автором.",
|
||||||
"status.read_more": "Читать далее",
|
"status.read_more": "Читать далее",
|
||||||
"status.reblog": "Продвинуть",
|
"status.reblog": "Продвинуть",
|
||||||
|
|
|
@ -779,6 +779,7 @@
|
||||||
"report_notification.categories.violation": "Порушення правил",
|
"report_notification.categories.violation": "Порушення правил",
|
||||||
"report_notification.categories.violation_sentence": "порушення правил",
|
"report_notification.categories.violation_sentence": "порушення правил",
|
||||||
"report_notification.open": "Відкрити скаргу",
|
"report_notification.open": "Відкрити скаргу",
|
||||||
|
"search.clear": "Очистити пошук",
|
||||||
"search.no_recent_searches": "Немає останніх пошуків",
|
"search.no_recent_searches": "Немає останніх пошуків",
|
||||||
"search.placeholder": "Пошук",
|
"search.placeholder": "Пошук",
|
||||||
"search.quick_action.account_search": "Збіг профілів {x}",
|
"search.quick_action.account_search": "Збіг профілів {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "Vi phạm nội quy",
|
"report_notification.categories.violation": "Vi phạm nội quy",
|
||||||
"report_notification.categories.violation_sentence": "vi phạm nội quy",
|
"report_notification.categories.violation_sentence": "vi phạm nội quy",
|
||||||
"report_notification.open": "Mở báo cáo",
|
"report_notification.open": "Mở báo cáo",
|
||||||
|
"search.clear": "Xóa tìm kiếm",
|
||||||
"search.no_recent_searches": "Gần đây chưa tìm gì",
|
"search.no_recent_searches": "Gần đây chưa tìm gì",
|
||||||
"search.placeholder": "Tìm kiếm",
|
"search.placeholder": "Tìm kiếm",
|
||||||
"search.quick_action.account_search": "Người tên {x}",
|
"search.quick_action.account_search": "Người tên {x}",
|
||||||
|
|
|
@ -804,6 +804,7 @@
|
||||||
"report_notification.categories.violation": "違反規則",
|
"report_notification.categories.violation": "違反規則",
|
||||||
"report_notification.categories.violation_sentence": "違反規則",
|
"report_notification.categories.violation_sentence": "違反規則",
|
||||||
"report_notification.open": "開啟檢舉報告",
|
"report_notification.open": "開啟檢舉報告",
|
||||||
|
"search.clear": "清除搜尋紀錄",
|
||||||
"search.no_recent_searches": "尚無最近的搜尋紀錄",
|
"search.no_recent_searches": "尚無最近的搜尋紀錄",
|
||||||
"search.placeholder": "搜尋",
|
"search.placeholder": "搜尋",
|
||||||
"search.quick_action.account_search": "符合的個人檔案 {x}",
|
"search.quick_action.account_search": "符合的個人檔案 {x}",
|
||||||
|
|
|
@ -1632,6 +1632,17 @@ a.sparkline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.timestamp {
|
||||||
|
color: $darker-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
time {
|
time {
|
||||||
margin-inline-start: 5px;
|
margin-inline-start: 5px;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
|
|
53
app/javascript/testing/api.ts
Normal file
53
app/javascript/testing/api.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { http, HttpResponse } from 'msw';
|
||||||
|
import { action } from 'storybook/actions';
|
||||||
|
|
||||||
|
import { relationshipsFactory } from './factories';
|
||||||
|
|
||||||
|
export const mockHandlers = {
|
||||||
|
mute: http.post<{ id: string }>('/api/v1/accounts/:id/mute', ({ params }) => {
|
||||||
|
action('muting account')(params);
|
||||||
|
return HttpResponse.json(
|
||||||
|
relationshipsFactory({ id: params.id, muting: true }),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
unmute: http.post<{ id: string }>(
|
||||||
|
'/api/v1/accounts/:id/unmute',
|
||||||
|
({ params }) => {
|
||||||
|
action('unmuting account')(params);
|
||||||
|
return HttpResponse.json(
|
||||||
|
relationshipsFactory({ id: params.id, muting: false }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
block: http.post<{ id: string }>(
|
||||||
|
'/api/v1/accounts/:id/block',
|
||||||
|
({ params }) => {
|
||||||
|
action('blocking account')(params);
|
||||||
|
return HttpResponse.json(
|
||||||
|
relationshipsFactory({ id: params.id, blocking: true }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
unblock: http.post<{ id: string }>(
|
||||||
|
'/api/v1/accounts/:id/unblock',
|
||||||
|
({ params }) => {
|
||||||
|
action('unblocking account')(params);
|
||||||
|
return HttpResponse.json(
|
||||||
|
relationshipsFactory({
|
||||||
|
id: params.id,
|
||||||
|
blocking: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unhandledRequestHandler = ({ url }: Request) => {
|
||||||
|
const { pathname } = new URL(url);
|
||||||
|
if (pathname.startsWith('/api/v1/')) {
|
||||||
|
action(`unhandled request to ${pathname}`)(url);
|
||||||
|
console.warn(
|
||||||
|
`Unhandled request to ${pathname}. Please add a handler for this request in your storybook configuration.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
70
app/javascript/testing/factories.ts
Normal file
70
app/javascript/testing/factories.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships';
|
||||||
|
import { createAccountFromServerJSON } from '@/mastodon/models/account';
|
||||||
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
|
|
||||||
|
type FactoryOptions<T> = {
|
||||||
|
id?: string;
|
||||||
|
} & Partial<T>;
|
||||||
|
|
||||||
|
type FactoryFunction<T> = (options?: FactoryOptions<T>) => T;
|
||||||
|
|
||||||
|
export const accountFactory: FactoryFunction<ApiAccountJSON> = ({
|
||||||
|
id,
|
||||||
|
...data
|
||||||
|
} = {}) => ({
|
||||||
|
id: id ?? '1',
|
||||||
|
acct: 'testuser',
|
||||||
|
avatar: '/avatars/original/missing.png',
|
||||||
|
avatar_static: '/avatars/original/missing.png',
|
||||||
|
username: 'testuser',
|
||||||
|
display_name: 'Test User',
|
||||||
|
bot: false,
|
||||||
|
created_at: '2023-01-01T00:00:00.000Z',
|
||||||
|
discoverable: true,
|
||||||
|
emojis: [],
|
||||||
|
fields: [],
|
||||||
|
followers_count: 0,
|
||||||
|
following_count: 0,
|
||||||
|
group: false,
|
||||||
|
header: '/header.png',
|
||||||
|
header_static: '/header_static.png',
|
||||||
|
indexable: true,
|
||||||
|
last_status_at: '2023-01-01',
|
||||||
|
locked: false,
|
||||||
|
mute_expires_at: null,
|
||||||
|
note: 'This is a test user account.',
|
||||||
|
statuses_count: 0,
|
||||||
|
suspended: false,
|
||||||
|
url: '/@testuser',
|
||||||
|
uri: '/users/testuser',
|
||||||
|
noindex: false,
|
||||||
|
roles: [],
|
||||||
|
hide_collections: false,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accountFactoryState = (
|
||||||
|
options: FactoryOptions<ApiAccountJSON> = {},
|
||||||
|
) => createAccountFromServerJSON(accountFactory(options));
|
||||||
|
|
||||||
|
export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
|
||||||
|
id,
|
||||||
|
...data
|
||||||
|
} = {}) => ({
|
||||||
|
id: id ?? '1',
|
||||||
|
following: false,
|
||||||
|
followed_by: false,
|
||||||
|
blocking: false,
|
||||||
|
blocked_by: false,
|
||||||
|
languages: null,
|
||||||
|
muting_notifications: false,
|
||||||
|
note: '',
|
||||||
|
requested_by: false,
|
||||||
|
muting: false,
|
||||||
|
requested: false,
|
||||||
|
domain_blocking: false,
|
||||||
|
endorsed: false,
|
||||||
|
notifying: false,
|
||||||
|
showing_reblogs: true,
|
||||||
|
...data,
|
||||||
|
});
|
|
@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router';
|
||||||
import type { RenderOptions } from '@testing-library/react';
|
import type { RenderOptions } from '@testing-library/react';
|
||||||
import { render as rtlRender } from '@testing-library/react';
|
import { render as rtlRender } from '@testing-library/react';
|
||||||
|
|
||||||
import { IdentityContext } from './identity_context';
|
import { IdentityContext } from '@/mastodon/identity_context';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
|
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
|
|
@ -163,6 +163,7 @@ class Account < ApplicationRecord
|
||||||
after_update_commit :trigger_update_webhooks
|
after_update_commit :trigger_update_webhooks
|
||||||
|
|
||||||
delegate :email,
|
delegate :email,
|
||||||
|
:email_domain,
|
||||||
:unconfirmed_email,
|
:unconfirmed_email,
|
||||||
:current_sign_in_at,
|
:current_sign_in_at,
|
||||||
:created_at,
|
:created_at,
|
||||||
|
|
|
@ -18,6 +18,7 @@ module Account::Associations
|
||||||
has_many :favourites
|
has_many :favourites
|
||||||
has_many :featured_tags, -> { includes(:tag) }
|
has_many :featured_tags, -> { includes(:tag) }
|
||||||
has_many :list_accounts
|
has_many :list_accounts
|
||||||
|
has_many :instance_moderation_notes
|
||||||
has_many :media_attachments
|
has_many :media_attachments
|
||||||
has_many :mentions
|
has_many :mentions
|
||||||
has_many :migrations, class_name: 'AccountMigration'
|
has_many :migrations, class_name: 'AccountMigration'
|
||||||
|
|
|
@ -21,6 +21,7 @@ class Instance < ApplicationRecord
|
||||||
belongs_to :unavailable_domain
|
belongs_to :unavailable_domain
|
||||||
|
|
||||||
has_many :accounts, dependent: nil
|
has_many :accounts, dependent: nil
|
||||||
|
has_many :moderation_notes, class_name: 'InstanceModerationNote', dependent: :destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }
|
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }
|
||||||
|
|
27
app/models/instance_moderation_note.rb
Normal file
27
app/models/instance_moderation_note.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: instance_moderation_notes
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# content :text
|
||||||
|
# domain :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
#
|
||||||
|
class InstanceModerationNote < ApplicationRecord
|
||||||
|
include DomainNormalizable
|
||||||
|
include DomainMaterializable
|
||||||
|
|
||||||
|
CONTENT_SIZE_LIMIT = 2_000
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :instance, inverse_of: :moderation_notes, foreign_key: :domain, primary_key: :domain, optional: true
|
||||||
|
|
||||||
|
scope :chronological, -> { reorder(id: :asc) }
|
||||||
|
|
||||||
|
validates :content, presence: true, length: { maximum: CONTENT_SIZE_LIMIT }
|
||||||
|
validates :domain, presence: true, domain: true
|
||||||
|
end
|
|
@ -20,7 +20,7 @@ class Rule < ApplicationRecord
|
||||||
self.discard_column = :deleted_at
|
self.discard_column = :deleted_at
|
||||||
|
|
||||||
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
|
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
|
||||||
accepts_nested_attributes_for :translations, reject_if: :all_blank, allow_destroy: true
|
accepts_nested_attributes_for :translations, reject_if: ->(attributes) { attributes['text'].blank? }, allow_destroy: true
|
||||||
|
|
||||||
validates :text, presence: true, length: { maximum: TEXT_SIZE_LIMIT }
|
validates :text, presence: true, length: { maximum: TEXT_SIZE_LIMIT }
|
||||||
|
|
||||||
|
|
|
@ -20,4 +20,8 @@ class RuleTranslation < ApplicationRecord
|
||||||
|
|
||||||
scope :for_locale, ->(locale) { where(language: I18n::Locale::Tag.tag(locale).to_a.first) }
|
scope :for_locale, ->(locale) { where(language: I18n::Locale::Tag.tag(locale).to_a.first) }
|
||||||
scope :by_language_length, -> { order(Arel.sql('LENGTH(LANGUAGE)').desc) }
|
scope :by_language_length, -> { order(Arel.sql('LENGTH(LANGUAGE)').desc) }
|
||||||
|
|
||||||
|
def self.languages
|
||||||
|
RuleTranslation.joins(:rule).merge(Rule.kept).select(:language).distinct.pluck(:language).sort
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -223,6 +223,12 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_domain
|
||||||
|
Mail::Address.new(email).domain
|
||||||
|
rescue Mail::Field::ParseError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def update_sign_in!(new_sign_in: false)
|
def update_sign_in!(new_sign_in: false)
|
||||||
old_current = current_sign_in_at
|
old_current = current_sign_in_at
|
||||||
new_current = Time.now.utc
|
new_current = Time.now.utc
|
||||||
|
|
17
app/policies/instance_moderation_note_policy.rb
Normal file
17
app/policies/instance_moderation_note_policy.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InstanceModerationNotePolicy < ApplicationPolicy
|
||||||
|
def create?
|
||||||
|
role.can?(:manage_federation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
owner? || (role.can?(:manage_federation) && role.overrides?(record.account.user_role))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def owner?
|
||||||
|
record.account_id == current_account&.id
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class OauthMetadataPresenter < ActiveModelSerializers::Model
|
class OAuthMetadataPresenter < ActiveModelSerializers::Model
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :issuer, :authorization_endpoint, :token_endpoint,
|
attributes :issuer, :authorization_endpoint, :token_endpoint,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class OauthMetadataSerializer < ActiveModel::Serializer
|
class OAuthMetadataSerializer < ActiveModel::Serializer
|
||||||
attributes :issuer, :authorization_endpoint, :token_endpoint,
|
attributes :issuer, :authorization_endpoint, :token_endpoint,
|
||||||
:revocation_endpoint, :userinfo_endpoint, :scopes_supported,
|
:revocation_endpoint, :userinfo_endpoint, :scopes_supported,
|
||||||
:response_types_supported, :response_modes_supported,
|
:response_types_supported, :response_modes_supported,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class OauthUserinfoSerializer < ActiveModel::Serializer
|
class OAuthUserinfoSerializer < ActiveModel::Serializer
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :iss, :sub, :name, :preferred_username, :profile, :picture
|
attributes :iss, :sub, :name, :preferred_username, :profile, :picture
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
%td.accounts-table__extra
|
%td.accounts-table__extra
|
||||||
- if account.local?
|
- if account.local?
|
||||||
- if account.user_email
|
- if account.user_email
|
||||||
= link_to account.user_email.split('@').last, admin_accounts_path(email: "%@#{account.user_email.split('@').last}"), title: account.user_email
|
= link_to account.user_email_domain, admin_accounts_path(email: "%@#{account.user_email_domain}"), title: account.user_email
|
||||||
- else
|
- else
|
||||||
\-
|
\-
|
||||||
%br/
|
%br/
|
||||||
|
|
|
@ -22,10 +22,10 @@
|
||||||
%td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= account.user_email
|
%td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= account.user_email
|
||||||
%td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(account.id) if can?(:change_email, account.user)
|
%td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(account.id) if can?(:change_email, account.user)
|
||||||
%tr
|
%tr
|
||||||
%td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{account.user_email.split('@').last}")
|
%td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{account.user_email_domain}")
|
||||||
- if can?(:create, :email_domain_block)
|
- if can?(:create, :email_domain_block)
|
||||||
%tr
|
%tr
|
||||||
%td= table_link_to 'hide_source', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: account.user_email.split('@').last)
|
%td= table_link_to 'hide_source', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: account.user_email_domain)
|
||||||
- if account.user_unconfirmed_email.present?
|
- if account.user_unconfirmed_email.present?
|
||||||
%tr
|
%tr
|
||||||
%th= t('admin.accounts.unconfirmed_email')
|
%th= t('admin.accounts.unconfirmed_email')
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
= date_range(@time_period)
|
= date_range(@time_period)
|
||||||
|
|
||||||
- if @instance.persisted?
|
- if @instance.persisted?
|
||||||
= render 'dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first
|
= render 'admin/instances/dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first
|
||||||
- else
|
- else
|
||||||
%p
|
%p
|
||||||
= t('admin.instances.unknown_instance')
|
= t('admin.instances.unknown_instance')
|
||||||
|
@ -55,6 +55,24 @@
|
||||||
= render partial: 'admin/action_logs/action_log', collection: @action_logs
|
= render partial: 'admin/action_logs/action_log', collection: @action_logs
|
||||||
= link_to t('admin.instances.audit_log.view_all'), admin_action_logs_path(target_domain: @instance.domain), class: 'button'
|
= link_to t('admin.instances.audit_log.view_all'), admin_action_logs_path(target_domain: @instance.domain), class: 'button'
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
- if @instance.domain.present?
|
||||||
|
%h3#instance-notes= t('admin.instances.moderation_notes.title')
|
||||||
|
%p= t('admin.instances.moderation_notes.description_html')
|
||||||
|
.report-notes
|
||||||
|
= render partial: 'admin/report_notes/report_note', collection: @instance_moderation_notes
|
||||||
|
|
||||||
|
= simple_form_for @instance_moderation_note, url: admin_instance_moderation_notes_path(instance_id: @instance.domain) do |form|
|
||||||
|
= render 'shared/error_messages', object: @instance_moderation_note
|
||||||
|
|
||||||
|
.field-group
|
||||||
|
= form.input :content, input_html: { placeholder: t('admin.instances.moderation_notes.placeholder'), maxlength: InstanceModerationNote::CONTENT_SIZE_LIMIT, rows: 6, autofocus: @instance_moderation_note.errors.any? }
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= form.button :button, t('admin.instances.moderation_notes.create'), type: :submit
|
||||||
|
|
||||||
|
- if @instance.persisted?
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
%h3= t('admin.instances.availability.title')
|
%h3= t('admin.instances.availability.title')
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
.report-notes__item
|
.report-notes__item{ id: dom_id(report_note) }
|
||||||
= image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
|
= image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
|
||||||
|
|
||||||
.report-notes__item__header
|
.report-notes__item__header
|
||||||
%span.username
|
%span.username
|
||||||
= link_to report_note.account.username, admin_account_path(report_note.account_id)
|
= link_to report_note.account.username, admin_account_path(report_note.account_id)
|
||||||
%time.relative-formatted{ datetime: report_note.created_at.iso8601, title: report_note.created_at }
|
%a.timestamp{ href: "##{dom_id(report_note)}" }
|
||||||
= l report_note.created_at.to_date
|
%time.relative-formatted{ datetime: report_note.created_at.iso8601, title: report_note.created_at }
|
||||||
|
= l report_note.created_at.to_date
|
||||||
|
|
||||||
.report-notes__item__content
|
.report-notes__item__content
|
||||||
= linkify(report_note.content)
|
= linkify(report_note.content)
|
||||||
|
@ -14,5 +15,7 @@
|
||||||
.report-notes__item__actions
|
.report-notes__item__actions
|
||||||
- if report_note.is_a?(AccountModerationNote)
|
- if report_note.is_a?(AccountModerationNote)
|
||||||
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_account_moderation_note_path(report_note), method: :delete
|
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_account_moderation_note_path(report_note), method: :delete
|
||||||
|
- elsif report_note.is_a?(InstanceModerationNote)
|
||||||
|
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_instance_moderation_note_path(instance_id: report_note.domain, id: report_note.id), method: :delete
|
||||||
- else
|
- else
|
||||||
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete
|
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
= f.input :language,
|
= f.input :language,
|
||||||
collection: ui_languages,
|
collection: ui_languages,
|
||||||
include_blank: false,
|
include_blank: false,
|
||||||
label_method: ->(locale) { native_locale_name(locale) }
|
label_method: ->(locale) { "#{native_locale_name(locale)} (#{standard_locale_name(locale)})" }
|
||||||
|
|
||||||
.fields-row__column.fields-group
|
.fields-row__column.fields-group
|
||||||
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
|
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
|
||||||
|
|
|
@ -47,7 +47,6 @@ require_relative '../lib/chewy/strategy/mastodon'
|
||||||
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
||||||
require_relative '../lib/rails/engine_extensions'
|
require_relative '../lib/rails/engine_extensions'
|
||||||
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
||||||
require_relative '../lib/stoplight/redis_data_store_extensions'
|
|
||||||
require_relative '../lib/active_record/database_tasks_extensions'
|
require_relative '../lib/active_record/database_tasks_extensions'
|
||||||
require_relative '../lib/active_record/batches'
|
require_relative '../lib/active_record/batches'
|
||||||
require_relative '../lib/simple_navigation/item_extensions'
|
require_relative '../lib/simple_navigation/item_extensions'
|
||||||
|
|
|
@ -20,6 +20,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
inflect.acronym 'DeepL'
|
inflect.acronym 'DeepL'
|
||||||
inflect.acronym 'DSL'
|
inflect.acronym 'DSL'
|
||||||
inflect.acronym 'JsonLd'
|
inflect.acronym 'JsonLd'
|
||||||
|
inflect.acronym 'OAuth'
|
||||||
inflect.acronym 'OEmbed'
|
inflect.acronym 'OEmbed'
|
||||||
inflect.acronym 'OStatus'
|
inflect.acronym 'OStatus'
|
||||||
inflect.acronym 'PubSubHubbub'
|
inflect.acronym 'PubSubHubbub'
|
||||||
|
|
|
@ -2,117 +2,117 @@
|
||||||
ru:
|
ru:
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Ваш адрес e-mail был успешно подтвержден.
|
confirmed: Ваш адрес электронной почты успешно подтверждён.
|
||||||
send_instructions: Вы получите e-mail с инструкцией по подтверждению вашего адреса e-mail в течение нескольких минут.
|
send_instructions: В течение нескольких минут вы получите письмо с инструкциями по подтверждению адреса электронной почты. Если письмо не приходит, проверьте папку «Спам».
|
||||||
send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail с инструкцией по подтверждению вашего адреса в течение нескольких минут.
|
send_paranoid_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по его подтверждению. Если письмо не приходит, проверьте папку «Спам».
|
||||||
failure:
|
failure:
|
||||||
already_authenticated: Вы уже вошли.
|
already_authenticated: Вы уже авторизованы.
|
||||||
inactive: Ваша учётная запись ещё не активирована.
|
inactive: Ваша учётная запись ещё не активирована.
|
||||||
invalid: Неверно введены %{authentication_keys} или пароль.
|
invalid: "%{authentication_keys} или пароль введены неверно."
|
||||||
last_attempt: У Вас есть последняя попытка, после чего вход будет заблокирован.
|
last_attempt: У вас осталась последняя попытка ввода пароля до блокировки учётной записи.
|
||||||
locked: Ваша учётная запись заблокирована.
|
locked: Ваша учётная запись заблокирована.
|
||||||
not_found_in_database: Неверно введены %{authentication_keys} или пароль.
|
not_found_in_database: "%{authentication_keys} или пароль введены неверно."
|
||||||
omniauth_user_creation_failure: Ошибка создания учетной записи с этим идентификатором.
|
omniauth_user_creation_failure: Не удалось создать учётную запись с помощью выбранного способа идентификации.
|
||||||
pending: Ваша заявка на вступление всё ещё рассматривается.
|
pending: Ваша заявка на регистрацию всё ещё рассматривается.
|
||||||
timeout: Ваша сессия истекла. Пожалуйста, войдите снова, чтобы продолжить.
|
timeout: Ваш сеанс закончился. Пожалуйста, войдите снова.
|
||||||
unauthenticated: Вам необходимо войти или зарегистрироваться.
|
unauthenticated: Вам необходимо войти или зарегистрироваться.
|
||||||
unconfirmed: Вам необходимо подтвердить ваш адрес e-mail для продолжения.
|
unconfirmed: Вы должны подтвердить свой адрес электронной почты.
|
||||||
mailer:
|
mailer:
|
||||||
confirmation_instructions:
|
confirmation_instructions:
|
||||||
action: Подтвердить e-mail адрес
|
action: Подтвердить
|
||||||
action_with_app: Подтвердить и вернуться в %{app}
|
action_with_app: Подтвердить и вернуться в %{app}
|
||||||
explanation: Вы создали учётную запись на сайте %{host}, используя этот e-mail адрес. Остался лишь один шаг для активации. Если это были не вы, просто игнорируйте письмо.
|
explanation: Вы создали учётную запись на сайте %{host}, используя этот адрес электронной почты. Остался лишь один шаг для её активации. Если это сделали не вы, просто проигнорируйте это письмо.
|
||||||
explanation_when_pending: Вы подали заявку на %{host}, используя этот адрес e-mail. Как только вы его подтвердите, мы начнём изучать вашу заявку. До тех пор вы не сможете войти на сайт. Если ваша заявка будет отклонена, все данные будут автоматически удалены, от вас не потребуется никаких дополнительных действий. Если это были не вы, пожалуйста, проигнорируйте данное письмо.
|
explanation_when_pending: Вы подали заявку, чтобы создать учётную запись на сайте %{host}, используя этот адрес электронной почты. После того как вы его подтвердите, мы начнём рассматривать вашу заявку. До тех пор вы сможете войти на сайт только для того, чтобы редактировать данные своей учётной записи или удалить её. Если ваша заявка будет отклонена, все данные будут автоматически удалены, от вас не потребуется никаких дополнительных действий. Если заявку подали не вы, пожалуйста, проигнорируйте это письмо.
|
||||||
extra_html: Пожалуйста, ознакомьтесь <a href="%{terms_path}">правилами узла</a> and <a href="%{policy_path}">условиями пользования Сервисом</a>.
|
extra_html: Пожалуйста, также ознакомьтесь с <a href="%{terms_path}">правилами сервера</a> и <a href="%{policy_path}">политикой конфиденциальности</a>.
|
||||||
subject: 'Mastodon: Инструкция по подтверждению на узле %{instance}'
|
subject: 'Mastodon: Инструкции по подтверждению учётной записи на %{instance}'
|
||||||
title: Подтвердите e-mail адрес
|
title: Подтверждение адреса электронной почты
|
||||||
email_changed:
|
email_changed:
|
||||||
explanation: 'E-mail адрес вашей учётной записи будет изменён на:'
|
explanation: 'Ваш адрес электронной почты будет изменён на:'
|
||||||
extra: Если вы не меняли e-mail адрес, возможно кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором узла, если вы уже потеряли доступ к ней.
|
extra: Если вы не меняли адрес электронной почты, возможно, кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором сервера, если вы уже потеряли к ней доступ.
|
||||||
subject: 'Mastodon: Изменён e-mail адрес'
|
subject: 'Mastodon: Адрес электронной почты изменён'
|
||||||
title: Новый адрес e-mail
|
title: Адрес электронной почты изменён
|
||||||
password_change:
|
password_change:
|
||||||
explanation: Пароль Вашей учётной записи был изменён.
|
explanation: Ваш пароль был изменён.
|
||||||
extra: Если вы не меняли пароль, возможно кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором узла, если вы уже потеряли доступ к ней.
|
extra: Если вы не меняли пароль, возможно, кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором сервера, если вы уже потеряли к ней доступ.
|
||||||
subject: 'Mastodon: Пароль изменен'
|
subject: 'Mastodon: Пароль изменён'
|
||||||
title: Пароль изменён
|
title: Пароль изменён
|
||||||
reconfirmation_instructions:
|
reconfirmation_instructions:
|
||||||
explanation: Для завершения смены e-mail, нажмите кнопку ниже.
|
explanation: Чтобы завершить изменение адреса электронной почты, подтвердите новый адрес.
|
||||||
extra: Если вы не изменяли e-mail, пожалуйста, игнорируйте это письмо. Новый адрес не будет привязан к учётной записи, пока вы не перейдёте по ссылке ниже.
|
extra: Если запрос инициировали не вы, пожалуйста, проигнорируйте это письмо. Новый адрес не будет привязан к учётной записи, пока вы не перейдёте по ссылке выше.
|
||||||
subject: 'Mastodon: Подтвердите свой новый e-mail на %{instance}'
|
subject: 'Mastodon: Подтвердите новый адрес электронной почты на %{instance}'
|
||||||
title: Подтвердите e-mail адрес
|
title: Подтверждение адреса электронной почты
|
||||||
reset_password_instructions:
|
reset_password_instructions:
|
||||||
action: Смена пароля
|
action: Сменить пароль
|
||||||
explanation: Вы запросили новый пароль для вашей учётной записи.
|
explanation: Вы запросили новый пароль для вашей учётной записи.
|
||||||
extra: Если это сделали не вы, пожалуйста, игнорируйте письмо. Ваш пароль не будет изменён, пока вы не перейдёте по ссылке выше и не создадите новый пароль.
|
extra: Если вы не запрашивали изменение пароля, проигнорируйте это письмо. Ваш пароль не будет изменён, пока вы не перейдёте по ссылке и не введёте новый пароль.
|
||||||
subject: 'Mastodon: Инструкция по сбросу пароля'
|
subject: 'Mastodon: Инструкции по восстановлению пароля'
|
||||||
title: Сброс пароля
|
title: Восстановление пароля
|
||||||
two_factor_disabled:
|
two_factor_disabled:
|
||||||
explanation: Вход в систему теперь возможен только с использованием адреса электронной почты и пароля.
|
explanation: Теперь вход возможен с использованием одних лишь адреса электронной почты и пароля.
|
||||||
subject: 'Mastodon: Двухфакторная авторизация отключена'
|
subject: 'Mastodon: Двухфакторная аутентификация отключена'
|
||||||
subtitle: Двухфакторная аутентификация для вашей учетной записи была отключена.
|
subtitle: Двухфакторная аутентификация отключена для вашей учетной записи.
|
||||||
title: 2ФА отключена
|
title: 2FA отключена
|
||||||
two_factor_enabled:
|
two_factor_enabled:
|
||||||
explanation: Для входа в систему потребуется токен, сгенерированный сопряженным приложением TOTP.
|
explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением TOTP.
|
||||||
subject: 'Mastodon: Настроена двухфакторная авторизация'
|
subject: 'Mastodon: Двухфакторная аутентификация включена'
|
||||||
subtitle: Для вашей учетной записи была включена двухфакторная аутентификация.
|
subtitle: Двухфакторная аутентификация включена для вашей учётной записи.
|
||||||
title: 2ФА включена
|
title: 2FA включена
|
||||||
two_factor_recovery_codes_changed:
|
two_factor_recovery_codes_changed:
|
||||||
explanation: Предыдущие резервные коды были аннулированы и созданы новые.
|
explanation: Прежние резервные коды были аннулированы, и были созданы новые.
|
||||||
subject: 'Mastodon: Резервные коды двуфакторной авторизации обновлены'
|
subject: 'Mastodon: Резервные коды двухфакторной аутентификации пересозданы'
|
||||||
subtitle: Предыдущие коды восстановления были аннулированы и сгенерированы новые.
|
subtitle: Прежние резервные коды были аннулированы, и были созданы новые.
|
||||||
title: Коды восстановления 2FA изменены
|
title: Резервные коды 2FA изменены
|
||||||
unlock_instructions:
|
unlock_instructions:
|
||||||
subject: 'Mastodon: Инструкция по разблокировке'
|
subject: 'Mastodon: Инструкции по снятию блокировки учётной записи'
|
||||||
webauthn_credential:
|
webauthn_credential:
|
||||||
added:
|
added:
|
||||||
explanation: Следующий ключ безопасности был добавлен в вашу учётную запись
|
explanation: Новый электронный ключ добавлен для вашей учётной записи
|
||||||
subject: 'Мастодон: Новый ключ безопасности'
|
subject: 'Mastodon: Новый электронный ключ'
|
||||||
title: Был добавлен новый ключ безопасности
|
title: Добавлен новый электронный ключ
|
||||||
deleted:
|
deleted:
|
||||||
explanation: Следующий ключ безопасности был удален из вашей учётной записи
|
explanation: Один из ваших электронных ключей удалён и больше не сможет быть использован для входа в вашу учётную запись
|
||||||
subject: 'Мастодон: Ключ Безопасности удален'
|
subject: 'Mastodon: Электронный ключ удалён'
|
||||||
title: Один из ваших защитных ключей был удален
|
title: Один из ваших электронных ключей удалён
|
||||||
webauthn_disabled:
|
webauthn_disabled:
|
||||||
explanation: Аутентификация с помощью ключей безопасности была отключена для вашей учетной записи.
|
explanation: Аутентификация по электронным ключам деактивирована для вашей учетной записи.
|
||||||
extra: Теперь вход в систему возможен только с использованием токена, сгенерированного сопряженным приложением TOTP.
|
extra: Теперь вход возможен с использованием только лишь одноразового кода, сгенерированного сопряжённым приложением TOTP.
|
||||||
subject: 'Мастодон: Аутентификация с ключами безопасности отключена'
|
subject: 'Mastodon: Аутентификация по электронным ключам деактивирована'
|
||||||
title: Ключи безопасности отключены
|
title: Вход по электронным ключам деактивирован
|
||||||
webauthn_enabled:
|
webauthn_enabled:
|
||||||
explanation: Для вашей учетной записи включена аутентификация по ключу безопасности.
|
explanation: Аутентификация по электронным ключам активирована для вашей учетной записи.
|
||||||
extra: Теперь ваш ключ безопасности можно использовать для входа в систему.
|
extra: Теперь ваш электронный ключ можно использовать для входа.
|
||||||
subject: 'Мастодон: Включена аутентификация по ключу безопасности'
|
subject: 'Mastodon: Аутентификация по электронным ключам активирована'
|
||||||
title: Ключи безопасности включены
|
title: Вход по электронным ключам активирован
|
||||||
omniauth_callbacks:
|
omniauth_callbacks:
|
||||||
failure: Не получилось аутентифицировать вас с помощью %{kind} по следующей причине - "%{reason}".
|
failure: Вы не можете войти под учётной записью %{kind}, так как «%{reason}».
|
||||||
success: Аутентификация с помощью учётной записи %{kind} прошла успешно.
|
success: Вход выполнен под учётной записью %{kind}.
|
||||||
passwords:
|
passwords:
|
||||||
no_token: Вы можете получить доступ к этой странице, только перейдя по ссылке в e-mail для сброса пароля. Если вы действительно перешли по такой ссылке, пожалуйста, удостоверьтесь, что ссылка была введена полностью и без изменений.
|
no_token: Доступ к этой странице возможен только по ссылке из письма о восстановлении пароля. Если вы перешли по такой ссылке, пожалуйста, убедитесь, что вы скопировали всю ссылку целиком.
|
||||||
send_instructions: Вы получите e-mail с инструкцией по сбросу пароля в течение нескольких минут.
|
send_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по восстановлению пароля. Если письмо не приходит, проверьте папку «Спам».
|
||||||
send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail со ссылкой для сброса пароля в течение нескольких минут.
|
send_paranoid_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по восстановлению пароля. Если письмо не приходит, проверьте папку «Спам».
|
||||||
updated: Ваш пароль был успешно изменен. Вход выполнен.
|
updated: Ваш пароль изменён. Теперь вы авторизованы.
|
||||||
updated_not_active: Ваш пароль был успешно изменен.
|
updated_not_active: Ваш пароль изменён.
|
||||||
registrations:
|
registrations:
|
||||||
destroyed: До свидания! Ваша учётная запись была успешно удалена. Мы надеемся скоро увидеть вас снова.
|
destroyed: До свидания! Ваша учётная запись удалена. Надеемся увидеть вас снова.
|
||||||
update_needs_confirmation: Данные учётной записи обновлены, но нам необходимо подтвердить ваш новый e-mail адрес. Проверьте почту и перейдите по ссылке из письма. Если оно не приходит, проверьте папку «спам».
|
update_needs_confirmation: Вы успешно обновили данные своей учётной записи, но необходимо подтвердить новый адрес электронной почты. Пожалуйста, проверьте свой почтовый ящик и перейдите по ссылке, чтобы закончить процедуру проверки нового адреса. Если письмо не приходит, проверьте папку «Спам».
|
||||||
updated: Ваша учётная запись успешно обновлена.
|
updated: Ваша учётная запись обновлена.
|
||||||
sessions:
|
sessions:
|
||||||
already_signed_out: Выход прошел успешно.
|
already_signed_out: Выход выполнен.
|
||||||
signed_in: Вход прошел успешно.
|
signed_in: Вход выполнен.
|
||||||
signed_out: Выход прошел успешно.
|
signed_out: Выход выполнен.
|
||||||
unlocks:
|
unlocks:
|
||||||
send_instructions: Вы получите e-mail с инструкцией по разблокировке вашей учётной записи в течение нескольких минут.
|
send_instructions: В течение нескольких минут вы получите письмо с инструкциями по разблокировке учётной записи. Если письмо не приходит, проверьте папку «Спам».
|
||||||
send_paranoid_instructions: Если ваша учётная запись существует, вы получите e-mail с инструкцией по её разблокировке в течение нескольких минут.
|
send_paranoid_instructions: Если ваша учётная запись существует, то в течение нескольких минут вы получите письмо с инструкциями по её разблокировке. Если письмо не приходит, проверьте папку «Спам».
|
||||||
unlocked: Ваша учётная запись был успешно разблокирована. Пожалуйста, войдите для продолжения.
|
unlocked: Ваша учётная запись разблокирована. Теперь вы можете войти.
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
already_confirmed: уже подтвержден, пожалуйста, попробуйте войти
|
already_confirmed: уже подтверждён. Пожалуйста, попробуйте войти
|
||||||
confirmation_period_expired: не был подтвержден в течение %{period}, пожалуйста, запросите новый
|
confirmation_period_expired: не был подтверждён в течение %{period}. Пожалуйста, повторите запрос на подтверждение
|
||||||
expired: истек, пожалуйста, запросите новый
|
expired: истёк. Пожалуйста, запросите новый код
|
||||||
not_found: не найден
|
not_found: не найден
|
||||||
not_locked: не был заблокирован
|
not_locked: не заблокирован
|
||||||
not_saved:
|
not_saved:
|
||||||
few: "%{count} ошибки помешали сохранению этого %{resource}:"
|
few: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
|
||||||
many: "%{count} ошибок помешали сохранению этого %{resource}:"
|
many: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
|
||||||
one: '1 ошибка помешала сохранению этого %{resource}:'
|
one: "%{resource}: сохранение не удалось из-за %{count} ошибки:"
|
||||||
other: "%{count} ошибок помешали сохранению этого %{resource}:"
|
other: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
|
||||||
|
|
|
@ -15,43 +15,43 @@ ru:
|
||||||
fragment_present: не может содержать фрагмент.
|
fragment_present: не может содержать фрагмент.
|
||||||
invalid_uri: должен быть правильным URI.
|
invalid_uri: должен быть правильным URI.
|
||||||
relative_uri: должен быть абсолютным URI.
|
relative_uri: должен быть абсолютным URI.
|
||||||
secured_uri: нужен HTTPS/SSL URI.
|
secured_uri: должен быть HTTPS/SSL URI.
|
||||||
doorkeeper:
|
doorkeeper:
|
||||||
applications:
|
applications:
|
||||||
buttons:
|
buttons:
|
||||||
authorize: Авторизовать
|
authorize: Авторизовать
|
||||||
cancel: Отменить
|
cancel: Отмена
|
||||||
destroy: Удалить
|
destroy: Удалить
|
||||||
edit: Изменить
|
edit: Редактировать
|
||||||
submit: Принять
|
submit: Готово
|
||||||
confirmations:
|
confirmations:
|
||||||
destroy: Вы уверены?
|
destroy: Вы уверены?
|
||||||
edit:
|
edit:
|
||||||
title: Изменить приложение
|
title: Редактирование приложения
|
||||||
form:
|
form:
|
||||||
error: Ой! Проверьте Вашу форму на возможные ошибки
|
error: Упс! Проверьте форму на наличие ошибок
|
||||||
help:
|
help:
|
||||||
native_redirect_uri: Используйте %{native_redirect_uri} для локального тестирования
|
native_redirect_uri: Используйте %{native_redirect_uri} для локального тестирования
|
||||||
redirect_uri: Используйте по одной строке на URI
|
redirect_uri: По одному URI на строку
|
||||||
scopes: Разделяйте список разрешений пробелами. Оставьте незаполненным для использования разрешений по умолчанию.
|
scopes: Разделяйте список разрешений пробелами. Оставьте незаполненным для использования разрешений по умолчанию.
|
||||||
index:
|
index:
|
||||||
application: Приложение
|
application: Приложение
|
||||||
callback_url: URL-адреса обратного вызова
|
callback_url: Callback-адрес URL
|
||||||
delete: Удалить
|
delete: Удалить
|
||||||
empty: У вас нет созданных приложений.
|
empty: Вы ещё не создали ни одного приложения.
|
||||||
name: Название
|
name: Название
|
||||||
new: Новое приложение
|
new: Создать приложение
|
||||||
scopes: Разрешения
|
scopes: Разрешения
|
||||||
show: Показывать
|
show: Открыть
|
||||||
title: Ваши приложения
|
title: Ваши приложения
|
||||||
new:
|
new:
|
||||||
title: Создание приложения
|
title: Создать приложение
|
||||||
show:
|
show:
|
||||||
actions: Действия
|
actions: Действия
|
||||||
application_id: Ключ клиента
|
application_id: ID приложения
|
||||||
callback_urls: URL-адреса обратного вызова
|
callback_urls: Callback-адреса URL
|
||||||
scopes: Разрешения
|
scopes: Разрешения
|
||||||
secret: Секрет
|
secret: Секретный ключ
|
||||||
title: 'Приложение: %{name}'
|
title: 'Приложение: %{name}'
|
||||||
authorizations:
|
authorizations:
|
||||||
buttons:
|
buttons:
|
||||||
|
@ -60,47 +60,47 @@ ru:
|
||||||
error:
|
error:
|
||||||
title: Произошла ошибка
|
title: Произошла ошибка
|
||||||
new:
|
new:
|
||||||
prompt_html: "%{client_name} хочет получить доступ к вашему аккаунту. <strong>Принимайте запрос только в том случае, если узнаёте, откуда он, и доверяете источнику.</strong>"
|
prompt_html: Приложение %{client_name} запрашивает доступ к вашей учётной записи. <strong>Не принимайте этот запрос, если он исходит из незнакомого или недоверенного источника.</strong>
|
||||||
review_permissions: Просмотр разрешений
|
review_permissions: Запрашиваемые разрешения
|
||||||
title: Требуется авторизация
|
title: Требуется авторизация
|
||||||
show:
|
show:
|
||||||
title: Скопируйте этот код авторизации и вставьте его в приложении.
|
title: Скопируйте этот код авторизации и вставьте его в приложение.
|
||||||
authorized_applications:
|
authorized_applications:
|
||||||
buttons:
|
buttons:
|
||||||
revoke: Отозвать авторизацию
|
revoke: Отозвать доступ
|
||||||
confirmations:
|
confirmations:
|
||||||
revoke: Вы уверены?
|
revoke: Вы уверены?
|
||||||
index:
|
index:
|
||||||
authorized_at: Доступ получен %{date}
|
authorized_at: Доступ получен %{date}
|
||||||
description_html: Это приложения, которые могут получить доступ к вашей учетной записи с помощью API. Если здесь есть приложения, которые вы не узнаете, или приложения, работающие неправильно, вы можете отозвать их доступ.
|
description_html: Это приложения, которые могут получить доступ к вашей учётной записи с помощью API. Если здесь есть приложения, которые вы не узнаёте, или приложения, работающие неправильно, вы можете отозвать им доступ.
|
||||||
last_used_at: Последнее использование %{date}
|
last_used_at: В последний раз использовалось %{date}
|
||||||
never_used: Не использовалось
|
never_used: Не использовалось
|
||||||
scopes: Разрешения
|
scopes: Разрешения
|
||||||
superapp: Внутреннее
|
superapp: Служебное приложение
|
||||||
title: Ваши авторизованные приложения
|
title: Связанные приложения
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
access_denied: Владелец ресурса или сервер авторизации ответил отказом на Ваш запрос.
|
access_denied: Владелец ресурса или сервер авторизации ответил отказом на ваш запрос.
|
||||||
credential_flow_not_configured: Поток с предоставлением клиенту пароля завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_from_credentials не был сконфигурирован.
|
credential_flow_not_configured: Процесс Resource Owner Password Credentials завершился неудачей, поскольку параметр конфигурации Doorkeeper.configure.resource_owner_from_credentials не был задан.
|
||||||
invalid_client: Клиентская аутентификация завершилась неудачей (неизвестный клиент, не включена клиентская аутентификация, или метод аутентификации не поддерживается.
|
invalid_client: 'Не удалось аутентифицировать клиент по одной из следующих причин: неизвестный клиент; отсутствует аутентификация клиента; неподдерживаемый метод аутентификации.'
|
||||||
invalid_code_challenge_method: Метод проверки кода должен быть S256, простой не годится.
|
invalid_code_challenge_method: Функция хеширования для механизма PKCE должна быть установлена в значение S256, метод PLAIN не поддерживается.
|
||||||
invalid_grant: Предоставленный доступ некорректен, истек, отозван, не совпадает с URI перенаправления, использованным в запросе авторизации, или был выпущен для другого клиента.
|
invalid_grant: Предоставленное разрешение на авторизацию либо недействительно, либо истекло, либо отозвано, либо не соответствует использованному в запросе на авторизацию URI перенаправления, либо было выдано для другого клиента.
|
||||||
invalid_redirect_uri: Включенный URI перенаправления некорректен.
|
invalid_redirect_uri: Предоставленный URI перенаправления недействителен.
|
||||||
invalid_request:
|
invalid_request:
|
||||||
missing_param: 'Отсутствует обязательный параметр: %{value}.'
|
missing_param: 'Отсутствует обязательный параметр: %{value}.'
|
||||||
request_not_authorized: Запрос должен быть авторизован. Обязательный параметр для авторизации запроса отсутствует или недействителен.
|
request_not_authorized: Запрос должен быть авторизован. Обязательный параметр для авторизации запроса отсутствует или недействителен.
|
||||||
unknown: В запросе отсутствует обязательный параметр, включено неподдерживаемое значение параметра или он имеет иной формат.
|
unknown: В запросе отсутствует обязательный параметр либо присутствует неподдерживаемое значение параметра, или запрос является недействительным по какой-либо ещё причине.
|
||||||
invalid_resource_owner: Предоставленные данные владельца ресурса некорректны, или владелец ресурса не может быть найден
|
invalid_resource_owner: Предоставленные данные владельца ресурса недействительны, или владелец ресурса не найден
|
||||||
invalid_scope: Запрошенное разрешение некорректно, неизвестно или неверно сформировано.
|
invalid_scope: Запрошенное разрешение недействительно, неизвестно или имеет неправильный формат.
|
||||||
invalid_token:
|
invalid_token:
|
||||||
expired: Токен доступа истек
|
expired: Срок действия токена доступа истёк
|
||||||
revoked: Токен доступа был отменен
|
revoked: Токен доступа был отозван
|
||||||
unknown: Токен доступа некорректен
|
unknown: Токен доступа недействителен
|
||||||
resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_authenticator не был сконфигурирован.
|
resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр конфигурации Doorkeeper.configure.resource_owner_authenticator не был задан.
|
||||||
server_error: Сервер авторизации встретился с неожиданной ошибкой, не позволившей ему выполнить запрос.
|
server_error: На сервере авторизации произошла непредвиденная ошибка, не позволившая ему выполнить запрос.
|
||||||
temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или профилактики.
|
temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или технического обслуживания.
|
||||||
unauthorized_client: Клиент не авторизован для выполнения этого запроса с использованием этого метода.
|
unauthorized_client: Клиент не авторизован для выполнения этого запроса с использованием этого метода.
|
||||||
unsupported_grant_type: Тип авторизации не поддерживается сервером авторизации.
|
unsupported_grant_type: Тип разрешения на авторизацию не поддерживается сервером авторизации.
|
||||||
unsupported_response_type: Сервер авторизации не поддерживает этот тип ответа.
|
unsupported_response_type: Сервер авторизации не поддерживает этот тип ответа.
|
||||||
flash:
|
flash:
|
||||||
applications:
|
applications:
|
||||||
|
@ -112,33 +112,33 @@ ru:
|
||||||
notice: Приложение обновлено.
|
notice: Приложение обновлено.
|
||||||
authorized_applications:
|
authorized_applications:
|
||||||
destroy:
|
destroy:
|
||||||
notice: Авторизация приложения отозвана.
|
notice: Приложению был отозван доступ.
|
||||||
grouped_scopes:
|
grouped_scopes:
|
||||||
access:
|
access:
|
||||||
read: Доступ только для чтения
|
read: Доступ только для чтения
|
||||||
read/write: Доступ на чтение и запись
|
read/write: Доступ для чтения и записи
|
||||||
write: Доступ только для записи
|
write: Доступ только для записи
|
||||||
title:
|
title:
|
||||||
accounts: Учётные записи
|
accounts: Учётные записи
|
||||||
admin/accounts: Управление учётными записями
|
admin/accounts: Управление учётными записями пользователей
|
||||||
admin/all: Все административные функции
|
admin/all: Все административные функции
|
||||||
admin/reports: Управление отчётами
|
admin/reports: Управление жалобами
|
||||||
all: Полный доступ к вашей учетной записи Mastodon
|
all: Полный доступ к вашей учётной записи Mastodon
|
||||||
blocks: Блокировки
|
blocks: Блокировки
|
||||||
bookmarks: Закладки
|
bookmarks: Закладки
|
||||||
conversations: Диалоги
|
conversations: Беседы
|
||||||
crypto: Сквозное шифрование
|
crypto: Сквозное шифрование
|
||||||
favourites: Избранные
|
favourites: Избранное
|
||||||
filters: Фильтры
|
filters: Фильтры
|
||||||
follow: Подписки, заглушенные и заблокированные
|
follow: Подписки, а также списки игнорируемых и заблокированных пользователей
|
||||||
follows: Подписки
|
follows: Подписки
|
||||||
lists: Списки
|
lists: Списки
|
||||||
media: Медиафайлы
|
media: Медиафайлы
|
||||||
mutes: Игнорирует
|
mutes: Игнорируемые пользователи
|
||||||
notifications: Уведомления
|
notifications: Уведомления
|
||||||
profile: Ваш профиль Mastodon
|
profile: Ваш профиль Mastodon
|
||||||
push: Push-уведомления
|
push: Push-уведомления
|
||||||
reports: Обращения
|
reports: Жалобы
|
||||||
search: Поиск
|
search: Поиск
|
||||||
statuses: Посты
|
statuses: Посты
|
||||||
layouts:
|
layouts:
|
||||||
|
@ -150,49 +150,49 @@ ru:
|
||||||
title: Требуется авторизация OAuth
|
title: Требуется авторизация OAuth
|
||||||
scopes:
|
scopes:
|
||||||
admin:read: читать все данные на сервере
|
admin:read: читать все данные на сервере
|
||||||
admin:read:accounts: читать конфиденциальную информацию всех учётных записей
|
admin:read:accounts: читать конфиденциальные сведения обо всех учётных записях
|
||||||
admin:read:canonical_email_blocks: чтение конфиденциальной информации всех канонических блоков электронной почты
|
admin:read:canonical_email_blocks: читать конфиденциальные сведения обо всех блокировках по каноническому адресу электронной почты
|
||||||
admin:read:domain_allows: чтение конфиденциальной информации для всего домена позволяет
|
admin:read:domain_allows: читать конфиденциальные сведения обо всех разрешённых доменах
|
||||||
admin:read:domain_blocks: чтение конфиденциальной информации для всего домена позволяет
|
admin:read:domain_blocks: читать конфиденциальные сведения обо всех заблокированных доменах
|
||||||
admin:read:email_domain_blocks: читать конфиденциальную информацию обо всех блоках домена электронной почты
|
admin:read:email_domain_blocks: читать конфиденциальные сведения обо всех блокировках по домену электронной почты
|
||||||
admin:read:ip_blocks: читать конфиденциальную информацию обо всех IP-блоках
|
admin:read:ip_blocks: читать конфиденциальные сведения обо всех блокировках по IP-адресу
|
||||||
admin:read:reports: читать конфиденциальную информацию о всех жалобах и учётных записях с жалобами
|
admin:read:reports: читать конфиденциальные сведения обо всех жалобах и учётных записях с жалобами
|
||||||
admin:write: модифицировать все данные на сервере
|
admin:write: вносить изменения во все данные на сервере
|
||||||
admin:write:accounts: производить модерацию учётных записей
|
admin:write:accounts: осуществлять модерацию применительно к учётным записям
|
||||||
admin:write:canonical_email_blocks: выполнять действия по модерации канонических блоков электронной почты
|
admin:write:canonical_email_blocks: осуществлять модерацию применительно к блокировкам по каноническому адресу электронной почты
|
||||||
admin:write:domain_allows: производить модерацию учётных записей
|
admin:write:domain_allows: осуществлять модерацию применительно к разрешённым доменам
|
||||||
admin:write:domain_blocks: выполнять модерационные действия над блокировкой домена
|
admin:write:domain_blocks: осуществлять модерацию применительно к заблокированным доменам
|
||||||
admin:write:email_domain_blocks: выполнять действия по модерации блоков домена электронной почты
|
admin:write:email_domain_blocks: осуществлять модерацию применительно к блокировкам по домену электронной почты
|
||||||
admin:write:ip_blocks: выполнять модерационные действия над блокировками IP
|
admin:write:ip_blocks: осуществлять модерацию применительно к блокировкам по IP-адресу
|
||||||
admin:write:reports: производить модерацию жалоб
|
admin:write:reports: осуществлять модерацию применительно к жалобам
|
||||||
crypto: использ. сквозное шифрование
|
crypto: использовать сквозное шифрование
|
||||||
follow: управлять подписками и списком блокировок
|
follow: вносить изменения в отношения с другими пользователями
|
||||||
profile: данные вашего профиля только для чтения
|
profile: читать исключительно сведения о вашем профиле
|
||||||
push: получать push-уведомления
|
push: получать push-уведомления
|
||||||
read: просматривать данные вашей учётной записи
|
read: читать данные вашей учётной записи
|
||||||
read:accounts: видеть информацию об учётных записях
|
read:accounts: иметь доступ к информации об учётных записях
|
||||||
read:blocks: видеть ваши блокировки
|
read:blocks: иметь доступ к вашим блокировкам
|
||||||
read:bookmarks: видеть ваши закладки
|
read:bookmarks: иметь доступ к вашим закладкам
|
||||||
read:favourites: видеть ваше избранное
|
read:favourites: иметь доступ к списку постов, которые вы добавили в избранное
|
||||||
read:filters: видеть ваши фильтры
|
read:filters: иметь доступ к вашим фильтрам
|
||||||
read:follows: видеть ваши подписки
|
read:follows: иметь доступ к вашим подпискам
|
||||||
read:lists: видеть ваши списки
|
read:lists: иметь доступ к вашим спискам
|
||||||
read:mutes: смотреть список игнорируемых
|
read:mutes: иметь доступ к списку пользователей, которых вы игнорируете
|
||||||
read:notifications: получать уведомления
|
read:notifications: иметь доступ к вашим уведомлениям
|
||||||
read:reports: видеть ваши жалобы
|
read:reports: иметь доступ к вашим жалобам
|
||||||
read:search: использовать поиск
|
read:search: использовать поиск
|
||||||
read:statuses: видеть все ваши посты
|
read:statuses: иметь доступ ко всем постам
|
||||||
write: изменять все данные вашей учётной записи
|
write: вносить изменения во все данные вашей учётной записи
|
||||||
write:accounts: редактировать ваш профиль
|
write:accounts: вносить изменения в ваш профиль
|
||||||
write:blocks: блокировать учётные записи и домены
|
write:blocks: блокировать учётные записи и домены
|
||||||
write:bookmarks: добавлять посты в закладки
|
write:bookmarks: добавлять посты в закладки
|
||||||
write:conversations: игнорировать и удалить разговоры
|
write:conversations: игнорировать и удалять беседы
|
||||||
write:favourites: добавить посты в избранное
|
write:favourites: добавлять посты в избранное
|
||||||
write:filters: создавать фильтры
|
write:filters: создавать фильтры
|
||||||
write:follows: подписываться на людей
|
write:follows: подписываться на людей
|
||||||
write:lists: создавать списки
|
write:lists: создавать списки
|
||||||
write:media: загружать медиафайлы
|
write:media: загружать медиафайлы
|
||||||
write:mutes: игнорировать людей и обсуждения
|
write:mutes: игнорировать людей и обсуждения
|
||||||
write:notifications: очищать список уведомлений
|
write:notifications: очищать список уведомлений
|
||||||
write:reports: отправлять жалобы на других
|
write:reports: отправлять жалобы на других пользователей
|
||||||
write:statuses: публиковать посты
|
write:statuses: публиковать посты
|
||||||
|
|
|
@ -578,6 +578,13 @@ en:
|
||||||
all: All
|
all: All
|
||||||
limited: Limited
|
limited: Limited
|
||||||
title: Moderation
|
title: Moderation
|
||||||
|
moderation_notes:
|
||||||
|
create: Add Moderation Note
|
||||||
|
created_msg: Instance moderation note successfully created!
|
||||||
|
description_html: View and leave notes for other moderators and your future self
|
||||||
|
destroyed_msg: Instance moderation note successfully deleted!
|
||||||
|
placeholder: Information about this instance, actions taken, or anything else that will help you moderate this instance in the future.
|
||||||
|
title: Moderation Notes
|
||||||
private_comment: Private comment
|
private_comment: Private comment
|
||||||
public_comment: Public comment
|
public_comment: Public comment
|
||||||
purge: Purge
|
purge: Purge
|
||||||
|
|
|
@ -91,6 +91,8 @@ namespace :admin do
|
||||||
post :restart_delivery
|
post :restart_delivery
|
||||||
post :stop_delivery
|
post :stop_delivery
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :moderation_notes, controller: 'instances/moderation_notes', only: [:create, :destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :rules, only: [:index, :new, :create, :edit, :update, :destroy] do
|
resources :rules, only: [:index, :new, :create, :edit, :update, :destroy] do
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddSuperappToOauthApplications < ActiveRecord::Migration[5.0]
|
class AddSuperappToOAuthApplications < ActiveRecord::Migration[5.0]
|
||||||
def change
|
def change
|
||||||
add_column :oauth_applications, :superapp, :boolean, default: false, null: false
|
add_column :oauth_applications, :superapp, :boolean, default: false, null: false
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddWebsiteToOauthApplication < ActiveRecord::Migration[5.0]
|
class AddWebsiteToOAuthApplication < ActiveRecord::Migration[5.0]
|
||||||
def change
|
def change
|
||||||
add_column :oauth_applications, :website, :string
|
add_column :oauth_applications, :website, :string
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddLastUsedAtToOauthAccessTokens < ActiveRecord::Migration[6.1]
|
class AddLastUsedAtToOAuthAccessTokens < ActiveRecord::Migration[6.1]
|
||||||
def change
|
def change
|
||||||
safety_assured do
|
safety_assured do
|
||||||
change_table(:oauth_access_tokens, bulk: true) do |t|
|
change_table(:oauth_access_tokens, bulk: true) do |t|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateInstanceModerationNotes < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :instance_moderation_notes do |t|
|
||||||
|
t.string :domain, null: false
|
||||||
|
t.belongs_to :account, foreign_key: { on_delete: :cascade }, index: false, null: false
|
||||||
|
t.text :content
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
|
||||||
|
t.index ['domain'], name: 'index_instance_moderation_notes_on_domain'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
class OptimizeNullIndexOauthAccessTokensRefreshToken < ActiveRecord::Migration[5.2]
|
class OptimizeNullIndexOAuthAccessTokensRefreshToken < ActiveRecord::Migration[5.2]
|
||||||
include Mastodon::MigrationHelpers
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
class OptimizeNullIndexOauthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2]
|
class OptimizeNullIndexOAuthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2]
|
||||||
include Mastodon::MigrationHelpers
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
20
db/schema.rb
20
db/schema.rb
|
@ -191,8 +191,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
||||||
t.boolean "hide_collections"
|
t.boolean "hide_collections"
|
||||||
t.integer "avatar_storage_schema_version"
|
t.integer "avatar_storage_schema_version"
|
||||||
t.integer "header_storage_schema_version"
|
t.integer "header_storage_schema_version"
|
||||||
t.datetime "sensitized_at", precision: nil
|
|
||||||
t.integer "suspension_origin"
|
t.integer "suspension_origin"
|
||||||
|
t.datetime "sensitized_at", precision: nil
|
||||||
t.boolean "trendable"
|
t.boolean "trendable"
|
||||||
t.datetime "reviewed_at", precision: nil
|
t.datetime "reviewed_at", precision: nil
|
||||||
t.datetime "requested_review_at", precision: nil
|
t.datetime "requested_review_at", precision: nil
|
||||||
|
@ -580,6 +580,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
||||||
t.index ["user_id"], name: "index_identities_on_user_id"
|
t.index ["user_id"], name: "index_identities_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "instance_moderation_notes", force: :cascade do |t|
|
||||||
|
t.string "domain", null: false
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.text "content"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["domain"], name: "index_instance_moderation_notes_on_domain"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "invites", force: :cascade do |t|
|
create_table "invites", force: :cascade do |t|
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.string "code", default: "", null: false
|
t.string "code", default: "", null: false
|
||||||
|
@ -595,12 +604,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "ip_blocks", force: :cascade do |t|
|
create_table "ip_blocks", force: :cascade do |t|
|
||||||
t.inet "ip", default: "0.0.0.0", null: false
|
|
||||||
t.integer "severity", default: 0, null: false
|
|
||||||
t.datetime "expires_at", precision: nil
|
|
||||||
t.text "comment", default: "", null: false
|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.datetime "expires_at", precision: nil
|
||||||
|
t.inet "ip", default: "0.0.0.0", null: false
|
||||||
|
t.integer "severity", default: 0, null: false
|
||||||
|
t.text "comment", default: "", null: false
|
||||||
t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
|
t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1372,6 +1381,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
||||||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
||||||
add_foreign_key "generated_annual_reports", "accounts"
|
add_foreign_key "generated_annual_reports", "accounts"
|
||||||
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
||||||
|
add_foreign_key "instance_moderation_notes", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "invites", "users", on_delete: :cascade
|
add_foreign_key "invites", "users", on_delete: :cascade
|
||||||
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
|
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
|
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
|
||||||
|
|
|
@ -251,8 +251,7 @@ export default tseslint.config([
|
||||||
devDependencies: [
|
devDependencies: [
|
||||||
'eslint.config.mjs',
|
'eslint.config.mjs',
|
||||||
'app/javascript/mastodon/performance.js',
|
'app/javascript/mastodon/performance.js',
|
||||||
'app/javascript/mastodon/test_setup.js',
|
'app/javascript/testing/**/*',
|
||||||
'app/javascript/mastodon/test_helpers.tsx',
|
|
||||||
'app/javascript/**/__tests__/**',
|
'app/javascript/**/__tests__/**',
|
||||||
'app/javascript/**/*.stories.ts',
|
'app/javascript/**/*.stories.ts',
|
||||||
'app/javascript/**/*.stories.tsx',
|
'app/javascript/**/*.stories.tsx',
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Restore compatibility with Redis < 6.2
|
|
||||||
|
|
||||||
module Stoplight
|
|
||||||
module DataStore
|
|
||||||
module RedisExtensions
|
|
||||||
def query_failures(light, transaction: @redis)
|
|
||||||
window_start = Time.now.to_i - light.window_size
|
|
||||||
|
|
||||||
transaction.zrevrangebyscore(failures_key(light), Float::INFINITY, window_start)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Stoplight::DataStore::Redis.prepend(Stoplight::DataStore::RedisExtensions)
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Oauth::AuthorizationsController do
|
RSpec.describe OAuth::AuthorizationsController do
|
||||||
let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') }
|
let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') }
|
||||||
|
|
||||||
describe 'GET #new' do
|
describe 'GET #new' do
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Oauth::AuthorizedApplicationsController do
|
RSpec.describe OAuth::AuthorizedApplicationsController do
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
describe 'GET #index' do
|
describe 'GET #index' do
|
||||||
|
|
7
spec/fabricators/instance_moderation_note_fabricator.rb
Normal file
7
spec/fabricators/instance_moderation_note_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:instance_moderation_note) do
|
||||||
|
domain { sequence(:domain) { |i| "#{i}#{Faker::Internet.domain_name}" } }
|
||||||
|
account { Fabricate.build(:account) }
|
||||||
|
content { Faker::Lorem.sentence }
|
||||||
|
end
|
42
spec/models/doorkeeper/access_grant_spec.rb
Normal file
42
spec/models/doorkeeper/access_grant_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Doorkeeper::AccessGrant do
|
||||||
|
describe 'Validations' do
|
||||||
|
subject { Fabricate :access_grant }
|
||||||
|
|
||||||
|
it { is_expected.to validate_presence_of(:application_id) }
|
||||||
|
it { is_expected.to validate_presence_of(:expires_in) }
|
||||||
|
it { is_expected.to validate_presence_of(:redirect_uri) }
|
||||||
|
it { is_expected.to validate_presence_of(:token) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Scopes' do
|
||||||
|
describe '.expired' do
|
||||||
|
let!(:unexpired) { Fabricate :access_grant, expires_in: 10.hours }
|
||||||
|
let!(:expired) do
|
||||||
|
travel_to 10.minutes.ago do
|
||||||
|
Fabricate :access_grant, expires_in: 5.minutes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns records past their expired time' do
|
||||||
|
expect(described_class.expired)
|
||||||
|
.to include(expired)
|
||||||
|
.and not_include(unexpired)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.revoked' do
|
||||||
|
let!(:revoked) { Fabricate :access_grant, revoked_at: 10.minutes.ago }
|
||||||
|
let!(:unrevoked) { Fabricate :access_grant, revoked_at: 10.minutes.from_now }
|
||||||
|
|
||||||
|
it 'returns records past their expired time' do
|
||||||
|
expect(described_class.revoked)
|
||||||
|
.to include(revoked)
|
||||||
|
.and not_include(unrevoked)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
63
spec/models/doorkeeper/access_token_spec.rb
Normal file
63
spec/models/doorkeeper/access_token_spec.rb
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Doorkeeper::AccessToken do
|
||||||
|
describe 'Associations' do
|
||||||
|
it { is_expected.to have_many(:web_push_subscriptions).class_name('Web::PushSubscription').inverse_of(:access_token) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Validations' do
|
||||||
|
subject { Fabricate :access_token }
|
||||||
|
|
||||||
|
it { is_expected.to validate_presence_of(:token) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Scopes' do
|
||||||
|
describe '.expired' do
|
||||||
|
let!(:unexpired) { Fabricate :access_token, expires_in: 10.hours }
|
||||||
|
let!(:expired) do
|
||||||
|
travel_to 10.minutes.ago do
|
||||||
|
Fabricate :access_token, expires_in: 5.minutes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns records past their expired time' do
|
||||||
|
expect(described_class.expired)
|
||||||
|
.to include(expired)
|
||||||
|
.and not_include(unexpired)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.revoked' do
|
||||||
|
let!(:revoked) { Fabricate :access_token, revoked_at: 10.minutes.ago }
|
||||||
|
let!(:unrevoked) { Fabricate :access_token, revoked_at: 10.minutes.from_now }
|
||||||
|
|
||||||
|
it 'returns records past their expired time' do
|
||||||
|
expect(described_class.revoked)
|
||||||
|
.to include(revoked)
|
||||||
|
.and not_include(unrevoked)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#revoke' do
|
||||||
|
let(:record) { Fabricate :access_token, revoked_at: 10.days.from_now }
|
||||||
|
|
||||||
|
it 'marks the record as revoked' do
|
||||||
|
expect { record.revoke }
|
||||||
|
.to change(record, :revoked_at).to(be_within(1).of(Time.now.utc))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update_last_used' do
|
||||||
|
let(:record) { Fabricate :access_token, last_used_at: nil, last_used_ip: nil }
|
||||||
|
let(:request) { instance_double(ActionDispatch::Request, remote_ip: '1.1.1.1') }
|
||||||
|
|
||||||
|
it 'marks the record as revoked' do
|
||||||
|
expect { record.update_last_used(request) }
|
||||||
|
.to change(record, :last_used_at).to(be_within(1).of(Time.now.utc))
|
||||||
|
.and change(record, :last_used_ip).to(IPAddr.new('1.1.1.1'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,8 +8,45 @@ RSpec.describe Doorkeeper::Application do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Validations' do
|
describe 'Validations' do
|
||||||
|
subject { Fabricate :application }
|
||||||
|
|
||||||
|
it { is_expected.to validate_presence_of(:name) }
|
||||||
|
it { is_expected.to validate_presence_of(:uid) }
|
||||||
|
|
||||||
it { is_expected.to validate_length_of(:name).is_at_most(described_class::APP_NAME_LIMIT) }
|
it { is_expected.to validate_length_of(:name).is_at_most(described_class::APP_NAME_LIMIT) }
|
||||||
it { is_expected.to validate_length_of(:redirect_uri).is_at_most(described_class::APP_REDIRECT_URI_LIMIT) }
|
it { is_expected.to validate_length_of(:redirect_uri).is_at_most(described_class::APP_REDIRECT_URI_LIMIT) }
|
||||||
it { is_expected.to validate_length_of(:website).is_at_most(described_class::APP_WEBSITE_LIMIT) }
|
it { is_expected.to validate_length_of(:website).is_at_most(described_class::APP_WEBSITE_LIMIT) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#redirect_uris' do
|
||||||
|
subject { Fabricate.build(:application, redirect_uri:).redirect_uris }
|
||||||
|
|
||||||
|
context 'with single value' do
|
||||||
|
let(:redirect_uri) { 'https://test.example/one' }
|
||||||
|
|
||||||
|
it { is_expected.to be_an(Array).and(eq(['https://test.example/one'])) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple values' do
|
||||||
|
let(:redirect_uri) { "https://test.example/one\nhttps://test.example/two" }
|
||||||
|
|
||||||
|
it { is_expected.to be_an(Array).and(eq(['https://test.example/one', 'https://test.example/two'])) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#confirmation_redirect_uri' do
|
||||||
|
subject { Fabricate.build(:application, redirect_uri:).confirmation_redirect_uri }
|
||||||
|
|
||||||
|
context 'with single value' do
|
||||||
|
let(:redirect_uri) { 'https://test.example/one ' }
|
||||||
|
|
||||||
|
it { is_expected.to eq('https://test.example/one') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple values' do
|
||||||
|
let(:redirect_uri) { "https://test.example/one \nhttps://test.example/two " }
|
||||||
|
|
||||||
|
it { is_expected.to eq('https://test.example/one') }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
37
spec/models/instance_moderation_note_spec.rb
Normal file
37
spec/models/instance_moderation_note_spec.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe InstanceModerationNote do
|
||||||
|
describe 'chronological' do
|
||||||
|
it 'returns the instance notes sorted by oldest first' do
|
||||||
|
instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain('mastodon.example'))
|
||||||
|
|
||||||
|
note1 = Fabricate(:instance_moderation_note, domain: instance.domain)
|
||||||
|
note2 = Fabricate(:instance_moderation_note, domain: instance.domain)
|
||||||
|
|
||||||
|
expect(instance.moderation_notes.chronological).to eq [note1, note2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'validations' do
|
||||||
|
it 'is invalid if the content is empty' do
|
||||||
|
note = Fabricate.build(:instance_moderation_note, domain: 'mastodon.example', content: '')
|
||||||
|
expect(note.valid?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is invalid if content is longer than character limit' do
|
||||||
|
note = Fabricate.build(:instance_moderation_note, domain: 'mastodon.example', content: comment_over_limit)
|
||||||
|
expect(note.valid?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is valid even if the instance does not exist yet' do
|
||||||
|
note = Fabricate.build(:instance_moderation_note, domain: 'non-existent.example', content: 'test comment')
|
||||||
|
expect(note.valid?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
def comment_over_limit
|
||||||
|
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,9 +3,9 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Instance do
|
RSpec.describe Instance do
|
||||||
describe 'Scopes' do
|
before { described_class.refresh }
|
||||||
before { described_class.refresh }
|
|
||||||
|
|
||||||
|
describe 'Scopes' do
|
||||||
describe '#searchable' do
|
describe '#searchable' do
|
||||||
let(:expected_domain) { 'host.example' }
|
let(:expected_domain) { 'host.example' }
|
||||||
let(:blocked_domain) { 'other.example' }
|
let(:blocked_domain) { 'other.example' }
|
||||||
|
|
|
@ -166,6 +166,34 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#email_domain' do
|
||||||
|
subject { described_class.new(email: email).email_domain }
|
||||||
|
|
||||||
|
context 'when value is nil' do
|
||||||
|
let(:email) { nil }
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when value is blank' do
|
||||||
|
let(:email) { '' }
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when value has valid domain' do
|
||||||
|
let(:email) { 'user@host.example' }
|
||||||
|
|
||||||
|
it { is_expected.to eq('host.example') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when value has no split' do
|
||||||
|
let(:email) { 'user$host.example' }
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#update_sign_in!' do
|
describe '#update_sign_in!' do
|
||||||
context 'with an existing user' do
|
context 'with an existing user' do
|
||||||
let!(:user) { Fabricate :user, last_sign_in_at: 10.days.ago, current_sign_in_at: 1.hour.ago, sign_in_count: 123 }
|
let!(:user) { Fabricate :user, last_sign_in_at: 10.days.ago, current_sign_in_at: 1.hour.ago, sign_in_count: 123 }
|
||||||
|
|
16
spec/requests/admin/instances/moderation_notes_spec.rb
Normal file
16
spec/requests/admin/instances/moderation_notes_spec.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin Report Notes' do
|
||||||
|
describe 'POST /admin/instance/moderation_notes' do
|
||||||
|
before { sign_in Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
it 'gracefully handles invalid nested params' do
|
||||||
|
post admin_instance_moderation_notes_path(instance_id: 'mastodon.test', instance_note: 'invalid')
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,9 +4,9 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Admin Instances' do
|
RSpec.describe 'Admin Instances' do
|
||||||
describe 'GET /admin/instances/:id' do
|
describe 'GET /admin/instances/:id' do
|
||||||
context 'with an unknown domain' do
|
before { sign_in Fabricate(:admin_user) }
|
||||||
before { sign_in Fabricate(:admin_user) }
|
|
||||||
|
|
||||||
|
context 'with an unknown domain' do
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
get admin_instance_path(id: 'unknown.example')
|
get admin_instance_path(id: 'unknown.example')
|
||||||
|
|
||||||
|
@ -14,5 +14,14 @@ RSpec.describe 'Admin Instances' do
|
||||||
.to have_http_status(200)
|
.to have_http_status(200)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an invalid domain' do
|
||||||
|
it 'returns http success' do
|
||||||
|
get admin_instance_path(id: ' ')
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Oauth Userinfo Endpoint' do
|
RSpec.describe 'OAuth Userinfo Endpoint' do
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
|
@ -35,7 +35,7 @@ RSpec.describe 'Admin::AccountModerationNotes' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_note
|
def delete_note
|
||||||
within('.report-notes__item__actions') do
|
within('.report-notes__item:first-child .report-notes__item__actions') do
|
||||||
click_on I18n.t('admin.reports.notes.delete')
|
click_on I18n.t('admin.reports.notes.delete')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
51
spec/system/admin/instance/moderation_notes_spec.rb
Normal file
51
spec/system/admin/instance/moderation_notes_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin::Instances::ModerationNotesController' do
|
||||||
|
let(:current_user) { Fabricate(:admin_user) }
|
||||||
|
let(:instance_domain) { 'mastodon.example' }
|
||||||
|
|
||||||
|
before { sign_in current_user }
|
||||||
|
|
||||||
|
describe 'Managing instance moderation notes' do
|
||||||
|
it 'saves and then deletes a record' do
|
||||||
|
visit admin_instance_path(instance_domain)
|
||||||
|
|
||||||
|
fill_in 'instance_moderation_note_content', with: ''
|
||||||
|
expect { submit_form }
|
||||||
|
.to not_change(InstanceModerationNote, :count)
|
||||||
|
expect(page)
|
||||||
|
.to have_content(/error below/)
|
||||||
|
|
||||||
|
fill_in 'instance_moderation_note_content', with: 'Test message ' * InstanceModerationNote::CONTENT_SIZE_LIMIT
|
||||||
|
expect { submit_form }
|
||||||
|
.to not_change(InstanceModerationNote, :count)
|
||||||
|
expect(page)
|
||||||
|
.to have_content(/error below/)
|
||||||
|
|
||||||
|
fill_in 'instance_moderation_note_content', with: 'Test message'
|
||||||
|
expect { submit_form }
|
||||||
|
.to change(InstanceModerationNote, :count).by(1)
|
||||||
|
expect(page)
|
||||||
|
.to have_current_path(admin_instance_path(instance_domain))
|
||||||
|
expect(page)
|
||||||
|
.to have_content(I18n.t('admin.instances.moderation_notes.created_msg'))
|
||||||
|
|
||||||
|
expect { delete_note }
|
||||||
|
.to change(InstanceModerationNote, :count).by(-1)
|
||||||
|
expect(page)
|
||||||
|
.to have_content(I18n.t('admin.instances.moderation_notes.destroyed_msg'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def submit_form
|
||||||
|
click_on I18n.t('admin.instances.moderation_notes.create')
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_note
|
||||||
|
within('.report-notes__item:first-child .report-notes__item__actions') do
|
||||||
|
click_on I18n.t('admin.reports.notes.delete')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -29,10 +29,7 @@
|
||||||
"vite.config.mts",
|
"vite.config.mts",
|
||||||
"vitest.config.mts",
|
"vitest.config.mts",
|
||||||
"config/vite",
|
"config/vite",
|
||||||
"app/javascript/mastodon",
|
"app/javascript",
|
||||||
"app/javascript/entrypoints",
|
".storybook/*"
|
||||||
"app/javascript/types",
|
|
||||||
".storybook/*.ts",
|
|
||||||
".storybook/*.tsx"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user