Compare commits
23 Commits
renovate/a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15a7507a09 | ||
|
|
cdf721a273 | ||
|
|
cafe7ea35c | ||
|
|
e18ca373eb | ||
|
|
e54f927149 | ||
|
|
6735902c1a | ||
|
|
dc3ffac4a2 | ||
|
|
fe885d5788 | ||
|
|
adfe7242f7 | ||
|
|
dfcfef38af | ||
|
|
fbc116ef90 | ||
|
|
6b5e18fb1d | ||
|
|
d39f7bc72f | ||
|
|
e68c1c824a | ||
|
|
076c8ec51e | ||
|
|
f5b57e8ba7 | ||
|
|
0786c1e57a | ||
|
|
ec2a99341c | ||
|
|
6e7e8de343 | ||
|
|
a444a0b572 | ||
|
|
6f8558a6b9 | ||
|
|
22203f8aeb | ||
|
|
f28715d370 |
@@ -59,7 +59,7 @@ body:
|
||||
Any additional technical details you may have, like logs or error traces
|
||||
value: |
|
||||
If this is happening on your own Mastodon server, please fill out those:
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.4)
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.5)
|
||||
- Node.js version: (from `node --version`, eg. v22.16.0)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
2
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
@@ -61,7 +61,7 @@ body:
|
||||
value: |
|
||||
Please at least include those informations:
|
||||
- Operating system: (eg. Ubuntu 24.04.2)
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.4)
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.5)
|
||||
- Node.js version: (from `node --version`, eg. v22.16.0)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
2
.github/actions/setup-ruby/action.yml
vendored
2
.github/actions/setup-ruby/action.yml
vendored
@@ -23,7 +23,7 @@ runs:
|
||||
${{ inputs.additional-system-dependencies }}
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
ruby-version: ${{ inputs.ruby-version }}
|
||||
bundler-cache: true
|
||||
|
||||
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
2
.github/workflows/lint-haml.yml
vendored
2
.github/workflows/lint-haml.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
2
.github/workflows/lint-ruby.yml
vendored
2
.github/workflows/lint-ruby.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
4.0.4
|
||||
4.0.5
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -2,6 +2,22 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.5.10] - 2026-05-20
|
||||
|
||||
### Security
|
||||
|
||||
- Fix SSRF protection bypass ([GHSA-crr4-7rm4-8gpw](https://github.com/mastodon/mastodon/security/advisories/GHSA-crr4-7rm4-8gpw), [GHSA-xx55-4rrg-8xg6](https://github.com/mastodon/mastodon/security/advisories/GHSA-xx55-4rrg-8xg6))
|
||||
- Fix Linked-Data Signature bypass through JSON-LD graph restructuring features ([GHSA-53m7-2wrh-q839](https://github.com/mastodon/mastodon/security/advisories/GHSA-53m7-2wrh-q839), [GHSA-chgx-jx3p-rf73](https://github.com/mastodon/mastodon/security/advisories/GHSA-chgx-jx3p-rf73))
|
||||
- Updated dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix type of `interactingObject`, `interactionTarget` and add missing `QuoteAuthorization` (#38940 by @ClearlyClaire)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove unused devise strategies (#38795 by @ClearlyClaire)
|
||||
|
||||
## [4.5.9] - 2026-04-15
|
||||
|
||||
### Security
|
||||
|
||||
@@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
|
||||
|
||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="4.0.x"]
|
||||
# renovate: datasource=docker depName=docker.io/ruby
|
||||
ARG RUBY_VERSION="4.0.4"
|
||||
ARG RUBY_VERSION="4.0.5"
|
||||
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"]
|
||||
# renovate: datasource=node-version depName=node
|
||||
ARG NODE_MAJOR_VERSION="24"
|
||||
|
||||
@@ -1097,7 +1097,7 @@ DEPENDENCIES
|
||||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 4.0.4
|
||||
ruby 4.0.5
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.11
|
||||
|
||||
@@ -59,7 +59,7 @@ Mastodon is a **free, open-source social network server** based on [ActivityPub]
|
||||
- **Ruby** 3.3+
|
||||
- **PostgreSQL** 14+
|
||||
- **Redis** 7.0+
|
||||
- **Node.js** 20+
|
||||
- **Node.js** 22+
|
||||
- **FFmpeg** 5.1+
|
||||
|
||||
This repository includes deployment configurations for **Docker and docker-compose**, as well as for other environments like Heroku and Scalingo. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). A [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the main documentation.
|
||||
|
||||
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@@ -12,7 +12,7 @@ sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
|
||||
# Add repo for NodeJS
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
NODE_MAJOR=20
|
||||
NODE_MAJOR=24
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
sudo apt-get update
|
||||
|
||||
|
||||
@@ -28,10 +28,9 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
cache_if_unauthenticated!
|
||||
authorize @account, :index_collections?
|
||||
|
||||
presenter = CollectionsPresenter.new(collections: @collections)
|
||||
render json: presenter, serializer: REST::CollectionsWithAccountPreviewsSerializer
|
||||
render json: @collections, each_serializer: REST::CollectionSerializer, adapter: :json
|
||||
rescue Mastodon::NotPermittedError
|
||||
render json: { collections: [], partial_accounts: [] }
|
||||
render json: { collections: [] }
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -74,7 +73,6 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
def set_collections
|
||||
@collections = @account.collections
|
||||
.with_tag
|
||||
.preload(top_items: :account)
|
||||
.order(created_at: :desc)
|
||||
.offset(offset_param)
|
||||
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
module JsonLdHelper
|
||||
include ContextHelper
|
||||
|
||||
UNSUPPORTED_JSONLD_KEYWORDS = %w(@graph @included @reverse).freeze
|
||||
|
||||
def equals_or_includes?(haystack, needle)
|
||||
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
|
||||
end
|
||||
@@ -110,6 +112,16 @@ module JsonLdHelper
|
||||
compacted
|
||||
end
|
||||
|
||||
def unsupported_jsonld_features?(json)
|
||||
if json.is_a?(Hash)
|
||||
json.any? { |key, value| UNSUPPORTED_JSONLD_KEYWORDS.include?(key) || unsupported_jsonld_features?(value) }
|
||||
elsif json.is_a?(Array)
|
||||
json.any? { |value| unsupported_jsonld_features?(value) }
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Patches a JSON-LD document to avoid compatibility issues on redistribution
|
||||
#
|
||||
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ensureComposeIsVisible = (getState) => {
|
||||
|
||||
export function setComposeToStatus(status, text, spoiler_text) {
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
const maxOptions = getState().server.server.item?.configuration.polls.max_options;
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_SET_STATUS,
|
||||
@@ -671,7 +671,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
||||
let completion, startPosition;
|
||||
|
||||
if (suggestion.type === 'emoji') {
|
||||
completion = suggestion.native || suggestion.colons;
|
||||
completion = suggestion.native || `:${suggestion.id}:`;
|
||||
startPosition = position - 1;
|
||||
|
||||
dispatch(useEmoji(suggestion));
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import api from '../api';
|
||||
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
|
||||
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
|
||||
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
|
||||
|
||||
export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST';
|
||||
export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS';
|
||||
export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL';
|
||||
|
||||
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
|
||||
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
|
||||
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
|
||||
|
||||
export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST';
|
||||
export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS';
|
||||
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
|
||||
|
||||
export const fetchServer = () => (dispatch, getState) => {
|
||||
if (getState().getIn(['server', 'server', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchServerRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v2/instance').then(({ data }) => {
|
||||
// Only import the account if it doesn't already exist,
|
||||
// because the API is cached even for logged in users.
|
||||
const account = data.contact.account;
|
||||
if (account) {
|
||||
const existingAccount = getState().getIn(['accounts', account.id]);
|
||||
if (!existingAccount) {
|
||||
dispatch(importFetchedAccount(account));
|
||||
}
|
||||
}
|
||||
dispatch(fetchServerSuccess(data));
|
||||
}).catch(err => dispatch(fetchServerFail(err)));
|
||||
};
|
||||
|
||||
const fetchServerRequest = () => ({
|
||||
type: SERVER_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchServerSuccess = server => ({
|
||||
type: SERVER_FETCH_SUCCESS,
|
||||
server,
|
||||
});
|
||||
|
||||
const fetchServerFail = error => ({
|
||||
type: SERVER_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchServerTranslationLanguages = () => (dispatch) => {
|
||||
dispatch(fetchServerTranslationLanguagesRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v1/instance/translation_languages').then(({ data }) => {
|
||||
dispatch(fetchServerTranslationLanguagesSuccess(data));
|
||||
}).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
|
||||
};
|
||||
|
||||
const fetchServerTranslationLanguagesRequest = () => ({
|
||||
type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchServerTranslationLanguagesSuccess = translationLanguages => ({
|
||||
type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
|
||||
translationLanguages,
|
||||
});
|
||||
|
||||
const fetchServerTranslationLanguagesFail = error => ({
|
||||
type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
||||
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchExtendedDescriptionRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v1/instance/extended_description')
|
||||
.then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
|
||||
.catch(err => dispatch(fetchExtendedDescriptionFail(err)));
|
||||
};
|
||||
|
||||
const fetchExtendedDescriptionRequest = () => ({
|
||||
type: EXTENDED_DESCRIPTION_REQUEST,
|
||||
});
|
||||
|
||||
const fetchExtendedDescriptionSuccess = description => ({
|
||||
type: EXTENDED_DESCRIPTION_SUCCESS,
|
||||
description,
|
||||
});
|
||||
|
||||
const fetchExtendedDescriptionFail = error => ({
|
||||
type: EXTENDED_DESCRIPTION_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
||||
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchDomainBlocksRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v1/instance/domain_blocks')
|
||||
.then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
|
||||
.catch(err => {
|
||||
if (err.response.status === 404) {
|
||||
dispatch(fetchDomainBlocksSuccess(false, []));
|
||||
} else {
|
||||
dispatch(fetchDomainBlocksFail(err));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDomainBlocksRequest = () => ({
|
||||
type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
|
||||
type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
|
||||
isAvailable,
|
||||
blocks,
|
||||
});
|
||||
|
||||
const fetchDomainBlocksFail = error => ({
|
||||
type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
34
app/javascript/mastodon/actions/server.ts
Normal file
34
app/javascript/mastodon/actions/server.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
apiGetInstance,
|
||||
apiGetExtendedDescription,
|
||||
apiGetDomainBlocks,
|
||||
apiGetTranslationLanguages,
|
||||
} from 'mastodon/api/instance';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
export const fetchServer = createDataLoadingThunk(
|
||||
'server/fetch',
|
||||
() => apiGetInstance(),
|
||||
(instance, { dispatch }) => {
|
||||
if (instance.contact.account) {
|
||||
dispatch(importFetchedAccount(instance.contact.account));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchExtendedDescription = createDataLoadingThunk(
|
||||
'server/extended_description',
|
||||
() => apiGetExtendedDescription(),
|
||||
);
|
||||
|
||||
export const fetchServerTranslationLanguages = createDataLoadingThunk(
|
||||
'server/translation_languages',
|
||||
() => apiGetTranslationLanguages(),
|
||||
);
|
||||
|
||||
export const fetchDomainBlocks = createDataLoadingThunk(
|
||||
'server/domain_blocks',
|
||||
() => apiGetDomainBlocks(),
|
||||
);
|
||||
@@ -111,7 +111,7 @@ export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||
|
||||
export function redraft(status, raw_text, quoted_status_id = null) {
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
const maxOptions = getState().server.server.item?.configuration.polls.max_options;
|
||||
|
||||
dispatch({
|
||||
type: REDRAFT,
|
||||
|
||||
@@ -2,6 +2,10 @@ import { apiRequestGet } from 'mastodon/api';
|
||||
import type {
|
||||
ApiTermsOfServiceJSON,
|
||||
ApiPrivacyPolicyJSON,
|
||||
ApiInstanceJSON,
|
||||
ApiExtendedDescriptionJSON,
|
||||
ApiTranslationLanguagesJSON,
|
||||
ApiDomainBlockJSON,
|
||||
} from 'mastodon/api_types/instance';
|
||||
|
||||
export const apiGetTermsOfService = (version?: string) =>
|
||||
@@ -13,3 +17,17 @@ export const apiGetTermsOfService = (version?: string) =>
|
||||
|
||||
export const apiGetPrivacyPolicy = () =>
|
||||
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
||||
|
||||
export const apiGetInstance = () =>
|
||||
apiRequestGet<ApiInstanceJSON>('v2/instance');
|
||||
|
||||
export const apiGetExtendedDescription = () =>
|
||||
apiRequestGet<ApiExtendedDescriptionJSON>('v1/instance/extended_description');
|
||||
|
||||
export const apiGetTranslationLanguages = () =>
|
||||
apiRequestGet<ApiTranslationLanguagesJSON>(
|
||||
'v1/instance/translation_languages',
|
||||
);
|
||||
|
||||
export const apiGetDomainBlocks = () =>
|
||||
apiRequestGet<ApiDomainBlockJSON[]>('v1/instance/domain_blocks');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
|
||||
export interface ApiTermsOfServiceJSON {
|
||||
effective_date: string;
|
||||
effective: boolean;
|
||||
@@ -9,3 +11,136 @@ export interface ApiPrivacyPolicyJSON {
|
||||
updated_at: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ApiBaseRuleJSON {
|
||||
text: string;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
export interface ApiRuleJSON {
|
||||
id: string;
|
||||
text: string;
|
||||
hint: string;
|
||||
translations?: Record<string, ApiBaseRuleJSON>;
|
||||
}
|
||||
|
||||
export interface ApiExtendedDescriptionJSON {
|
||||
updated_at: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ApiDomainBlockJSON {
|
||||
domain: string;
|
||||
digest: string;
|
||||
severity: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export type ApiTranslationLanguagesJSON = Record<string, string[]>;
|
||||
|
||||
export interface ApiInstanceJSON {
|
||||
domain: string;
|
||||
title: string;
|
||||
version: string;
|
||||
source_url: string;
|
||||
description: string;
|
||||
languages: string[];
|
||||
usage: {
|
||||
users: {
|
||||
active_month: number;
|
||||
};
|
||||
};
|
||||
thumbnail: {
|
||||
url: string;
|
||||
blurhash?: string;
|
||||
description: string;
|
||||
versions?: Record<string, string>;
|
||||
};
|
||||
contact: {
|
||||
email: string | null;
|
||||
account: ApiAccountJSON | null;
|
||||
};
|
||||
api_versions: {
|
||||
mastodon: number;
|
||||
};
|
||||
registrations: {
|
||||
enabled: boolean;
|
||||
approval_required: boolean;
|
||||
reason_required: boolean | null;
|
||||
message: string | null;
|
||||
min_age: string | null;
|
||||
url: string | null;
|
||||
};
|
||||
rules: ApiRuleJSON[];
|
||||
configuration: {
|
||||
urls: {
|
||||
streaming: string;
|
||||
status: string | null;
|
||||
about: string;
|
||||
privacy_policy: string | null;
|
||||
terms_of_service: string | null;
|
||||
};
|
||||
|
||||
vapid: {
|
||||
public_key: string;
|
||||
};
|
||||
|
||||
accounts: {
|
||||
max_display_name_length: number;
|
||||
max_note_length: number;
|
||||
max_avatar_description_length: number;
|
||||
max_header_description_length: number;
|
||||
max_featured_tags: number;
|
||||
max_pinned_statuses: number;
|
||||
max_profile_fields: number;
|
||||
profile_field_name_limit: number;
|
||||
profile_field_value_limit: number;
|
||||
};
|
||||
|
||||
statuses: {
|
||||
max_characters: number;
|
||||
max_media_attachments: number;
|
||||
characters_reserved_per_url: number;
|
||||
};
|
||||
|
||||
media_attachments: {
|
||||
description_limit: number;
|
||||
image_matrix_limit: number;
|
||||
image_size_limit: number;
|
||||
supported_mime_types: string[];
|
||||
video_frame_rate_limit: number;
|
||||
video_matrix_limit: number;
|
||||
video_size_limit: number;
|
||||
};
|
||||
|
||||
polls: {
|
||||
max_options: number;
|
||||
max_characters_per_option: number;
|
||||
min_expiration: number;
|
||||
max_expiration: number;
|
||||
};
|
||||
|
||||
translation: {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
timeline_access: {
|
||||
live_feeds: {
|
||||
local: string;
|
||||
remote: string;
|
||||
};
|
||||
|
||||
hashtag_feeds: {
|
||||
local: string;
|
||||
remote: string;
|
||||
};
|
||||
|
||||
trending_link_feeds: {
|
||||
local: string;
|
||||
remote: string;
|
||||
};
|
||||
};
|
||||
|
||||
limited_federation: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { initMuteModal } from '@/mastodon/actions/mutes';
|
||||
import { initReport } from '@/mastodon/actions/reports';
|
||||
import {
|
||||
canAccountBeAdded,
|
||||
canAccountBeAddedByFollowers,
|
||||
} from '@/mastodon/features/collections/utils';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useIdentity } from '@/mastodon/identity_context';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
@@ -214,6 +218,10 @@ const redesignMessages = defineMessages({
|
||||
id: 'account.menu.add_to_list',
|
||||
defaultMessage: 'Add to list…',
|
||||
},
|
||||
addToCollection: {
|
||||
id: 'account.menu.add_to_collection',
|
||||
defaultMessage: 'Add to collection…',
|
||||
},
|
||||
openOriginalPage: {
|
||||
id: 'account.menu.open_original_page',
|
||||
defaultMessage: 'View on {domain}',
|
||||
@@ -294,35 +302,57 @@ function getMenuItems({
|
||||
return items;
|
||||
}
|
||||
|
||||
// List and featuring options
|
||||
// Add to list
|
||||
if (relationship?.following) {
|
||||
items.push(
|
||||
{
|
||||
text: intl.formatMessage(redesignMessages.addToList),
|
||||
action: () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'LIST_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
items.push({
|
||||
text: intl.formatMessage(redesignMessages.addToList),
|
||||
action: () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'LIST_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(
|
||||
relationship.endorsed ? messages.unendorse : messages.endorse,
|
||||
),
|
||||
action: () => {
|
||||
if (relationship.endorsed) {
|
||||
dispatch(unpinAccount(account.id));
|
||||
} else {
|
||||
dispatch(pinAccount(account.id));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add to collection
|
||||
if (
|
||||
canAccountBeAdded(account) ||
|
||||
(canAccountBeAddedByFollowers(account) && relationship?.following)
|
||||
) {
|
||||
items.push({
|
||||
text: intl.formatMessage(redesignMessages.addToCollection),
|
||||
action: () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'COLLECTION_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Feature on profile
|
||||
if (relationship?.following) {
|
||||
items.push({
|
||||
text: intl.formatMessage(
|
||||
relationship.endorsed ? messages.unendorse : messages.endorse,
|
||||
),
|
||||
action: () => {
|
||||
if (relationship.endorsed) {
|
||||
dispatch(unpinAccount(account.id));
|
||||
} else {
|
||||
dispatch(pinAccount(account.id));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
|
||||
@@ -22,7 +22,7 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
server: state.getIn(['server', 'server']),
|
||||
server: state.server.server,
|
||||
});
|
||||
|
||||
class ServerBanner extends PureComponent {
|
||||
@@ -40,7 +40,7 @@ class ServerBanner extends PureComponent {
|
||||
|
||||
render () {
|
||||
const { server, intl } = this.props;
|
||||
const isLoading = server.get('isLoading');
|
||||
const isLoading = server.isLoading;
|
||||
|
||||
return (
|
||||
<div className='server-banner'>
|
||||
@@ -50,8 +50,8 @@ class ServerBanner extends PureComponent {
|
||||
|
||||
<NavLink to='/about'>
|
||||
<ServerHeroImage
|
||||
blurhash={server.getIn(['thumbnail', 'blurhash'])}
|
||||
src={server.getIn(['thumbnail', 'url'])}
|
||||
blurhash={server.item?.thumbnail.blurhash}
|
||||
src={server.item?.thumbnail.url}
|
||||
alt={intl.formatMessage(messages.aboutThisServer)}
|
||||
className='server-banner__hero'
|
||||
/>
|
||||
@@ -66,14 +66,14 @@ class ServerBanner extends PureComponent {
|
||||
<br />
|
||||
<Skeleton width='70%' />
|
||||
</>
|
||||
) : server.get('description')}
|
||||
) : server.item?.description}
|
||||
</div>
|
||||
|
||||
<div className='server-banner__meta'>
|
||||
<div className='server-banner__meta__column'>
|
||||
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
|
||||
|
||||
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
|
||||
<Account id={server.item?.contact.account?.id} size={36} minimal />
|
||||
</div>
|
||||
|
||||
<div className='server-banner__meta__column'>
|
||||
@@ -87,7 +87,7 @@ class ServerBanner extends PureComponent {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
|
||||
<strong className='server-banner__number'><ShortNumber value={server.item?.usage.users.active_month} /></strong>
|
||||
<br />
|
||||
<span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { ComponentProps, FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
||||
import { getCollectionPath } from '@/mastodon/features/collections/utils';
|
||||
import type { OnElementHandler } from '@/mastodon/utils/html';
|
||||
@@ -15,7 +14,7 @@ export interface HandledLinkProps {
|
||||
prevText?: string;
|
||||
hashtagAccountId?: string;
|
||||
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
|
||||
collection?: Pick<ApiCollectionJSON, 'id'>;
|
||||
collectionId?: string;
|
||||
}
|
||||
|
||||
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||
@@ -24,7 +23,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||
prevText,
|
||||
hashtagAccountId,
|
||||
mention,
|
||||
collection,
|
||||
collectionId,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
@@ -61,11 +60,11 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
} else if (collection) {
|
||||
} else if (collectionId) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(className)}
|
||||
to={getCollectionPath(collection.id)}
|
||||
to={getCollectionPath(collectionId)}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
@@ -98,15 +97,18 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||
|
||||
export const useElementHandledLink = ({
|
||||
hashtagAccountId,
|
||||
hrefToCollectionId: hrefToCollection,
|
||||
hrefToMention,
|
||||
}: {
|
||||
hashtagAccountId?: string;
|
||||
hrefToCollectionId?: (href: string) => string | undefined;
|
||||
hrefToMention?: (href: string) => ApiMentionJSON | undefined;
|
||||
} = {}) => {
|
||||
const onElement = useCallback<OnElementHandler>(
|
||||
(element, { key, ...props }, children) => {
|
||||
if (element instanceof HTMLAnchorElement) {
|
||||
const mention = hrefToMention?.(element.href);
|
||||
const collectionId = hrefToCollection?.(element.href);
|
||||
return (
|
||||
<HandledLink
|
||||
{...props}
|
||||
@@ -116,6 +118,7 @@ export const useElementHandledLink = ({
|
||||
prevText={element.previousSibling?.textContent ?? undefined}
|
||||
hashtagAccountId={hashtagAccountId}
|
||||
mention={mention}
|
||||
collectionId={collectionId}
|
||||
>
|
||||
{children}
|
||||
</HandledLink>
|
||||
@@ -123,7 +126,7 @@ export const useElementHandledLink = ({
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
[hashtagAccountId, hrefToMention],
|
||||
[hashtagAccountId, hrefToCollection, hrefToMention],
|
||||
);
|
||||
return { onElement };
|
||||
};
|
||||
|
||||
@@ -69,7 +69,7 @@ class TranslateButton extends PureComponent {
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
||||
languages: state.server.translationLanguages.items,
|
||||
});
|
||||
|
||||
class StatusContent extends PureComponent {
|
||||
@@ -170,7 +170,7 @@ class StatusContent extends PureComponent {
|
||||
text={element.innerText}
|
||||
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
||||
mention={mention?.toJSON()}
|
||||
collection={taggedCollection?.toJSON()}
|
||||
collectionId={taggedCollection?.get('id')}
|
||||
key={key}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { IntlShape } from 'react-intl';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
|
||||
import { Select } from '@/mastodon/components/form_fields';
|
||||
@@ -123,14 +122,13 @@ export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
|
||||
};
|
||||
|
||||
const selectRules = (state: RootState) => {
|
||||
const rules = state.server.getIn([
|
||||
'server',
|
||||
'rules',
|
||||
]) as ImmutableList<Rule> | null;
|
||||
const rules = state.server.server.item?.rules;
|
||||
|
||||
if (!rules) {
|
||||
return [];
|
||||
}
|
||||
return rules.toJS() as Rule[];
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
const rulesSelector = createSelector(
|
||||
|
||||
@@ -41,10 +41,10 @@ const severityMessages = {
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
server: state.getIn(['server', 'server']),
|
||||
server: state.server.server,
|
||||
locale: state.getIn(['meta', 'locale']),
|
||||
extendedDescription: state.getIn(['server', 'extendedDescription']),
|
||||
domainBlocks: state.getIn(['server', 'domainBlocks']),
|
||||
extendedDescription: state.server.extendedDescription,
|
||||
domainBlocks: state.server.domainBlocks,
|
||||
});
|
||||
|
||||
class About extends PureComponent {
|
||||
@@ -76,7 +76,7 @@ class About extends PureComponent {
|
||||
|
||||
render () {
|
||||
const { multiColumn, intl, server, extendedDescription, domainBlocks, locale } = this.props;
|
||||
const isLoading = server.get('isLoading');
|
||||
const isLoading = server.isLoading;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||
@@ -84,13 +84,13 @@ class About extends PureComponent {
|
||||
<div className='about__header'>
|
||||
<ServerHeroImage
|
||||
withAltBadge
|
||||
alt={server.getIn(['thumbnail', 'description']) ?? ''}
|
||||
blurhash={server.getIn(['thumbnail', 'blurhash'])}
|
||||
src={server.getIn(['thumbnail', 'url'])}
|
||||
srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')}
|
||||
alt={server.item?.thumbnail.description ?? ''}
|
||||
blurhash={server.item?.thumbnail.blurhash}
|
||||
src={server.item?.thumbnail.url}
|
||||
srcSet={Object.keys(server.item?.thumbnail.versions ?? {}).map((key) => `${server.item?.thumbnail.versions && server.item.thumbnail.versions[key]} ${key.replace('@', '')}`).join(', ')}
|
||||
className='about__header__hero'
|
||||
/>
|
||||
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
|
||||
<h1>{isLoading ? <Skeleton width='10ch' /> : server.domain}</h1>
|
||||
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ class About extends PureComponent {
|
||||
<div className='about__meta__column'>
|
||||
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
|
||||
|
||||
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
|
||||
<Account id={server.item?.contact?.account?.id} size={36} minimal />
|
||||
</div>
|
||||
|
||||
<hr className='about__meta__divider' />
|
||||
@@ -106,12 +106,12 @@ class About extends PureComponent {
|
||||
<div className='about__meta__column'>
|
||||
<h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4>
|
||||
|
||||
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.getIn(['contact', 'email'])}`}>{server.getIn(['contact', 'email'])}</a>}
|
||||
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.item?.contact?.email}`}>{server.item?.contact?.email}</a>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section open title={intl.formatMessage(messages.title)}>
|
||||
{extendedDescription.get('isLoading') ? (
|
||||
{extendedDescription.isLoading ? (
|
||||
<>
|
||||
<Skeleton width='100%' />
|
||||
<br />
|
||||
@@ -121,10 +121,10 @@ class About extends PureComponent {
|
||||
<br />
|
||||
<Skeleton width='70%' />
|
||||
</>
|
||||
) : (extendedDescription.get('content')?.length > 0 ? (
|
||||
) : (extendedDescription.item?.content?.length > 0 ? (
|
||||
<div
|
||||
className='prose'
|
||||
dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }}
|
||||
dangerouslySetInnerHTML={{ __html: extendedDescription.item?.content }}
|
||||
/>
|
||||
) : (
|
||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||
@@ -134,26 +134,26 @@ class About extends PureComponent {
|
||||
<RulesSection />
|
||||
|
||||
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
|
||||
{domainBlocks.get('isLoading') ? (
|
||||
{domainBlocks.isLoading ? (
|
||||
<>
|
||||
<Skeleton width='100%' />
|
||||
<br />
|
||||
<Skeleton width='70%' />
|
||||
</>
|
||||
) : (domainBlocks.get('isAvailable') ? (
|
||||
) : (domainBlocks.isAvailable ? (
|
||||
<>
|
||||
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
||||
|
||||
{domainBlocks.get('items').size > 0 && (
|
||||
{domainBlocks.items.length > 0 && (
|
||||
<div className='about__domain-blocks'>
|
||||
{domainBlocks.get('items').map(block => (
|
||||
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||
{domainBlocks.items.map(block => (
|
||||
<div className='about__domain-blocks__domain' key={block.domain}>
|
||||
<div className='about__domain-blocks__domain__header'>
|
||||
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
||||
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
||||
<h6><span title={`SHA-256: ${block.digest}`}>{block.domain}</span></h6>
|
||||
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.severity].explanation)}>{intl.formatMessage(severityMessages[block.severity].title)}</span>
|
||||
</div>
|
||||
|
||||
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
||||
<p>{(block.comment ?? '').length > 0 ? block.comment : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -37,10 +37,7 @@ const selectTags = createAppSelector(
|
||||
[
|
||||
(state) => state.profileEdit,
|
||||
(state) =>
|
||||
state.server.getIn(
|
||||
['server', 'accounts', 'max_featured_tags'],
|
||||
10,
|
||||
) as number,
|
||||
state.server.server.item?.configuration.accounts.max_featured_tags ?? 0,
|
||||
],
|
||||
(profileEdit, maxTags) => ({
|
||||
tags: profileEdit.profile?.featuredTags ?? [],
|
||||
|
||||
@@ -133,12 +133,7 @@ export const AccountEdit: FC = () => {
|
||||
|
||||
const maxFieldCount = useAppSelector(
|
||||
(state) =>
|
||||
(state.server.getIn([
|
||||
'server',
|
||||
'configuration',
|
||||
'accounts',
|
||||
'max_profile_fields',
|
||||
]) as number | undefined) ?? 4,
|
||||
state.server.server.item?.configuration.accounts.max_profile_fields ?? 4,
|
||||
);
|
||||
|
||||
const handleOpenModal = useCallback(
|
||||
|
||||
@@ -36,13 +36,7 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
||||
);
|
||||
const [newBio, setNewBio] = useState(bio ?? '');
|
||||
const maxLength = useAppSelector(
|
||||
(state) =>
|
||||
state.server.getIn([
|
||||
'server',
|
||||
'configuration',
|
||||
'accounts',
|
||||
'max_note_length',
|
||||
]) as number | undefined,
|
||||
(state) => state.server.server.item?.configuration.accounts.max_note_length,
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -9,8 +9,6 @@ import type { FC, FocusEventHandler } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import type { FieldStatus } from '@/mastodon/components/form_fields';
|
||||
@@ -97,15 +95,10 @@ const messages = defineMessages({
|
||||
// We have two different values- the hard limit set by the server,
|
||||
// and the soft limit for mobile display.
|
||||
const selectFieldLimits = createAppSelector(
|
||||
[
|
||||
(state) =>
|
||||
state.server.getIn(['server', 'configuration', 'accounts']) as
|
||||
| ImmutableMap<string, number>
|
||||
| undefined,
|
||||
],
|
||||
[(state) => state.server.server.item?.configuration.accounts],
|
||||
(accounts) => ({
|
||||
nameLimit: accounts?.get('profile_field_name_limit'),
|
||||
valueLimit: accounts?.get('profile_field_value_limit'),
|
||||
nameLimit: accounts?.profile_field_name_limit,
|
||||
valueLimit: accounts?.profile_field_value_limit,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -84,15 +84,8 @@ export const ImageAltTextField: FC<{
|
||||
}> = ({ imageSrc, altText, onChange, hideTip }) => {
|
||||
const altLimit = useAppSelector(
|
||||
(state) =>
|
||||
state.server.getIn(
|
||||
[
|
||||
'server',
|
||||
'configuration',
|
||||
'accounts',
|
||||
'max_header_description_length',
|
||||
],
|
||||
150,
|
||||
) as number,
|
||||
state.server.server.item?.configuration.accounts
|
||||
.max_header_description_length ?? 0,
|
||||
);
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
|
||||
@@ -33,12 +33,7 @@ export const NameModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
||||
);
|
||||
const maxLength = useAppSelector(
|
||||
(state) =>
|
||||
state.server.getIn([
|
||||
'server',
|
||||
'configuration',
|
||||
'accounts',
|
||||
'max_display_name_length',
|
||||
]) as number | undefined,
|
||||
state.server.server.item?.configuration.accounts.max_display_name_length,
|
||||
);
|
||||
|
||||
const [newName, setNewName] = useState(displayName ?? '');
|
||||
|
||||
@@ -2,14 +2,15 @@ import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { DisplayName } from '@/mastodon/components/display_name';
|
||||
import { EmptyState } from '@/mastodon/components/empty_state';
|
||||
import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
|
||||
import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
@@ -28,8 +29,8 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
blockedBy,
|
||||
withoutAddCollectionButton,
|
||||
}) => {
|
||||
const { acct } = useParams<{ acct?: string }>();
|
||||
const me = useCurrentAccountId();
|
||||
const account = useAccount(accountId);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -116,12 +117,12 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if (acct) {
|
||||
if (account) {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured.other'
|
||||
defaultMessage='{acct} has not featured anything yet.'
|
||||
values={{ acct }}
|
||||
values={{ acct: <DisplayName variant='simple' account={account} /> }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -31,7 +31,7 @@ const selectServerName = createAppSelector(
|
||||
[
|
||||
(state) => state.accounts,
|
||||
(_, accountId: string) => accountId,
|
||||
(state) => state.server.getIn(['server', 'domain']) as string | undefined,
|
||||
(state) => state.server.server.item?.domain,
|
||||
],
|
||||
(accounts, accountId, serverDomain) => {
|
||||
const acct = accounts.getIn([accountId, 'acct']) as string | undefined;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.wrapper {
|
||||
--list-item-gap: 16px;
|
||||
--list-item-padding-block: 12px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
&:has(input:disabled) {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useId } from 'react';
|
||||
|
||||
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||
import { Toggle } from '@/mastodon/components/form_fields';
|
||||
import {
|
||||
ListItemContent,
|
||||
ListItemWrapper,
|
||||
} from '@/mastodon/components/list_item';
|
||||
import {
|
||||
AvatarGrid,
|
||||
CollectionInfo,
|
||||
} from 'mastodon/features/collections/components/collection_lockup';
|
||||
|
||||
import classes from './collection_toggle.module.scss';
|
||||
|
||||
export interface CollectionToggleProps {
|
||||
collection: ApiCollectionJSON;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
subtitle?: React.ReactNode;
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export const CollectionToggle: React.FC<CollectionToggleProps> = ({
|
||||
collection,
|
||||
checked,
|
||||
disabled,
|
||||
subtitle,
|
||||
onChange,
|
||||
}) => {
|
||||
const uniqueId = useId();
|
||||
const toggleId = `${uniqueId}-toggle`;
|
||||
const infoId = `${uniqueId}-info`;
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
className={classes.wrapper}
|
||||
icon={
|
||||
<AvatarGrid
|
||||
accountIds={collection.items.map((item) => item.account_id)}
|
||||
sensitive={collection.sensitive}
|
||||
/>
|
||||
}
|
||||
sideContent={
|
||||
<Toggle
|
||||
id={toggleId}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
aria-describedby={infoId}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ListItemContent
|
||||
as='label'
|
||||
htmlFor={toggleId}
|
||||
subtitle={
|
||||
subtitle ?? (
|
||||
<CollectionInfo
|
||||
collection={collection}
|
||||
withTimestamp={false}
|
||||
withAuthorHandle={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{collection.name}
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
);
|
||||
};
|
||||
143
app/javascript/mastodon/features/collection_adder/index.tsx
Normal file
143
app/javascript/mastodon/features/collection_adder/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useCallback, useId, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import {
|
||||
addCollectionItem,
|
||||
removeCollectionItem,
|
||||
} from '@/mastodon/reducers/slices/collections';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { MAX_COLLECTION_ACCOUNT_COUNT } from '../collections/editor/accounts';
|
||||
import { useCollectionsCreatedBy } from '../collections/overview/created_by_you';
|
||||
|
||||
import { CollectionToggle } from './collection_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: {
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const ListItem: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
account: Account;
|
||||
}> = ({ collection, account }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const accountItemInCollection = collection.items.find(
|
||||
(item) => item.account_id === account.id,
|
||||
);
|
||||
const isAccountInCollection = !!accountItemInCollection;
|
||||
|
||||
const addOrRemove = useCallback(
|
||||
async (shouldAdd: boolean) => {
|
||||
setIsUpdating(true);
|
||||
|
||||
if (shouldAdd) {
|
||||
await dispatch(
|
||||
addCollectionItem({
|
||||
collectionId: collection.id,
|
||||
accountId: account.id,
|
||||
}),
|
||||
);
|
||||
} else if (accountItemInCollection) {
|
||||
await dispatch(
|
||||
removeCollectionItem({
|
||||
collectionId: collection.id,
|
||||
itemId: accountItemInCollection.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setIsUpdating(false);
|
||||
},
|
||||
[account.id, collection.id, accountItemInCollection, dispatch],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
void addOrRemove(e.target.checked);
|
||||
},
|
||||
[addOrRemove],
|
||||
);
|
||||
|
||||
const hasMaxItemCount =
|
||||
!isAccountInCollection &&
|
||||
collection.item_count >= MAX_COLLECTION_ACCOUNT_COUNT;
|
||||
|
||||
return (
|
||||
<CollectionToggle
|
||||
key={collection.id}
|
||||
collection={collection}
|
||||
disabled={isUpdating || hasMaxItemCount}
|
||||
subtitle={
|
||||
hasMaxItemCount ? (
|
||||
<FormattedMessage
|
||||
id='collections.search_accounts_max_reached'
|
||||
defaultMessage='You have added the maximum number of accounts'
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
checked={isAccountInCollection}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionAdder: React.FC<{
|
||||
accountId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ accountId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const titleId = useId();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const currentAccountId = useCurrentAccountId();
|
||||
const { collections, status } = useCollectionsCreatedBy(currentAccountId);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<IconButton
|
||||
className='dialog-modal__header__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title' id={titleId}>
|
||||
<FormattedMessage
|
||||
id='collections.add_to_collection'
|
||||
defaultMessage='Add {name} to collections'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div
|
||||
className='lists-scrollable'
|
||||
role='group'
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
{status === 'loading' || !account ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
collections.map((item) => (
|
||||
<ListItem key={item.id} collection={item} account={account} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -57,10 +57,45 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { id, name } = collection;
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
className={classNames(classes.wrapper, className)}
|
||||
icon={
|
||||
<AvatarGrid
|
||||
accountIds={collection.items.map((item) => item.account_id)}
|
||||
sensitive={collection.sensitive}
|
||||
/>
|
||||
}
|
||||
sideContent={sideContent}
|
||||
>
|
||||
<ListItemLink
|
||||
as='h3'
|
||||
to={getCollectionPath(id)}
|
||||
subtitle={
|
||||
<CollectionInfo
|
||||
collection={collection}
|
||||
withAuthorHandle={withAuthorHandle}
|
||||
withTimestamp={withTimestamp}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</ListItemLink>
|
||||
</ListItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionInfo: React.FC<
|
||||
Pick<
|
||||
CollectionLockupProps,
|
||||
'collection' | 'withAuthorHandle' | 'withTimestamp'
|
||||
>
|
||||
> = ({ collection, withAuthorHandle, withTimestamp }) => {
|
||||
const authorAccount = useAccount(collection.account_id);
|
||||
const authorHandle = useAccountHandle(authorAccount, domain);
|
||||
|
||||
const collectionInfo = (
|
||||
return (
|
||||
<ul>
|
||||
{collection.sensitive && (
|
||||
<li className='sr-only'>
|
||||
@@ -98,25 +133,4 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
className={classNames(classes.wrapper, className)}
|
||||
icon={
|
||||
<AvatarGrid
|
||||
accountIds={collection.items.map((item) => item.account_id)}
|
||||
sensitive={collection.sensitive}
|
||||
/>
|
||||
}
|
||||
sideContent={sideContent}
|
||||
>
|
||||
<ListItemLink
|
||||
as='h3'
|
||||
to={getCollectionPath(id)}
|
||||
subtitle={collectionInfo}
|
||||
>
|
||||
{name}
|
||||
</ListItemLink>
|
||||
</ListItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,11 +24,13 @@ import classes from './share_modal.module.scss';
|
||||
const messages = defineMessages({
|
||||
shareTextOwn: {
|
||||
id: 'collection.share_template_own',
|
||||
defaultMessage: 'Check out my new collection: {link}',
|
||||
defaultMessage: 'Check out my new collection:',
|
||||
description: 'Collection links are appended after a new line',
|
||||
},
|
||||
shareTextOther: {
|
||||
id: 'collection.share_template_other',
|
||||
defaultMessage: 'Check out this cool collection: {link}',
|
||||
defaultMessage: 'Check out this cool collection:',
|
||||
description: 'Collection links are appended after a new line',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,13 +53,10 @@ export const CollectionShareModal: React.FC<{
|
||||
}, [collectionLink]);
|
||||
|
||||
const handleShareViaPost = useCallback(() => {
|
||||
const shareMessage = isOwnCollection
|
||||
? intl.formatMessage(messages.shareTextOwn, {
|
||||
link: collectionLink,
|
||||
})
|
||||
: intl.formatMessage(messages.shareTextOther, {
|
||||
link: collectionLink,
|
||||
});
|
||||
let shareMessage = isOwnCollection
|
||||
? intl.formatMessage(messages.shareTextOwn)
|
||||
: intl.formatMessage(messages.shareTextOther);
|
||||
shareMessage += `\n\n${collectionLink}`;
|
||||
|
||||
onClose();
|
||||
dispatch(changeCompose(shareMessage));
|
||||
|
||||
@@ -39,11 +39,12 @@ import {
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { PendingNote } from '../detail';
|
||||
import { canAccountBeAdded, canAccountBeAddedByFollowers } from '../utils';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
import { WizardStepTitle } from './wizard_step_title';
|
||||
|
||||
const MAX_ACCOUNT_COUNT = 25;
|
||||
export const MAX_COLLECTION_ACCOUNT_COUNT = 25;
|
||||
|
||||
const AddedAccountItem: React.FC<{
|
||||
accountId: string;
|
||||
@@ -99,9 +100,6 @@ const renderAccountItem = (account: ApiMutedAccountJSON) => (
|
||||
|
||||
type GroupKey = 'available' | 'mustFollow' | 'disabled';
|
||||
|
||||
const canAccountBeAdded = (account: ApiMutedAccountJSON) =>
|
||||
['automatic', 'manual'].includes(account.feature_approval.current_user);
|
||||
|
||||
function groupSuggestions(
|
||||
accounts: ApiMutedAccountJSON[],
|
||||
relationships: ImmutableMap<string, Relationship>,
|
||||
@@ -113,12 +111,8 @@ function groupSuggestions(
|
||||
return 'available';
|
||||
}
|
||||
|
||||
const canAccountBeAddedByFollowers =
|
||||
account.feature_approval.automatic.includes('followers') ||
|
||||
account.feature_approval.manual.includes('followers');
|
||||
|
||||
if (
|
||||
canAccountBeAddedByFollowers &&
|
||||
canAccountBeAddedByFollowers(account) &&
|
||||
!relationships.get(account.id)?.following
|
||||
) {
|
||||
return 'mustFollow';
|
||||
@@ -212,7 +206,7 @@ export const CollectionAccounts: React.FC<{
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const hasItems = editorItems.length > 0;
|
||||
const hasMaxItems = editorItems.length === MAX_ACCOUNT_COUNT;
|
||||
const hasMaxItems = editorItems.length === MAX_COLLECTION_ACCOUNT_COUNT;
|
||||
|
||||
const {
|
||||
accounts: suggestedAccounts,
|
||||
@@ -406,7 +400,10 @@ export const CollectionAccounts: React.FC<{
|
||||
<FormattedMessage
|
||||
id='collections.hints.accounts_counter'
|
||||
defaultMessage='{count}/{max} accounts'
|
||||
values={{ count: editorItems.length, max: MAX_ACCOUNT_COUNT }}
|
||||
values={{
|
||||
count: editorItems.length,
|
||||
max: MAX_COLLECTION_ACCOUNT_COUNT,
|
||||
}}
|
||||
/>
|
||||
</AccountsHeadingElement>
|
||||
)}
|
||||
@@ -426,7 +423,7 @@ export const CollectionAccounts: React.FC<{
|
||||
id='collections.accounts.empty_description'
|
||||
defaultMessage='Add up to {count} accounts'
|
||||
values={{
|
||||
count: MAX_ACCOUNT_COUNT,
|
||||
count: MAX_COLLECTION_ACCOUNT_COUNT,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ApiMutedAccountJSON } from '@/mastodon/api_types/accounts';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
||||
|
||||
export function areCollectionsEnabled() {
|
||||
@@ -5,3 +7,12 @@ export function areCollectionsEnabled() {
|
||||
}
|
||||
|
||||
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
||||
|
||||
export const canAccountBeAdded = (account: ApiMutedAccountJSON | Account) =>
|
||||
['automatic', 'manual'].includes(account.feature_approval.current_user);
|
||||
|
||||
export const canAccountBeAddedByFollowers = (
|
||||
account: ApiMutedAccountJSON | Account,
|
||||
) =>
|
||||
account.feature_approval.automatic.includes('followers') ||
|
||||
account.feature_approval.manual.includes('followers');
|
||||
|
||||
@@ -82,10 +82,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
autoFocus: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
highlighted: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.textareaRef = createRef(null);
|
||||
@@ -220,8 +216,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
Promise.resolve().then(() => {
|
||||
this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.textareaRef.current.focus();
|
||||
this.setState({ highlighted: true });
|
||||
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
||||
}).catch(console.error);
|
||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||
this.textareaRef.current.focus();
|
||||
@@ -252,7 +246,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
render () {
|
||||
const { intl, onPaste, onDrop, autoFocus, withoutNavigation, maxChars, isSubmitting } = this.props;
|
||||
const { highlighted } = this.state;
|
||||
|
||||
return (
|
||||
<form className='compose-form' onSubmit={this.handleSubmit}>
|
||||
@@ -260,7 +253,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
{!withoutNavigation && <NavigationBar />}
|
||||
<Warning />
|
||||
|
||||
<div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
|
||||
<div className='compose-form__highlightable' ref={this.setRef}>
|
||||
<EditIndicator />
|
||||
|
||||
<div className='compose-form__dropdowns'>
|
||||
|
||||
@@ -228,7 +228,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||
|
||||
handleClick = (emoji, event) => {
|
||||
if (!emoji.native) {
|
||||
emoji.native = emoji.colons;
|
||||
emoji.native = `:${emoji.id}:`;
|
||||
}
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
|
||||
|
||||
@@ -419,9 +419,7 @@ const InteractionModal: React.FC<{
|
||||
}> = ({ accountId, url, intent }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const signupUrl = useAppSelector(
|
||||
(state) =>
|
||||
(state.server.getIn(['server', 'registrations', 'url'], null) ||
|
||||
'/auth/sign_up') as string,
|
||||
(state) => state.server.server.item?.registrations.url ?? '/auth/sign_up',
|
||||
);
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const name = <DisplayName account={account} variant='simple' />;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useId } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import { Toggle } from '@/mastodon/components/form_fields';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
} from 'mastodon/api/lists';
|
||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||
@@ -42,6 +42,8 @@ const ListItem: React.FC<{
|
||||
checked: boolean;
|
||||
onChange: (id: string, checked: boolean) => void;
|
||||
}> = ({ id, title, checked, onChange }) => {
|
||||
const uniqueId = useId();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.checked);
|
||||
@@ -50,14 +52,13 @@ const ListItem: React.FC<{
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='lists__item'>
|
||||
<label className='lists__item' htmlFor={uniqueId}>
|
||||
<div className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||
<Toggle id={uniqueId} checked={checked} onChange={handleChange} />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,10 +20,7 @@ export const SignInBanner: React.FC = () => {
|
||||
let signupButton: React.ReactNode;
|
||||
|
||||
const signupUrl = useAppSelector(
|
||||
(state) =>
|
||||
(state.server.getIn(['server', 'registrations', 'url'], null) as
|
||||
| string
|
||||
| null) ?? '/auth/sign_up',
|
||||
(state) => state.server.server.item?.registrations.url ?? '/auth/sign_up',
|
||||
);
|
||||
|
||||
if (sso_redirect) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { List } from 'immutable';
|
||||
import type { List, Map } from 'immutable';
|
||||
|
||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||
@@ -23,8 +23,19 @@ export const EmbeddedStatusContent: React.FC<{
|
||||
},
|
||||
[mentions],
|
||||
);
|
||||
const hrefToCollection = useCallback(
|
||||
(href: string) => {
|
||||
const collections = status.get('tagged_collections') as List<
|
||||
Map<'url' | 'id', string>
|
||||
>;
|
||||
const collection = collections.find((item) => item.get('url') === href);
|
||||
return collection?.get('id');
|
||||
},
|
||||
[status],
|
||||
);
|
||||
const htmlHandlers = useElementHandledLink({
|
||||
hashtagAccountId: status.get('account') as string | undefined,
|
||||
hrefToCollectionId: hrefToCollection,
|
||||
hrefToMention,
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ export const MODAL_COMPONENTS = {
|
||||
'DOMAIN_BLOCK': DomainBlockModal,
|
||||
'REPORT': ReportModal,
|
||||
'REPORT_COLLECTION': ReportCollectionModal,
|
||||
'COLLECTION_ADDER': () => import('@/mastodon/features/collection_adder').then(module => ({ default: module.CollectionAdder })),
|
||||
'SHARE_COLLECTION': () => import('@/mastodon/features/collections/components/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
||||
'REVOKE_COLLECTION_INCLUSION': () => import('@/mastodon/features/collections/detail/revoke_collection_inclusion_modal').then(module => ({ default: module.RevokeCollectionInclusionModal })),
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
|
||||
@@ -84,10 +84,7 @@ const NotificationsButton = () => {
|
||||
const LoginOrSignUp: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const signupUrl = useAppSelector(
|
||||
(state) =>
|
||||
(state.server.getIn(['server', 'registrations', 'url'], null) as
|
||||
| string
|
||||
| null) ?? '/auth/sign_up',
|
||||
(state) => state.server.server.item?.registrations.url ?? '/auth/sign_up',
|
||||
);
|
||||
|
||||
const openClosedRegistrationsModal = useCallback(() => {
|
||||
@@ -95,7 +92,7 @@ const LoginOrSignUp: React.FC = () => {
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchServer());
|
||||
void dispatch(fetchServer());
|
||||
}, [dispatch]);
|
||||
|
||||
if (sso_redirect) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ReportCollectionModal: React.FC<{
|
||||
const account = useAccount(account_id);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchServer());
|
||||
void dispatch(fetchServer());
|
||||
}, [dispatch]);
|
||||
|
||||
const [submitState, setSubmitState] = useState<
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Гэты ўліковы запіс пазначаны як схаваны. Уладальнік сам вырашае, хто можа падпісвацца на яго.",
|
||||
"account.media": "Медыя",
|
||||
"account.mention": "Згадаць @{name}",
|
||||
"account.menu.add_to_collection": "Дадаць у калекцыю…",
|
||||
"account.menu.add_to_list": "Дадаць у спіс…",
|
||||
"account.menu.block": "Заблакіраваць профіль",
|
||||
"account.menu.block_domain": "Заблакіраваць {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў",
|
||||
"collections.accounts.empty_editor_title": "У гэтай калекцыі пакуль нікога няма",
|
||||
"collections.accounts.empty_title": "Гэтая калекцыя пустая",
|
||||
"collections.add_to_collection": "Дадаць {name} у калекцыі",
|
||||
"collections.block_collection_owner": "Заблакіраваць профіль",
|
||||
"collections.by_account": "ад {account_handle}",
|
||||
"collections.collection_description": "Апісанне",
|
||||
@@ -394,6 +396,7 @@
|
||||
"collections.detail.loading": "Загружаецца калекцыя…",
|
||||
"collections.detail.revoke_inclusion": "Прыбраць сябе",
|
||||
"collections.detail.sensitive_content": "Адчувальнае змесціва",
|
||||
"collections.detail.sensitive_note": "Апісанне і ўліковыя запісы могуць не пасаваць усім гледачам.",
|
||||
"collections.detail.share": "Падзяліцца гэтай калекцыяй",
|
||||
"collections.detail.you_are_in_this_collection": "Вас уключылі ў гэтую калекцыю",
|
||||
"collections.edit_details": "Рэдагаваць падрабязнасці",
|
||||
@@ -424,6 +427,11 @@
|
||||
"collections.search_accounts_max_reached": "Вы дадалі максімальную колькасць уліковых запісаў",
|
||||
"collections.sensitive": "Адчувальная",
|
||||
"collections.share_short": "Абагуліць",
|
||||
"collections.sort_alphabetical": "Алфавіце",
|
||||
"collections.sort_by": "Сартаваць па:",
|
||||
"collections.sort_date_added": "Даце дадавання",
|
||||
"collections.sort_last_active": "Апошняй актыўнасці",
|
||||
"collections.sort_most_followers": "Колькасці падпісчыкаў",
|
||||
"collections.suggestions.can_not_add": "Немагчыма дадаць",
|
||||
"collections.suggestions.can_not_add_desc": "Магчыма, гэтыя ўліковыя запісы схаваныя ад рэкамендацый або знаходзяцца на серверы, які не падтрымлівае калекцыі.",
|
||||
"collections.suggestions.must_follow": "Патрабуецца падпіска",
|
||||
@@ -635,6 +643,7 @@
|
||||
"empty_column.blocks": "Вы яшчэ нікога не заблакіравалі.",
|
||||
"empty_column.bookmarked_statuses": "У Вашых закладках яшчэ няма допісаў. Калі Вы дадасце закладку, яна з’явіцца тут.",
|
||||
"empty_column.collections.featured_in": "Вас пакуль не дадалі ў ніякія калекцыі.",
|
||||
"empty_column.collections.featured_in_undiscoverable": "Каб людзі маглі дадаваць Вас у калекцыі, Вам трэба даць ім дазвол знаходзіць Вас у <link>Налады > Прыватнасць і пошук</link>",
|
||||
"empty_column.community": "Мясцовая стужка пустая. Напішыце нешта публічнае, каб разварушыць справу!",
|
||||
"empty_column.direct": "Пакуль у Вас няма асабістых згадванняў. Калі Вы дашляце або атрымаеце штосьці, яно з’явіцца тут.",
|
||||
"empty_column.disabled_feed": "Гэта стужка была адключаная Вашымі адміністратарамі сервера.",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Denne kontos fortrolighedsstatus er sat til låst. Ejeren bedømmer manuelt, hvem der kan følge vedkommende.",
|
||||
"account.media": "Medier",
|
||||
"account.mention": "Nævn @{name}",
|
||||
"account.menu.add_to_collection": "Føj til samling…",
|
||||
"account.menu.add_to_list": "Føj til liste…",
|
||||
"account.menu.block": "Blokér konto",
|
||||
"account.menu.block_domain": "Blokér {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Tilføj op til {count} konti",
|
||||
"collections.accounts.empty_editor_title": "Ingen er i denne samling endnu",
|
||||
"collections.accounts.empty_title": "Denne samling er tom",
|
||||
"collections.add_to_collection": "Tilføj {name} til samlinger",
|
||||
"collections.block_collection_owner": "Blokér konto",
|
||||
"collections.by_account": "af {account_handle}",
|
||||
"collections.collection_description": "Beskrivelse",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Die Privatsphäre dieses Kontos wurde auf „geschützt“ gesetzt. Die Person bestimmt manuell, wer ihrem Profil folgen darf.",
|
||||
"account.media": "Medien",
|
||||
"account.mention": "@{name} erwähnen",
|
||||
"account.menu.add_to_collection": "Zur Sammlung hinzufügen …",
|
||||
"account.menu.add_to_list": "Einer Liste hinzufügen …",
|
||||
"account.menu.block": "Konto blockieren",
|
||||
"account.menu.block_domain": "{domain} blockieren",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Füge bis zu {count} Konten hinzu",
|
||||
"collections.accounts.empty_editor_title": "Noch befindet sich niemand in dieser Sammlung",
|
||||
"collections.accounts.empty_title": "Diese Sammlung ist leer",
|
||||
"collections.add_to_collection": "{name} zur Sammlung hinzufügen",
|
||||
"collections.block_collection_owner": "Konto blockieren",
|
||||
"collections.by_account": "von {account_handle}",
|
||||
"collections.collection_description": "Beschreibung",
|
||||
@@ -426,7 +428,7 @@
|
||||
"collections.sensitive": "Inhaltswarnung",
|
||||
"collections.share_short": "Teilen",
|
||||
"collections.sort_alphabetical": "Alphabetisch",
|
||||
"collections.sort_by": "Sortierung:",
|
||||
"collections.sort_by": "Sortieren nach:",
|
||||
"collections.sort_date_added": "Datum des Hinzufügens",
|
||||
"collections.sort_last_active": "Neueste Aktivität",
|
||||
"collections.sort_most_followers": "Followerzahl",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Η κατάσταση απορρήτου αυτού του λογαριασμού έχει ρυθμιστεί σε κλειδωμένη. Ο ιδιοκτήτης ελέγχει χειροκίνητα ποιος μπορεί να τον ακολουθήσει.",
|
||||
"account.media": "Πολυμέσα",
|
||||
"account.mention": "Επισήμανση @{name}",
|
||||
"account.menu.add_to_collection": "Προσθήκη σε συλλογή…",
|
||||
"account.menu.add_to_list": "Προσθήκη στη λίστα…",
|
||||
"account.menu.block": "Αποκλεισμός λογαριασμού",
|
||||
"account.menu.block_domain": "Αποκλεισμός {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς",
|
||||
"collections.accounts.empty_editor_title": "Κανείς δεν είναι ακόμη σε αυτήν τη συλλογή",
|
||||
"collections.accounts.empty_title": "Αυτή η συλλογή είναι κενή",
|
||||
"collections.add_to_collection": "Προσθήκη {name} σε συλλογές",
|
||||
"collections.block_collection_owner": "Αποκλεισμός λογαριασμού",
|
||||
"collections.by_account": "από {account_handle}",
|
||||
"collections.collection_description": "Περιγραφή",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
||||
"account.media": "Media",
|
||||
"account.mention": "Mention @{name}",
|
||||
"account.menu.add_to_collection": "Add to collection…",
|
||||
"account.menu.add_to_list": "Add to list…",
|
||||
"account.menu.block": "Block account",
|
||||
"account.menu.block_domain": "Block {domain}",
|
||||
@@ -366,12 +367,13 @@
|
||||
"collection.share_modal.share_via_system": "Share to…",
|
||||
"collection.share_modal.title": "Share collection",
|
||||
"collection.share_modal.title_new": "Share your new collection!",
|
||||
"collection.share_template_other": "Check out this cool collection: {link}",
|
||||
"collection.share_template_own": "Check out my new collection: {link}",
|
||||
"collection.share_template_other": "Check out this cool collection:",
|
||||
"collection.share_template_own": "Check out my new collection:",
|
||||
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
|
||||
"collections.accounts.empty_description": "Add up to {count} accounts",
|
||||
"collections.accounts.empty_editor_title": "No one is in this collection yet",
|
||||
"collections.accounts.empty_title": "This collection is empty",
|
||||
"collections.add_to_collection": "Add {name} to collections",
|
||||
"collections.block_collection_owner": "Block account",
|
||||
"collections.by_account": "by {account_handle}",
|
||||
"collections.collection_description": "Description",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Esta cuenta es privada. El propietario manualmente revisa quién puede seguirle.",
|
||||
"account.media": "Medios",
|
||||
"account.mention": "Mencionar a @{name}",
|
||||
"account.menu.add_to_collection": "Agregar a la colección…",
|
||||
"account.menu.add_to_list": "Añadir a lista…",
|
||||
"account.menu.block": "Bloquear cuenta",
|
||||
"account.menu.block_domain": "Bloquear a {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Agregá hasta {count} cuentas",
|
||||
"collections.accounts.empty_editor_title": "Todavía no hay nadie en esta colección",
|
||||
"collections.accounts.empty_title": "Esta colección está vacía",
|
||||
"collections.add_to_collection": "Agregar {name} a las colecciones",
|
||||
"collections.block_collection_owner": "Bloquear cuenta",
|
||||
"collections.by_account": "por {account_handle}",
|
||||
"collections.collection_description": "Descripción",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "El estado de privacidad de esta cuenta está configurado como bloqueado. El propietario revisa manualmente quién puede seguirlo.",
|
||||
"account.media": "Multimedia",
|
||||
"account.mention": "Mencionar a @{name}",
|
||||
"account.menu.add_to_collection": "Añadir a colección…",
|
||||
"account.menu.add_to_list": "Añadir a lista…",
|
||||
"account.menu.block": "Bloquear cuenta",
|
||||
"account.menu.block_domain": "Bloquear {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Añade hasta {count} cuentas",
|
||||
"collections.accounts.empty_editor_title": "No hay nadie en esta colección todavía",
|
||||
"collections.accounts.empty_title": "Esta colección está vacía",
|
||||
"collections.add_to_collection": "Añadir a {name} a colecciones",
|
||||
"collections.block_collection_owner": "Bloquer cuenta",
|
||||
"collections.by_account": "de {account_handle}",
|
||||
"collections.collection_description": "Descripción",
|
||||
@@ -394,7 +396,7 @@
|
||||
"collections.detail.loading": "Cargando colección…",
|
||||
"collections.detail.revoke_inclusion": "Excluirme",
|
||||
"collections.detail.sensitive_content": "Contenido sensible",
|
||||
"collections.detail.sensitive_note": "La descripción y cuentas pueden no ser adecuadas para todas las personas.",
|
||||
"collections.detail.sensitive_note": "Es posible que la descripción y las cuentas no sean aptas para todos las personas.",
|
||||
"collections.detail.share": "Compartir esta colección",
|
||||
"collections.detail.you_are_in_this_collection": "Apareces en esta colección",
|
||||
"collections.edit_details": "Editar detalles",
|
||||
@@ -641,7 +643,7 @@
|
||||
"empty_column.blocks": "Aún no has bloqueado a ningún usuario.",
|
||||
"empty_column.bookmarked_statuses": "Aún no tienes ninguna publicación guardada como marcador. Cuando guardes una, se mostrará aquí.",
|
||||
"empty_column.collections.featured_in": "Aún no te han añadido a ninguna colección.",
|
||||
"empty_column.collections.featured_in_undiscoverable": "Para que la gente pueda añadirte a colecciones, debes permitir ser destacado en algoritmos de descubrimiento desde <link>Preferencias > Privacidad y alcance</link>",
|
||||
"empty_column.collections.featured_in_undiscoverable": "Para que los usuarios puedan añadirte a sus colecciones, debes habilitar la opción de aparecer en las experiencias de descubrimiento desde <link>Preferencias > Privacidad y alcance</link>",
|
||||
"empty_column.community": "La cronología local está vacía. ¡Escribe algo públicamente para ponerla en marcha!",
|
||||
"empty_column.direct": "Aún no tienes ninguna mención privada. Cuando envíes o recibas una, aparecerá aquí.",
|
||||
"empty_column.disabled_feed": "Esta cronología fue desactivada por los administradores de tu servidor.",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "El estado de privacidad de esta cuenta está configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.",
|
||||
"account.media": "Multimedia",
|
||||
"account.mention": "Mencionar a @{name}",
|
||||
"account.menu.add_to_collection": "Añadir a colección…",
|
||||
"account.menu.add_to_list": "Añadir a lista…",
|
||||
"account.menu.block": "Bloquear cuenta",
|
||||
"account.menu.block_domain": "Bloquear {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Añade hasta {count} cuentas",
|
||||
"collections.accounts.empty_editor_title": "No hay nadie en esta colección todavía",
|
||||
"collections.accounts.empty_title": "Esta colección está vacía",
|
||||
"collections.add_to_collection": "Añadir {name} a colecciones",
|
||||
"collections.block_collection_owner": "Bloquear cuenta",
|
||||
"collections.by_account": "de {account_handle}",
|
||||
"collections.collection_description": "Descripción",
|
||||
@@ -425,9 +427,9 @@
|
||||
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
|
||||
"collections.sensitive": "Sensible",
|
||||
"collections.share_short": "Compartir",
|
||||
"collections.sort_alphabetical": "Alfabético",
|
||||
"collections.sort_by": "Orden:",
|
||||
"collections.sort_date_added": "Añadido a fecha",
|
||||
"collections.sort_alphabetical": "Alfabéticamente",
|
||||
"collections.sort_by": "Ordenar por:",
|
||||
"collections.sort_date_added": "Fecha de inclusión",
|
||||
"collections.sort_last_active": "Última actividad",
|
||||
"collections.sort_most_followers": "Más seguidores",
|
||||
"collections.suggestions.can_not_add": "No puede ser añadida",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Selle konto privaatsusolek on „lukustatud“. Omanik vaatab üle, kes teda jälgida saab.",
|
||||
"account.media": "Meedium",
|
||||
"account.mention": "Maini kasutajat @{name}",
|
||||
"account.menu.add_to_collection": "Lisa kogumikku…",
|
||||
"account.menu.add_to_list": "Lisa loendisse…",
|
||||
"account.menu.block": "Blokeeri kasutajakonto",
|
||||
"account.menu.block_domain": "Blokeeri {domain}",
|
||||
@@ -147,7 +148,7 @@
|
||||
"account.unmute": "Lõpeta {name} kasutaja summutamine",
|
||||
"account.unmute_notifications_short": "Lõpeta teavituste summutamine",
|
||||
"account.unmute_short": "Lõpeta summutamine",
|
||||
"account_edit.advanced_settings.bot_hint": "Teavita teisi, et sel kontol tehakse peamiselt automatiseeritud toiminguid ja seda ei pruugita jälgida",
|
||||
"account_edit.advanced_settings.bot_hint": "Teavita teisi, et sel kasutajakontol tehakse peamiselt automatiseeritud toiminguid ja seda ei pruugita jälgida",
|
||||
"account_edit.advanced_settings.bot_label": "Automatiseeritud kasutajakonto",
|
||||
"account_edit.advanced_settings.title": "Täpsemad seadistused",
|
||||
"account_edit.bio.add_label": "Lisa elulugu",
|
||||
@@ -227,7 +228,7 @@
|
||||
"account_edit.profile_tab.show_relations.description": "Näitab sinu profiilis teistele kasutajatele kontosid, mida jälgid ning su jälgijaid. Inimesed saavad endiselt näha, kas sa neid jälgid.",
|
||||
"account_edit.profile_tab.show_relations.title": "Näita „Jälgijad“ ja „Jälgib“",
|
||||
"account_edit.profile_tab.subtitle": "Kohanda, kuidas su profiili näidatakse.",
|
||||
"account_edit.profile_tab.title": "Profiili näitamise seaded",
|
||||
"account_edit.profile_tab.title": "Profiili näitamise seadistused",
|
||||
"account_edit.save": "Salvesta",
|
||||
"account_edit.upload_modal.back": "Tagasi",
|
||||
"account_edit.upload_modal.done": "Valmis",
|
||||
@@ -365,13 +366,14 @@
|
||||
"collection.share_modal.share_via_post": "Postita Mastodonis",
|
||||
"collection.share_modal.share_via_system": "Jaga kohas…",
|
||||
"collection.share_modal.title": "Jaga kogumikku",
|
||||
"collection.share_modal.title_new": "Jaga oma ut kogumikku!",
|
||||
"collection.share_modal.title_new": "Jaga oma uut kogumikku!",
|
||||
"collection.share_template_other": "Vaata seda lahedat kogumikku: {link}",
|
||||
"collection.share_template_own": "Vaata mu uut kogumikku: {link}",
|
||||
"collections.account_count": "{count, plural, one {# kasutajakonto} other {# kasutajakontot}}",
|
||||
"collections.accounts.empty_description": "Lisa kuni {count} kontot",
|
||||
"collections.accounts.empty_description": "Lisa kuni {count} kasutajakontot",
|
||||
"collections.accounts.empty_editor_title": "Selles kogumikus pole veel kedagi",
|
||||
"collections.accounts.empty_title": "See kogumik on tühi",
|
||||
"collections.add_to_collection": "Lisa „{name}“ kogumikku",
|
||||
"collections.block_collection_owner": "Konto blokeerimine",
|
||||
"collections.by_account": "{account_handle} poolt",
|
||||
"collections.collection_description": "Kirjeldus",
|
||||
@@ -383,7 +385,7 @@
|
||||
"collections.content_warning": "Sisuhoiatus",
|
||||
"collections.continue": "Jätka",
|
||||
"collections.copy_link": "Kopeeri link",
|
||||
"collections.copy_link_confirmation": "Kogumiku link kopeeriti lõikelauale",
|
||||
"collections.copy_link_confirmation": "Kogumiku link on kopeeritud lõikelauale",
|
||||
"collections.create.accounts_title": "Kes saavad olema selles kogumikus?",
|
||||
"collections.create.basic_details_title": "Põhiandmed",
|
||||
"collections.create.steps": "Samm {step}/{total}",
|
||||
@@ -422,7 +424,7 @@
|
||||
"collections.revoke_inclusion.confirmation": "Oled eemaldatud \"{collection}\"-st",
|
||||
"collections.revoke_inclusion.error": "Oli viga, palun proovi hiljem uuesti.",
|
||||
"collections.search_accounts_label": "Otsi kontot, mida lisada",
|
||||
"collections.search_accounts_max_reached": "Oled lisanud maksimumarv kontosid",
|
||||
"collections.search_accounts_max_reached": "Oled lisanud maksimumarvu kontosid",
|
||||
"collections.sensitive": "Tundlik",
|
||||
"collections.share_short": "Jaga",
|
||||
"collections.sort_alphabetical": "Tähestikuline",
|
||||
@@ -462,17 +464,17 @@
|
||||
"column.list_members": "Halda loendi liikmeid",
|
||||
"column.lists": "Loetelud",
|
||||
"column.mutes": "Summutatud kasutajad",
|
||||
"column.notifications": "Teated",
|
||||
"column.notifications": "Teavitused",
|
||||
"column.other_collections": "Kasutaja {name} kogumikud",
|
||||
"column.pins": "Esiletõstetud postitused",
|
||||
"column.public": "Föderatiivne ajajoon",
|
||||
"column.public": "Födereeritud ajajoon",
|
||||
"column.your_collections": "Sinu kogumikud",
|
||||
"column_back_button.label": "Tagasi",
|
||||
"column_header.hide_settings": "Peida sätted",
|
||||
"column_header.hide_settings": "Peida seadistused",
|
||||
"column_header.moveLeft_settings": "Liiguta tulp vasakule",
|
||||
"column_header.moveRight_settings": "Liiguta tulp paremale",
|
||||
"column_header.pin": "Kinnita",
|
||||
"column_header.show_settings": "Näita sätteid",
|
||||
"column_header.show_settings": "Näita seadistusi",
|
||||
"column_header.unpin": "Eemalda kinnitus",
|
||||
"column_search.cancel": "Tühista",
|
||||
"combobox.close_results": "Sulge tulemused",
|
||||
@@ -532,7 +534,7 @@
|
||||
"confirmations.follow_to_list.message": "Pead jälgima kasutajat {name}, et lisada teda loetellu.",
|
||||
"confirmations.follow_to_list.title": "Jälgida kasutajat?",
|
||||
"confirmations.hide_featured_tab.confirm": "Peida vahekaart",
|
||||
"confirmations.hide_featured_tab.intro": "Saad seda igal ajal muuta menüüs <i>Profiili muutmine > Profiili vahekaardi seaded</i>.",
|
||||
"confirmations.hide_featured_tab.intro": "Saad seda igal ajal muuta menüüs <i>Profiili muutmine > Profiili vahekaardi seadistused</i>.",
|
||||
"confirmations.hide_featured_tab.message": "See peidab vahekaardi serveri {serverName} kasutajatel ja teistel serveritel, kus kasutatakse Mastodoni uusimat versiooni. Muudel serveritel võib kuvamine erineda.",
|
||||
"confirmations.hide_featured_tab.title": "Kas peidame vahekaardi „Esiletõstetud“?",
|
||||
"confirmations.logout.confirm": "Välju",
|
||||
@@ -586,7 +588,7 @@
|
||||
"directory.local": "Ainult domeenilt {domain}",
|
||||
"directory.new_arrivals": "Uustulijad",
|
||||
"directory.recently_active": "Hiljuti aktiivne",
|
||||
"disabled_account_banner.account_settings": "Kontosätted",
|
||||
"disabled_account_banner.account_settings": "Kasutajakonto seadistused",
|
||||
"disabled_account_banner.text": "Su konto {disabledAccount} on hetkel keelatud.",
|
||||
"dismissable_banner.community_timeline": "Need on kõige viimased avalikud postitused inimestelt, kelle kontosid majutab {domain}.",
|
||||
"dismissable_banner.dismiss": "Sulge",
|
||||
@@ -641,6 +643,7 @@
|
||||
"empty_column.blocks": "Blokeeritud kasutajaid pole.",
|
||||
"empty_column.bookmarked_statuses": "Järjehoidjatesse pole veel lisatud postitusi. Kui lisad mõne, näed neid siin.",
|
||||
"empty_column.collections.featured_in": "Sind pole lisatud veel ühtegi kogumikku.",
|
||||
"empty_column.collections.featured_in_undiscoverable": "Selleks, et kasutajad saaks sind lisada kogumikesse, pead sa seadistustest lubama tuvastamise ja esiletõstmise siit: <link>Eelistused > Privaatsus ja ulatus</link>",
|
||||
"empty_column.community": "Kohalik ajajoon on tühi. Kirjuta midagi avalikult, et pall veerema ajada!",
|
||||
"empty_column.direct": "Sul pole veel ühtegi privaatset mainimist. Kui saadad või saad mõne, ilmuvad need siin.",
|
||||
"empty_column.disabled_feed": "See infovoog on serveri peakasutajate poolt välja lülitatud.",
|
||||
@@ -683,7 +686,7 @@
|
||||
"filter_modal.added.expired_explanation": "Selle filtri kategooria on aegunud. pead muutma aegumiskuupäeva, kui tahad, et filter kehtiks.",
|
||||
"filter_modal.added.expired_title": "Aegunud filter!",
|
||||
"filter_modal.added.review_and_configure": "Et vaadata üle ja täpsemalt seadistada seda filtrikategooriat, mine lehele {settings_link}.",
|
||||
"filter_modal.added.review_and_configure_title": "Filtrite sätted",
|
||||
"filter_modal.added.review_and_configure_title": "Filtrite seadistused",
|
||||
"filter_modal.added.settings_link": "sätete leht",
|
||||
"filter_modal.added.short_explanation": "See postitus on lisatud järgmisesse filtrikategooriasse: {title}.",
|
||||
"filter_modal.added.title": "Filter lisatud!",
|
||||
@@ -743,7 +746,7 @@
|
||||
"hashtag.column_header.tag_mode.all": "ja {additional}",
|
||||
"hashtag.column_header.tag_mode.any": "või teemaviide {additional}",
|
||||
"hashtag.column_header.tag_mode.none": "ilma teemaviiteta {additional}",
|
||||
"hashtag.column_settings.select.no_options_message": "Soovitusi ei leitud",
|
||||
"hashtag.column_settings.select.no_options_message": "Soovitusi ei leidu",
|
||||
"hashtag.column_settings.select.placeholder": "Sisesta teemaviited…",
|
||||
"hashtag.column_settings.tag_mode.all": "Kõik need",
|
||||
"hashtag.column_settings.tag_mode.any": "Mõni neist",
|
||||
@@ -989,23 +992,23 @@
|
||||
"notifications.clear_title": "Tühjenda teavitus?",
|
||||
"notifications.column_settings.admin.report": "Uued teavitused:",
|
||||
"notifications.column_settings.admin.sign_up": "Uued kasutajad:",
|
||||
"notifications.column_settings.alert": "Töölauateated",
|
||||
"notifications.column_settings.alert": "Teavitused töölaual",
|
||||
"notifications.column_settings.favourite": "Lemmikud:",
|
||||
"notifications.column_settings.filter_bar.advanced": "Näita kõiki kategooriaid",
|
||||
"notifications.column_settings.filter_bar.category": "Kiirfiltri riba",
|
||||
"notifications.column_settings.follow": "Uued jälgijad:",
|
||||
"notifications.column_settings.follow_request": "Uued jälgimistaotlused:",
|
||||
"notifications.column_settings.group": "Grupp",
|
||||
"notifications.column_settings.group": "Grupeeri",
|
||||
"notifications.column_settings.mention": "Mainimised:",
|
||||
"notifications.column_settings.poll": "Küsitluse tulemused:",
|
||||
"notifications.column_settings.push": "Push teated",
|
||||
"notifications.column_settings.push": "Tõuketeavitused",
|
||||
"notifications.column_settings.quote": "Tsitaadid:",
|
||||
"notifications.column_settings.reblog": "Jagamised:",
|
||||
"notifications.column_settings.show": "Kuva tulbas",
|
||||
"notifications.column_settings.sound": "Mängi heli",
|
||||
"notifications.column_settings.sound": "Esita heli",
|
||||
"notifications.column_settings.status": "Uued postitused:",
|
||||
"notifications.column_settings.unread_notifications.category": "Lugemata teated",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Tõsta esile lugemata teated",
|
||||
"notifications.column_settings.unread_notifications.category": "Lugemata teavitused",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Tõsta lugemata teavitused esile",
|
||||
"notifications.column_settings.update": "Muudatused:",
|
||||
"notifications.filter.all": "Kõik",
|
||||
"notifications.filter.boosts": "Jagamised",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Tilin yksityisyystilaksi on määritetty lukittu. Tilin omistaja arvioi erikseen, kuka voi seurata häntä.",
|
||||
"account.media": "Media",
|
||||
"account.mention": "Mainitse @{name}",
|
||||
"account.menu.add_to_collection": "Lisää kokoelmaan…",
|
||||
"account.menu.add_to_list": "Lisää listaan…",
|
||||
"account.menu.block": "Estä tili",
|
||||
"account.menu.block_domain": "Estä {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Lisää enintään {count} tiliä",
|
||||
"collections.accounts.empty_editor_title": "Kukaan ei ole vielä tässä kokoelmassa",
|
||||
"collections.accounts.empty_title": "Tämä kokoelma on tyhjä",
|
||||
"collections.add_to_collection": "Lisää {name} kokoelmiin",
|
||||
"collections.block_collection_owner": "Estä tili",
|
||||
"collections.by_account": "koonnut {account_handle}",
|
||||
"collections.collection_description": "Kuvaus",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Le statut de confidentialité de ce compte est privé. Son propriétaire vérifie manuellement qui peut le/la suivre.",
|
||||
"account.media": "Média",
|
||||
"account.mention": "Mentionner @{name}",
|
||||
"account.menu.add_to_collection": "Ajouter à une collection…",
|
||||
"account.menu.add_to_list": "Ajouter à la liste…",
|
||||
"account.menu.block": "Bloquer le compte",
|
||||
"account.menu.block_domain": "Bloquer {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Ajoutez jusqu'à {count} comptes",
|
||||
"collections.accounts.empty_editor_title": "Il n'y a personne dans cette collection",
|
||||
"collections.accounts.empty_title": "Cette collection est vide",
|
||||
"collections.add_to_collection": "Ajouter {name} aux collections",
|
||||
"collections.block_collection_owner": "Bloquer le compte",
|
||||
"collections.by_account": "par {account_handle}",
|
||||
"collections.collection_description": "Description",
|
||||
@@ -394,6 +396,7 @@
|
||||
"collections.detail.loading": "Chargement de la collection…",
|
||||
"collections.detail.revoke_inclusion": "Me retirer",
|
||||
"collections.detail.sensitive_content": "Contenu sensible",
|
||||
"collections.detail.sensitive_note": "La description et les comptes peuvent ne pas convenir à tous les publics.",
|
||||
"collections.detail.share": "Partager la collection",
|
||||
"collections.detail.you_are_in_this_collection": "Vous faites partie de cette collection",
|
||||
"collections.edit_details": "Modifier les détails",
|
||||
@@ -424,6 +427,11 @@
|
||||
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
|
||||
"collections.sensitive": "Sensible",
|
||||
"collections.share_short": "Partager",
|
||||
"collections.sort_alphabetical": "Alphabétique",
|
||||
"collections.sort_by": "Trier par :",
|
||||
"collections.sort_date_added": "Date d'ajout",
|
||||
"collections.sort_last_active": "Dernière activité",
|
||||
"collections.sort_most_followers": "Nombre d'abonné·e·s",
|
||||
"collections.suggestions.can_not_add": "Ne peut pas être ajouté",
|
||||
"collections.suggestions.can_not_add_desc": "Ces comptes peuvent avoir choisi de ne pas être découverts, ou ils peuvent être sur un serveur qui ne supporte pas les collections.",
|
||||
"collections.suggestions.must_follow": "Vous devez d'abord suivre",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Ce compte est privé. Son ou sa propriétaire approuve manuellement qui peut le suivre.",
|
||||
"account.media": "Médias",
|
||||
"account.mention": "Mentionner @{name}",
|
||||
"account.menu.add_to_collection": "Ajouter à une collection…",
|
||||
"account.menu.add_to_list": "Ajouter à la liste…",
|
||||
"account.menu.block": "Bloquer le compte",
|
||||
"account.menu.block_domain": "Bloquer {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Ajoutez jusqu'à {count} comptes",
|
||||
"collections.accounts.empty_editor_title": "Il n'y a personne dans cette collection",
|
||||
"collections.accounts.empty_title": "Cette collection est vide",
|
||||
"collections.add_to_collection": "Ajouter {name} aux collections",
|
||||
"collections.block_collection_owner": "Bloquer le compte",
|
||||
"collections.by_account": "par {account_handle}",
|
||||
"collections.collection_description": "Description",
|
||||
@@ -394,6 +396,7 @@
|
||||
"collections.detail.loading": "Chargement de la collection…",
|
||||
"collections.detail.revoke_inclusion": "Me retirer",
|
||||
"collections.detail.sensitive_content": "Contenu sensible",
|
||||
"collections.detail.sensitive_note": "La description et les comptes peuvent ne pas convenir à tous les publics.",
|
||||
"collections.detail.share": "Partager la collection",
|
||||
"collections.detail.you_are_in_this_collection": "Vous faites partie de cette collection",
|
||||
"collections.edit_details": "Modifier les détails",
|
||||
@@ -424,6 +427,11 @@
|
||||
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
|
||||
"collections.sensitive": "Sensible",
|
||||
"collections.share_short": "Partager",
|
||||
"collections.sort_alphabetical": "Alphabétique",
|
||||
"collections.sort_by": "Trier par :",
|
||||
"collections.sort_date_added": "Date d'ajout",
|
||||
"collections.sort_last_active": "Dernière activité",
|
||||
"collections.sort_most_followers": "Nombre d'abonné·e·s",
|
||||
"collections.suggestions.can_not_add": "Ne peut pas être ajouté",
|
||||
"collections.suggestions.can_not_add_desc": "Ces comptes peuvent avoir choisi de ne pas être découverts, ou ils peuvent être sur un serveur qui ne supporte pas les collections.",
|
||||
"collections.suggestions.must_follow": "Vous devez d'abord suivre",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Esta é unha conta privada. A propietaria revisa de xeito manual quen pode seguila.",
|
||||
"account.media": "Multimedia",
|
||||
"account.mention": "Mencionar @{name}",
|
||||
"account.menu.add_to_collection": "Engadir á coleción…",
|
||||
"account.menu.add_to_list": "Engadir á lista…",
|
||||
"account.menu.block": "Bloquear conta",
|
||||
"account.menu.block_domain": "Bloquear a {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Engade ate {count} contas",
|
||||
"collections.accounts.empty_editor_title": "Aínda non hai ninguén nesta colección",
|
||||
"collections.accounts.empty_title": "A colección está baleira",
|
||||
"collections.add_to_collection": "Engadir a {name} ás coleccións",
|
||||
"collections.block_collection_owner": "Bloquear conta",
|
||||
"collections.by_account": "de {account_handle}",
|
||||
"collections.collection_description": "Descrición",
|
||||
@@ -425,6 +427,11 @@
|
||||
"collections.search_accounts_max_reached": "Acadaches o máximo de contas permitidas",
|
||||
"collections.sensitive": "Sensible",
|
||||
"collections.share_short": "Compartir",
|
||||
"collections.sort_alphabetical": "Alfabética",
|
||||
"collections.sort_by": "Orde por:",
|
||||
"collections.sort_date_added": "Data de alta",
|
||||
"collections.sort_last_active": "Última actividade",
|
||||
"collections.sort_most_followers": "Con máis seguidoras",
|
||||
"collections.suggestions.can_not_add": "Non se pode engadir",
|
||||
"collections.suggestions.can_not_add_desc": "Estas contas optaron por poder ser engadidas, ou pode que estean nun servidor que aínda non é compatible coas coleccións.",
|
||||
"collections.suggestions.must_follow": "Primeiro tes que seguila",
|
||||
|
||||
@@ -394,6 +394,7 @@
|
||||
"collections.detail.loading": "טוען אוסף…",
|
||||
"collections.detail.revoke_inclusion": "הסירוני",
|
||||
"collections.detail.sensitive_content": "תוכן רגיש",
|
||||
"collections.detail.sensitive_note": "התיאור והחשבונות עשויים שלא להתאים לכל הצופיםות.",
|
||||
"collections.detail.share": "שיתוף אוסף",
|
||||
"collections.detail.you_are_in_this_collection": "אתם מופיעים באוסף זה",
|
||||
"collections.edit_details": "עריכת פרטים",
|
||||
@@ -424,6 +425,11 @@
|
||||
"collections.search_accounts_max_reached": "הגעת למספר החשבונות המירבי",
|
||||
"collections.sensitive": "רגיש",
|
||||
"collections.share_short": "שיתוף",
|
||||
"collections.sort_alphabetical": "בסדר האלפבית",
|
||||
"collections.sort_by": "מיין לפי:",
|
||||
"collections.sort_date_added": "תאריך הוספה",
|
||||
"collections.sort_last_active": "פעילות אחרונה",
|
||||
"collections.sort_most_followers": "מירב העוקבים",
|
||||
"collections.suggestions.can_not_add": "לא ניתן להוסיף",
|
||||
"collections.suggestions.can_not_add_desc": "חשבונות אלו כנראה ביקשו לא להכלל ב\"תגליות\" או שהם משתמשים בשרת שאינו תומך באוספים.",
|
||||
"collections.suggestions.must_follow": "יש לעקוב תחילה",
|
||||
@@ -635,6 +641,7 @@
|
||||
"empty_column.blocks": "עדיין לא חסמתם משתמשים אחרים.",
|
||||
"empty_column.bookmarked_statuses": "אין עדיין הודעות שחיבבת. כשתחבב את הראשונה, היא תופיע כאן.",
|
||||
"empty_column.collections.featured_in": "עוד לא הוסיפו אותך לאף אוסף.",
|
||||
"empty_column.collections.featured_in_undiscoverable": "כדי שאחרים יוכלו להוסיפך לאוספים, עליך לאפשר להופיע ב\"תגליות\" תחת <link>העדפות > פרטיות ומידת חשיפה</link>",
|
||||
"empty_column.community": "פיד השרת המקומי ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",
|
||||
"empty_column.direct": "אין לך שום הודעות פרטיות עדיין. כשתשלחו או תקבלו אחת, היא תופיע כאן.",
|
||||
"empty_column.disabled_feed": "פיד זה נחסם לשימוש על ידי מנהלי השרת שלך.",
|
||||
|
||||
@@ -394,6 +394,7 @@
|
||||
"collections.detail.loading": "Gyűjtemény betöltése…",
|
||||
"collections.detail.revoke_inclusion": "Saját magam eltávolítása",
|
||||
"collections.detail.sensitive_content": "Kényes tartalom",
|
||||
"collections.detail.sensitive_note": "A leírás és a fiókok lehet, hogy nem minden megtekintő számára megfelelőek.",
|
||||
"collections.detail.share": "Gyűjtemény megosztása",
|
||||
"collections.detail.you_are_in_this_collection": "Kiemeltek téged ebben a gyűjteményhez",
|
||||
"collections.edit_details": "Részletek szerkesztése",
|
||||
@@ -424,6 +425,11 @@
|
||||
"collections.search_accounts_max_reached": "Elérte a hozzáadott fiókok maximális számát",
|
||||
"collections.sensitive": "Érzékeny",
|
||||
"collections.share_short": "Megosztás",
|
||||
"collections.sort_alphabetical": "Betűrendben",
|
||||
"collections.sort_by": "Rendezés:",
|
||||
"collections.sort_date_added": "Hozzáadás dátuma",
|
||||
"collections.sort_last_active": "Utoljára aktív",
|
||||
"collections.sort_most_followers": "Legtöbb követő",
|
||||
"collections.suggestions.can_not_add": "Nem adható hozzá",
|
||||
"collections.suggestions.can_not_add_desc": "Ezek a fiókok lehet, hogy a felfedezés kikapcsolását kérték, vagy olyan kiszolgálón lehetnek, mely nem támogatja a gyűjteményeket.",
|
||||
"collections.suggestions.must_follow": "Először követni kell",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Lo stato della privacy di questo profilo è impostato a bloccato. Il proprietario revisiona manualmente chi può seguirlo.",
|
||||
"account.media": "Media",
|
||||
"account.mention": "Menziona @{name}",
|
||||
"account.menu.add_to_collection": "Aggiungi alla collezione…",
|
||||
"account.menu.add_to_list": "Aggiungi alla lista…",
|
||||
"account.menu.block": "Blocca l'account",
|
||||
"account.menu.block_domain": "Blocca {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Aggiungi fino a {count} account",
|
||||
"collections.accounts.empty_editor_title": "Nessuno è ancora in questa collezione",
|
||||
"collections.accounts.empty_title": "Questa collezione è vuota",
|
||||
"collections.add_to_collection": "Aggiungi {name} alle collezioni",
|
||||
"collections.block_collection_owner": "Blocca l'account",
|
||||
"collections.by_account": "di {account_handle}",
|
||||
"collections.collection_description": "Descrizione",
|
||||
@@ -425,6 +427,11 @@
|
||||
"collections.search_accounts_max_reached": "Hai aggiunto il numero massimo di account",
|
||||
"collections.sensitive": "Sensibile",
|
||||
"collections.share_short": "Condividi",
|
||||
"collections.sort_alphabetical": "Ordine alfabetico",
|
||||
"collections.sort_by": "Ordina per:",
|
||||
"collections.sort_date_added": "Data aggiunta",
|
||||
"collections.sort_last_active": "Ultima attività",
|
||||
"collections.sort_most_followers": "Più follower",
|
||||
"collections.suggestions.can_not_add": "Non può essere aggiunto",
|
||||
"collections.suggestions.can_not_add_desc": "Questi account potrebbero aver scelto di non essere scoperti, oppure potrebbero trovarsi su un server che non supporta le collezioni.",
|
||||
"collections.suggestions.must_follow": "Devi prima seguire",
|
||||
@@ -762,7 +769,7 @@
|
||||
"hints.profiles.see_more_posts": "Vedi altri post su {domain}",
|
||||
"home.column_settings.show_quotes": "Mostra le citazioni",
|
||||
"home.column_settings.show_reblogs": "Mostra le condivisioni",
|
||||
"home.column_settings.show_replies": "Mostra risposte",
|
||||
"home.column_settings.show_replies": "Mostra le risposte",
|
||||
"home.hide_announcements": "Nascondi annunci",
|
||||
"home.pending_critical_update.body": "Ti preghiamo di aggiornare il tuo server di Mastodon, il prima possibile!",
|
||||
"home.pending_critical_update.link": "Visualizza aggiornamenti",
|
||||
@@ -935,7 +942,7 @@
|
||||
"notification.follow_request.name_and_others": "{name} e {count, plural, one {# altro} other {altri #}} hanno richiesto di seguirti",
|
||||
"notification.label.mention": "Menziona",
|
||||
"notification.label.private_mention": "Menzione privata",
|
||||
"notification.label.private_reply": "Rispondi in privato",
|
||||
"notification.label.private_reply": "Risposta in privato",
|
||||
"notification.label.quote": "{name} ha citato il tuo post",
|
||||
"notification.label.reply": "Rispondi",
|
||||
"notification.mention": "Menziona",
|
||||
@@ -952,7 +959,7 @@
|
||||
"notification.own_poll": "Il tuo sondaggio è terminato",
|
||||
"notification.poll": "Un sondaggio in cui hai votato è terminato",
|
||||
"notification.quoted_update": "{name} ha modificato un post che hai citato",
|
||||
"notification.reblog": "{name} ha rebloggato il tuo post",
|
||||
"notification.reblog": "{name} ha condiviso il tuo post",
|
||||
"notification.reblog.name_and_others_with_link": "{name} e <a>{count, plural, one {# altro} other {altri #}}</a> hanno condiviso il tuo post",
|
||||
"notification.relationships_severance_event": "Connessioni perse con {name}",
|
||||
"notification.relationships_severance_event.account_suspension": "Un amministratore da {from} ha sospeso {target}, il che significa che non puoi più ricevere aggiornamenti da loro o interagire con loro.",
|
||||
@@ -1267,7 +1274,7 @@
|
||||
"status.reblog_or_quote": "Condividi o cita",
|
||||
"status.reblog_private": "Condividi di nuovo con i tuoi follower",
|
||||
"status.reblogged_by": "{name} ha condiviso",
|
||||
"status.reblogs.empty": "Ancora nessuno ha rebloggato questo post. Quando qualcuno lo farà, apparirà qui.",
|
||||
"status.reblogs.empty": "Nessuno ha ancora condiviso questo post. Quando qualcuno lo farà, comparirà qui.",
|
||||
"status.reblogs_count": "{count, plural, one {{counter} condivisione} other {{counter} condivisioni}}",
|
||||
"status.redraft": "Elimina e riscrivi",
|
||||
"status.remove_bookmark": "Rimuovi segnalibro",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "De privacystatus van dit account is ingesteld op vergrendeld. De eigenaar beoordeelt handmatig wie diegene kan volgen.",
|
||||
"account.media": "Media",
|
||||
"account.mention": "@{name} vermelden",
|
||||
"account.menu.add_to_collection": "Aan verzameling toevoegen…",
|
||||
"account.menu.add_to_list": "Aan lijst toevoegen…",
|
||||
"account.menu.block": "Account blokkeren",
|
||||
"account.menu.block_domain": "{domain} blokkeren",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Tot {count} accounts toevoegen",
|
||||
"collections.accounts.empty_editor_title": "Er is nog nog niemand in deze verzameling",
|
||||
"collections.accounts.empty_title": "Deze verzameling is leeg",
|
||||
"collections.add_to_collection": "Voeg {name} aan verzamelingen toe",
|
||||
"collections.block_collection_owner": "Account blokkeren",
|
||||
"collections.by_account": "door {account_handle}",
|
||||
"collections.collection_description": "Omschrijving",
|
||||
@@ -425,6 +427,11 @@
|
||||
"collections.search_accounts_max_reached": "Je hebt het maximum aantal accounts toegevoegd",
|
||||
"collections.sensitive": "Gevoelig",
|
||||
"collections.share_short": "Delen",
|
||||
"collections.sort_alphabetical": "Alfabetisch",
|
||||
"collections.sort_by": "Sortering:",
|
||||
"collections.sort_date_added": "Datum toegevoegd",
|
||||
"collections.sort_last_active": "Laatst actief",
|
||||
"collections.sort_most_followers": "Meeste volgers",
|
||||
"collections.suggestions.can_not_add": "Kan niet worden toegevoegd",
|
||||
"collections.suggestions.can_not_add_desc": "Deze accounts willen mogelijk niet worden uitgelicht, of ze bevinden zich op een server die geen verzamelingen ondersteunt.",
|
||||
"collections.suggestions.must_follow": "Moeten eerst gevolgd worden",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"account.featured.accounts": "Perfis",
|
||||
"account.featured.collections": "Coleções",
|
||||
"account.featured.new_collection": "Nova coleção",
|
||||
"account.field_overflow": "Exibir conteúdo inteiro",
|
||||
"account.field_overflow": "Exibir conteúdo completo",
|
||||
"account.filters.all": "Toda atividade",
|
||||
"account.filters.boosts_toggle": "Exibir impulsos",
|
||||
"account.filters.posts_boosts": "Publicações e impulsos",
|
||||
@@ -70,13 +70,13 @@
|
||||
"account.hide_reblogs": "Ocultar impulsos de @{name}",
|
||||
"account.in_memoriam": "In Memoriam.",
|
||||
"account.join_modal.day": "Dia",
|
||||
"account.join_modal.me": "Você juntou-se à {server} em",
|
||||
"account.join_modal.me_anniversary": "Feliz Fediversário! Você juntou-se à {server} em",
|
||||
"account.join_modal.me": "Você está em {server} desde",
|
||||
"account.join_modal.me_anniversary": "Feliz Fediversário! Você está em {server} desde",
|
||||
"account.join_modal.me_today": "É seu primeiro dia em {server}!",
|
||||
"account.join_modal.other": "{name} juntou-se à {server} em",
|
||||
"account.join_modal.other": "{name} está em {server} desde",
|
||||
"account.join_modal.other_today": "Este é o primeiro dia de {name} em {server}!",
|
||||
"account.join_modal.share.celebrate": "Compartilhar publicação comemorativa",
|
||||
"account.join_modal.share.intro": "Compartilhar publicação de introdução",
|
||||
"account.join_modal.share.intro": "Compartilhar publicação de apresentação",
|
||||
"account.join_modal.share.welcome": "Compartilhar publicação de boas-vindas",
|
||||
"account.join_modal.years": "{number, plural, one {ano} other {anos}}",
|
||||
"account.joined_short": "Entrou",
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Trancado. Seguir requer aprovação manual do perfil.",
|
||||
"account.media": "Mídia",
|
||||
"account.mention": "Mencionar @{name}",
|
||||
"account.menu.add_to_collection": "Adicionar à coleção…",
|
||||
"account.menu.add_to_list": "Adicionar à lista…",
|
||||
"account.menu.block": "Bloquear conta",
|
||||
"account.menu.block_domain": "Bloquear {domain}",
|
||||
@@ -103,7 +104,7 @@
|
||||
"account.menu.show_reblogs": "Exibir impulsos na timeline",
|
||||
"account.menu.unblock": "Desbloquear conta",
|
||||
"account.menu.unblock_domain": "Desbloquear {domain}",
|
||||
"account.menu.unmute": "Deixar de silenciar a conta",
|
||||
"account.menu.unmute": "Dessilenciar conta",
|
||||
"account.moved_to": "{name} indicou que sua nova conta agora é:",
|
||||
"account.mute": "Silenciar @{name}",
|
||||
"account.mute_notifications_short": "Silenciar notificações",
|
||||
@@ -111,22 +112,22 @@
|
||||
"account.muted": "Silenciado",
|
||||
"account.mutual": "Vocês se seguem",
|
||||
"account.name.copy": "Copiar usuário",
|
||||
"account.name.help.domain": "{domain} é o servidor que hospeda o perfil e publicações do usuário.",
|
||||
"account.name.help.domain": "{domain} é o servidor que hospeda este perfil e suas publicações.",
|
||||
"account.name.help.domain_self": "{domain} é o servidor que hospeda seu perfil e suas publicações.",
|
||||
"account.name.help.footer": "Assim como você pode enviar e-mails para outras pessoas usando endereços de e-mail diferentes, você pode interagir com usuários em outros servidores do Mastodon, e com qualquer um dos outros aplicativos do Fediverso.",
|
||||
"account.name.help.header": "Um nome de usuário é como um e-mail",
|
||||
"account.name.help.username": "{username} é o usuário desta conta em seu próprio servidor. Alguém do mesmo servidor talvez possua o mesmo usuário.",
|
||||
"account.name.help.username_self": "{username} é seu usuário neste servidor. Alguém em outro servidor talvez possua o mesmo usuário.",
|
||||
"account.name.help.footer": "Assim como pode enviar e-mails para pessoas usando diferentes provedores de e-mail, você pode interagir com pessoas em outros servidor do Mastodon, e com qualquer um em outros apps da Fediverse.",
|
||||
"account.name.help.header": "Um nome de usuário é como um endereço de email",
|
||||
"account.name.help.username": "{username} é o nome de usuário desta conta no servidor dela. Alguém em outro servidor pode ter o mesmo nome de usuário.",
|
||||
"account.name.help.username_self": "{username} é seu nome de usuário neste servidor. Alguém em outro servidor pode ter o mesmo nome de usuário.",
|
||||
"account.name_info": "O que isso quer dizer?",
|
||||
"account.no_bio": "Nenhuma descrição fornecida.",
|
||||
"account.node_modal.callout": "Notas pessoais são visíveis somente para você.",
|
||||
"account.node_modal.callout": "Notas pessoais são visíveis apenas para você.",
|
||||
"account.node_modal.edit_title": "Editar nota pessoal",
|
||||
"account.node_modal.error_unknown": "Não foi possível salvar nota",
|
||||
"account.node_modal.error_unknown": "Não foi possível salvar a nota",
|
||||
"account.node_modal.field_label": "Nota Pessoal",
|
||||
"account.node_modal.save": "Salvar",
|
||||
"account.node_modal.title": "Adicionar nota pessoal",
|
||||
"account.note.edit_button": "Editar",
|
||||
"account.note.title": "Nota pessoal (visível só para você)",
|
||||
"account.note.title": "Nota pessoal (visível apenas para você)",
|
||||
"account.open_original_page": "Abrir a página original",
|
||||
"account.pending": "Pendente",
|
||||
"account.posts": "Publicações",
|
||||
@@ -137,7 +138,7 @@
|
||||
"account.show_reblogs": "Mostrar impulsos de @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
|
||||
"account.timeline.pinned": "Fixado",
|
||||
"account.timeline.pinned.view_all": "Ver publicações fixadas",
|
||||
"account.timeline.pinned.view_all": "Ver todas as publicações fixadas",
|
||||
"account.unblock": "Desbloquear @{name}",
|
||||
"account.unblock_domain": "Desbloquear domínio {domain}",
|
||||
"account.unblock_domain_short": "Desbloquear",
|
||||
@@ -147,50 +148,50 @@
|
||||
"account.unmute": "Dessilenciar @{name}",
|
||||
"account.unmute_notifications_short": "Ativar som de notificações",
|
||||
"account.unmute_short": "Desativar silêncio",
|
||||
"account_edit.advanced_settings.bot_hint": "Alertar a todos que a conta realiza ações automáticas que talvez não sejam monitoradas",
|
||||
"account_edit.advanced_settings.bot_label": "Conta automática",
|
||||
"account_edit.advanced_settings.title": "Opções avançadas",
|
||||
"account_edit.advanced_settings.bot_hint": "Alertar a outros que a conta realiza ações automáticas que talvez não sejam monitoradas",
|
||||
"account_edit.advanced_settings.bot_label": "Conta automatizada",
|
||||
"account_edit.advanced_settings.title": "Configurações avançadas",
|
||||
"account_edit.bio.add_label": "Adicionar biografia",
|
||||
"account_edit.bio.edit_label": "Editar biografia",
|
||||
"account_edit.bio.placeholder": "Escreva uma curta introdução para ajudar os outros a identificarem você.",
|
||||
"account_edit.bio.placeholder": "Insira uma breve apresentação para ajudar os outros a lhe identificarem.",
|
||||
"account_edit.bio.title": "Biografia",
|
||||
"account_edit.bio_modal.add_title": "Adicionar biografia",
|
||||
"account_edit.bio_modal.edit_title": "Editar biografia",
|
||||
"account_edit.column_button": "Concluído",
|
||||
"account_edit.column_title": "Editar perfil",
|
||||
"account_edit.custom_fields.add_label": "Novo espaço",
|
||||
"account_edit.custom_fields.edit_label": "Editar espaço",
|
||||
"account_edit.custom_fields.placeholder": "Adicione seus pronomes, links externos ou qualquer coisa que deseja compartilhar.",
|
||||
"account_edit.custom_fields.reorder_button": "Reordenar espaços",
|
||||
"account_edit.custom_fields.tip_content": "Você pode adicionar credibilidade facilmente a sua conta ao verificar qualquer link para seus sites.",
|
||||
"account_edit.custom_fields.tip_title": "Dica: Adicionando links verificados",
|
||||
"account_edit.custom_fields.title": "Espaços personalizados",
|
||||
"account_edit.custom_fields.verified_hint": "Como adicionar um link verificado?",
|
||||
"account_edit.display_name.add_label": "Adicionar nome de exibição",
|
||||
"account_edit.custom_fields.add_label": "Acrescentar campo",
|
||||
"account_edit.custom_fields.edit_label": "Editar campo",
|
||||
"account_edit.custom_fields.placeholder": "Insira seus pronomes, links externos ou qualquer coisa que queira compartilhar.",
|
||||
"account_edit.custom_fields.reorder_button": "Reordenar campos",
|
||||
"account_edit.custom_fields.tip_content": "Você pode facilmente dar credibilidade à sua conta Mastodon verificando os links para os seus sites.",
|
||||
"account_edit.custom_fields.tip_title": "Dica: Adicionar links verificados",
|
||||
"account_edit.custom_fields.title": "Campos personalizados",
|
||||
"account_edit.custom_fields.verified_hint": "Como adiciono um link verificado?",
|
||||
"account_edit.display_name.add_label": "Acrescentar nome de exibição",
|
||||
"account_edit.display_name.edit_label": "Editar nome de exibição",
|
||||
"account_edit.display_name.placeholder": "O nome de exibição é como seu nome aparece em seu perfil e em timelines.",
|
||||
"account_edit.display_name.placeholder": "Seu nome de exibição é a forma com que seu nome aparece em seu perfil e em suas linhas do tempo.",
|
||||
"account_edit.display_name.title": "Nome de exibição",
|
||||
"account_edit.featured_hashtags.edit_label": "Adicionar tags",
|
||||
"account_edit.featured_hashtags.placeholder": "Ajude todos a identificarem, e acessarem rapidamente seus tópicos favoritos.",
|
||||
"account_edit.featured_hashtags.title": "Tags em destaque",
|
||||
"account_edit.field_actions.delete": "Remover espaço",
|
||||
"account_edit.field_actions.edit": "Editar espaço",
|
||||
"account_edit.field_delete_modal.confirm": "Deseja mesmo excluir este espaço? Esta ação não pode ser desfeita.",
|
||||
"account_edit.featured_hashtags.edit_label": "Acrescentar hashtags",
|
||||
"account_edit.featured_hashtags.placeholder": "Ajude outros a identificar e ter acesso rápido a seus tópicos favoritos.",
|
||||
"account_edit.featured_hashtags.title": "Hashtags em destaque",
|
||||
"account_edit.field_actions.delete": "Remover campo",
|
||||
"account_edit.field_actions.edit": "Editar campo",
|
||||
"account_edit.field_delete_modal.confirm": "Tem certeza que deseja excluir este campo personalizado? Esta ação não pode ser desfeita.",
|
||||
"account_edit.field_delete_modal.delete_button": "Excluir",
|
||||
"account_edit.field_delete_modal.title": "Remover espaço?",
|
||||
"account_edit.field_edit_modal.add_title": "Adicionar espaço",
|
||||
"account_edit.field_edit_modal.discard_confirm": "Descartar",
|
||||
"account_edit.field_edit_modal.discard_message": "Você possui alterações não salvas. Deseja mesmo descartá-las?",
|
||||
"account_edit.field_edit_modal.edit_title": "Editar campo personalizado",
|
||||
"account_edit.field_edit_modal.length_warning": "Limite de caracteres recomendados ultrapassado. Usuários em telefones podem não conseguir ver o espaço completo.",
|
||||
"account_edit.field_edit_modal.link_emoji_warning": "Recomendamos não utilizar emojis personalizados combinados com URLs. Campos personalizados contendo ambos serão exibidos apenas como texto em vez de link, para evitar confusão dos usuários.",
|
||||
"account_edit.field_edit_modal.name_hint": "Ex. \"Site pessoal\"",
|
||||
"account_edit.field_edit_modal.name_label": "Descrição",
|
||||
"account_edit.field_edit_modal.url_warning": "Para adicionar um link, inclua {protocol} no início.",
|
||||
"account_edit.field_edit_modal.edit_title": "Editar espaço",
|
||||
"account_edit.field_edit_modal.length_warning": "Limite de caracteres excedido. Usuários móveis talvez não consigam ver seus espaços por inteiro.",
|
||||
"account_edit.field_edit_modal.link_emoji_warning": "Recomendamos não usar emojis personalizados com URLs. Espaços contendo ambos serão exibidos apenas como um texto invés de um link para evitar confusão.",
|
||||
"account_edit.field_edit_modal.name_hint": "p. e.x.: “Site pessoal”",
|
||||
"account_edit.field_edit_modal.name_label": "Rótulo",
|
||||
"account_edit.field_edit_modal.url_warning": "Para adicionar links, inclua {protocol} no início.",
|
||||
"account_edit.field_edit_modal.value_hint": "Ex.: \"https://example.me\"",
|
||||
"account_edit.field_edit_modal.value_label": "Valor",
|
||||
"account_edit.field_reorder_modal.drag_cancel": "O arrasto foi cancelado. O campo \"{item}\" foi descartado.",
|
||||
"account_edit.field_reorder_modal.drag_end": "O campo \"{item}\" foi descartado.",
|
||||
"account_edit.field_reorder_modal.drag_cancel": "Arraste cancelado. O campo \"{item}\" foi solto.",
|
||||
"account_edit.field_reorder_modal.drag_end": "O campo \"{item}\" foi solto.",
|
||||
"account_edit.field_reorder_modal.drag_instructions": "Para reordenar campos personalizados, pressione espaço ou enter. Enquanto arrasta, utilize as teclas de seta para mover o campo para cima ou para baixo. Pressione espaço ou enter novamente para colocar o campo em sua nova posição, ou pressione 'Esc' para cancelar.",
|
||||
"account_edit.field_reorder_modal.drag_move": "Campo \"{item}\" foi movido.",
|
||||
"account_edit.field_reorder_modal.drag_over": "Campo \"{item}\" foi movido para \"{over}\".",
|
||||
@@ -201,62 +202,62 @@
|
||||
"account_edit.image_alt_modal.details_content": "FAÇA: <ul><li> Descreva-se como retratado/a</li><li>Utilize linguagem em terceira pessoa (p. ex.: \"Alex\" em vez de \"eu\")</li><li>Seja sucinto/a — algumas palavras costumam ser o suficiente</li></ul> NÃO FAÇA: <ul><li>Comece com \"Foto de\" — é redundante para leitores de tela</li></ul> EXEMPLO:<ul><li>\"Alex vestindo uma camisa verde e óvulos\"</li></ul>",
|
||||
"account_edit.image_alt_modal.details_title": "Dicas: Texto alternativo para fotos",
|
||||
"account_edit.image_alt_modal.edit_title": "Editar texto alternativo",
|
||||
"account_edit.image_alt_modal.text_hint": "Texto alternativo ajuda leitores de tela a entender seu conteúdo.",
|
||||
"account_edit.image_alt_modal.text_hint": "Texto alternativo ajuda susuários de leitores de tela a entender seu conteúdo.",
|
||||
"account_edit.image_alt_modal.text_label": "Texto alternativo",
|
||||
"account_edit.image_delete_modal.confirm": "Tem certeza de que deseja excluir esta imagem? Esta ação não pode ser desfeita.",
|
||||
"account_edit.image_delete_modal.delete_button": "Deletar",
|
||||
"account_edit.image_delete_modal.title": "Deletar imagem?",
|
||||
"account_edit.image_edit.add_button": "Adicionar imagem",
|
||||
"account_edit.image_edit.alt_add_button": "Adicionar texto alternativo",
|
||||
"account_edit.image_delete_modal.delete_button": "Excluir",
|
||||
"account_edit.image_delete_modal.title": "Excluir imagem?",
|
||||
"account_edit.image_edit.add_button": "Inserir imagem",
|
||||
"account_edit.image_edit.alt_add_button": "Inserir texto alternativo",
|
||||
"account_edit.image_edit.alt_edit_button": "Editar texto alternativo",
|
||||
"account_edit.image_edit.remove_button": "Remover imagem",
|
||||
"account_edit.image_edit.replace_button": "Substituir imagem",
|
||||
"account_edit.item_list.delete": "Deletar {name}",
|
||||
"account_edit.item_list.delete": "Excluir {name}",
|
||||
"account_edit.item_list.edit": "Editar {name}",
|
||||
"account_edit.name_modal.add_title": "Inserir nome de exibição",
|
||||
"account_edit.name_modal.edit_title": "Editar nome de exibição",
|
||||
"account_edit.profile_tab.button_label": "Personalizar",
|
||||
"account_edit.profile_tab.hint.description": "Essas configurações definem o que os usuários veem no {server} nos apps oficiais, mas podem não se aplicar a usuários em servidores e apps de terceiros.",
|
||||
"account_edit.profile_tab.hint.title": "Exibições divergem",
|
||||
"account_edit.profile_tab.hint.description": "Essas configurações definem o que os usuários veem no {server} através dos aplicativos oficiais, mas podem não se aplicar a usuários em servidores e aplicativos de terceiros.",
|
||||
"account_edit.profile_tab.hint.title": "A exibição pode variar",
|
||||
"account_edit.profile_tab.show_featured.description": "'Em Destaque' é uma aba opcional onde você pode exibir outras contas.",
|
||||
"account_edit.profile_tab.show_featured.title": "Mostrar aba \"Destaque\"",
|
||||
"account_edit.profile_tab.show_media.description": "\"Mídia\" é uma aba opcional que mostra seus posts, contendo imagens ou vídeos.",
|
||||
"account_edit.profile_tab.show_media.title": "Mostrar aba \"Mídia\"",
|
||||
"account_edit.profile_tab.show_media_replies.description": "Quando ativa, a aba Mídia mostra seus posts e respostas nos posts de outras pessoas.",
|
||||
"account_edit.profile_tab.show_media_replies.title": "Incluir respostas na aba \"Mídia\"",
|
||||
"account_edit.profile_tab.show_relations.description": "Exibe contas que segue e seguidores para outros usuários no seu perfil. As pessoas poderão ainda ver se você está as seguindo.",
|
||||
"account_edit.profile_tab.show_featured.title": "Exibir aba 'Em destaque'",
|
||||
"account_edit.profile_tab.show_media.description": "'Mídia' é uma aba opcional que mostra suas publicações contendo imagens ou vídeos.",
|
||||
"account_edit.profile_tab.show_media.title": "Exibir aba 'Mídia'",
|
||||
"account_edit.profile_tab.show_media_replies.description": "Se ativa, a aba Mídia mostrará ambas suas publicações e respostas a outras pessoas.",
|
||||
"account_edit.profile_tab.show_media_replies.title": "Incluir respostas na aba 'Mídia'",
|
||||
"account_edit.profile_tab.show_relations.description": "Exibe em seu perfil as contas que segue e seguidores para outros usuários. As pessoas poderão ainda ver se você as segue.",
|
||||
"account_edit.profile_tab.show_relations.title": "Exibir 'Seguidores' e 'Seguindo'",
|
||||
"account_edit.profile_tab.subtitle": "Customizar como seu perfil é exibido.",
|
||||
"account_edit.profile_tab.subtitle": "Personalize como seu perfil é exibido.",
|
||||
"account_edit.profile_tab.title": "Configurações de exibição do perfil",
|
||||
"account_edit.save": "Salvar",
|
||||
"account_edit.upload_modal.back": "Voltar",
|
||||
"account_edit.upload_modal.done": "Concluído",
|
||||
"account_edit.upload_modal.next": "Próximo",
|
||||
"account_edit.upload_modal.step_crop.zoom": "Aproximar",
|
||||
"account_edit.upload_modal.step_upload.button": "Procurar arquivos",
|
||||
"account_edit.upload_modal.step_crop.zoom": "Ampliar",
|
||||
"account_edit.upload_modal.step_upload.button": "Buscar arquivos",
|
||||
"account_edit.upload_modal.step_upload.dragging": "Solte para enviar",
|
||||
"account_edit.upload_modal.step_upload.header": "Escolha uma imagem",
|
||||
"account_edit.upload_modal.step_upload.hint": "WEBP, formatos GIF ou JPG, até {limit}MB.{br} imagem será redimensionada para {width}x{height}px.",
|
||||
"account_edit.upload_modal.step_upload.hint": "Formato .webp, .png, .gif ou .jpg, até {limit}MB.{br}A imagem será redimensionada para {width}x{height}px.",
|
||||
"account_edit.upload_modal.title_add.avatar": "Adicionar foto de perfil",
|
||||
"account_edit.upload_modal.title_add.header": "Adicionar foto de capa",
|
||||
"account_edit.upload_modal.title_replace.avatar": "Substituir foto de perfil",
|
||||
"account_edit.upload_modal.title_replace.header": "Substituir foto de capa",
|
||||
"account_edit.verified_modal.details": "Dê credibilidade ao seu perfil do Mastodon, verificando links para sites pessoais. Veja como funciona:",
|
||||
"account_edit.verified_modal.invisible_link.details": "Adicione o link para seu HTML do site. A parte importante é rel=\"eu\" onde previne falsificação de identidade em sites com conteúdo gerado por usuários. Você pode até mesmo usar um link tag no HTML da página ao invés de {tag}, mas o HTML deve ser acessível sem a execução do JavaScript.",
|
||||
"account_edit.verified_modal.invisible_link.summary": "Como posso tornar o link invisível?",
|
||||
"account_edit.verified_modal.invisible_link.details": "Adicione o link ao cabeçalho. A parte importante é rel=\"me\" que impede personificação em sites com conteúdo gerado por usuários. Você pode usar um rótulo de link no cabeçalho em vez de {tag}, porém o HTML deverá ser acessível sem precisar de JavaScript.",
|
||||
"account_edit.verified_modal.invisible_link.summary": "Como tornar o link invisível?",
|
||||
"account_edit.verified_modal.step1.header": "Copie o código HTML abaixo e cole no cabeçalho do seu site",
|
||||
"account_edit.verified_modal.step2.details": "Se já adicionou seu site como um campo personalizado, deverá excluí-lo e adicioná-lo novamente para acionar a verificação.",
|
||||
"account_edit.verified_modal.step2.header": "Adicione seu site como um campo personalizado",
|
||||
"account_edit.verified_modal.title": "Como adicionar um link verificado",
|
||||
"account_edit_tags.add_tag": "Adicionar #{tagName}",
|
||||
"account_edit_tags.column_title": "Editar Tags",
|
||||
"account_edit_tags.column_title": "Editar rótulos",
|
||||
"account_edit_tags.help_text": "Hashtags em destaque ajudam os usuários a descobrir e interagir com seu perfil. Elas aparecem como filtros na visualização de Atividade da sua página de Perfil.",
|
||||
"account_edit_tags.max_tags_reached": "Você atingiu o número máximo de hashtags em destaque.",
|
||||
"account_edit_tags.search_placeholder": "Insira uma hashtag…",
|
||||
"account_edit_tags.suggestions": "Sugestões:",
|
||||
"account_edit_tags.tag_status_count": "{count, plural, one {# publicação} other {# publicações}}",
|
||||
"account_list.hidden_notice": "Isto é apenas visível a ti. Para mostrar essa lista aos outros, vá a <link>{page} > {modal} > {field}</link>.",
|
||||
"account_list.total": "{total, plural,one {#conta}other {#contas}}",
|
||||
"account_list.hidden_notice": "Isto é visível somente para você. Para exibir para todos, vá a <link>{page} > {modal} > {field}</link>.",
|
||||
"account_list.total": "{total, plural, one {# conta} other {# contas}}",
|
||||
"admin.dashboard.daily_retention": "Taxa de retenção de usuários por dia, após a inscrição",
|
||||
"admin.dashboard.monthly_retention": "Taxa de retenção de usuários por mês, após a inscrição",
|
||||
"admin.dashboard.retention.average": "Média",
|
||||
@@ -277,17 +278,17 @@
|
||||
"alt_text_modal.change_thumbnail": "Alterar miniatura",
|
||||
"alt_text_modal.describe_for_people_with_hearing_impairments": "Descreva isto para pessoas com deficiências auditivas…",
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "Descreva isto para pessoas com deficiências visuais…",
|
||||
"alt_text_modal.done": "Feito",
|
||||
"alt_text_modal.done": "Concluído",
|
||||
"announcement.announcement": "Anúncio",
|
||||
"annual_report.announcement.action_build": "Gerar meu Wrapstodon",
|
||||
"annual_report.announcement.action_dismiss": "Não, obrigado/a",
|
||||
"annual_report.announcement.action_dismiss": "Não, obrigado",
|
||||
"annual_report.announcement.action_view": "Ver meu Wrapstodon",
|
||||
"annual_report.announcement.description": "Descubra mais sobre seu engajamento no Mastodon ao longo do último ano.",
|
||||
"annual_report.announcement.title": "Chegou o Wrapstodon de {year}",
|
||||
"annual_report.nav_item.badge": "Novo",
|
||||
"annual_report.shared_page.donate": "Doe",
|
||||
"annual_report.shared_page.donate": "Doar",
|
||||
"annual_report.shared_page.footer": "Criado com {heart} pela equipe do Mastodon",
|
||||
"annual_report.shared_page.footer_server_info": "{username} utiliza {domain}, uma das várias comunidades baseadas no Mastodon.",
|
||||
"annual_report.shared_page.footer_server_info": "{username} usa {domain}, uma de muitas comunidades baseadas no Mastodon.",
|
||||
"annual_report.summary.archetype.booster.desc_public": "{name} se manteve na caça por publicações para impulsionar, amplificando outros criadores com uma mira perfeita.",
|
||||
"annual_report.summary.archetype.booster.desc_self": "Você se manteve na caça por publicações para impulsionar, amplificando outros criadores com uma mira perfeita.",
|
||||
"annual_report.summary.archetype.booster.name": "O Arqueiro",
|
||||
@@ -362,7 +363,7 @@
|
||||
"closed_registrations_modal.preamble": "O Mastodon é descentralizado, não importa onde você criou a sua conta, será possível seguir e interagir com qualquer pessoa neste servidor. Você pode até mesmo criar o seu próprio servidor!",
|
||||
"closed_registrations_modal.title": "Inscrevendo-se no Mastodon",
|
||||
"collection.share_modal.share_link_label": "Compartilhar link",
|
||||
"collection.share_modal.share_via_post": "Postar no Mastodon",
|
||||
"collection.share_modal.share_via_post": "Publicar no Mastodon",
|
||||
"collection.share_modal.share_via_system": "Enviar para…",
|
||||
"collection.share_modal.title": "Compartilhar coleção",
|
||||
"collection.share_modal.title_new": "Compartilhe sua nova coleção!",
|
||||
@@ -372,10 +373,11 @@
|
||||
"collections.accounts.empty_description": "Adicione até {count} contas",
|
||||
"collections.accounts.empty_editor_title": "Ainda não há ninguém nesta coleção",
|
||||
"collections.accounts.empty_title": "Esta coleção está vazia",
|
||||
"collections.add_to_collection": "Adicionar {name} à coleção",
|
||||
"collections.block_collection_owner": "Bloquear conta",
|
||||
"collections.by_account": "por {account_handle}",
|
||||
"collections.collection_description": "Descrição",
|
||||
"collections.collection_language": "Língua",
|
||||
"collections.collection_language": "Idioma",
|
||||
"collections.collection_language_none": "Nenhum",
|
||||
"collections.collection_name": "Nome",
|
||||
"collections.collection_topic": "Tópico",
|
||||
@@ -388,17 +390,17 @@
|
||||
"collections.create.basic_details_title": "Detalhes básicos",
|
||||
"collections.create.steps": "Passo {step}/{total}",
|
||||
"collections.create_collection": "Criar coleção",
|
||||
"collections.delete_collection": "Eliminar coleção",
|
||||
"collections.delete_collection": "Excluir coleção",
|
||||
"collections.description_length_hint": "Limite de 100 caracteres",
|
||||
"collections.detail.author_added_you_on_date": "{author} te adicionou em {date}",
|
||||
"collections.detail.loading": "Carregando coleção…",
|
||||
"collections.detail.revoke_inclusion": "Remover-me",
|
||||
"collections.detail.sensitive_content": "Conteúdo sensível",
|
||||
"collections.detail.sensitive_note": "A descrição e as contas talvez possam não ser adequadas para todos.",
|
||||
"collections.detail.sensitive_note": "A descrição e as contas podem não ser adequadas para todos os públicos.",
|
||||
"collections.detail.share": "Compartilhar esta coleção",
|
||||
"collections.detail.you_are_in_this_collection": "Você aparece nesta coleção",
|
||||
"collections.edit_details": "Editar detalhes",
|
||||
"collections.error_loading_collections": "Houve um erro ao tentar carregar essas coleções.",
|
||||
"collections.error_loading_collections": "Houve um erro ao tentar carregar estas coleções.",
|
||||
"collections.hidden_accounts_description": "Você bloqueou ou silencilou {count, plural, one{este usuário} other {estes usuários}}",
|
||||
"collections.hidden_accounts_link": "{count, plural, one {# conta oculta} other {# contas ocultas}}",
|
||||
"collections.hints.accounts_counter": "{count}/{max} contas",
|
||||
@@ -491,7 +493,7 @@
|
||||
"compose.saved.body": "Publicação salva.",
|
||||
"compose_form.direct_message_warning_learn_more": "Saiba mais",
|
||||
"compose_form.encryption_warning": "As publicações no Mastodon não são criptografadas de ponta-a-ponta. Não compartilhe nenhuma informação sensível no Mastodon.",
|
||||
"compose_form.hashtag_warning": "Esta publicação não será exibida sob nenhuma hashtag, já que não é pública. Apenas postagens públicas podem ser pesquisadas por meio de hashtags.",
|
||||
"compose_form.hashtag_warning": "Esta publicação não será exibida sob nenhuma hashtag, já que não é pública. Apenas publicações públicas podem ser pesquisadas por meio de hashtags.",
|
||||
"compose_form.lock_disclaimer": "Seu perfil não está {locked}. Qualquer um pode te seguir e ver as suas publicações privadas.",
|
||||
"compose_form.lock_disclaimer.lock": "trancado",
|
||||
"compose_form.placeholder": "No que você está pensando?",
|
||||
@@ -513,9 +515,9 @@
|
||||
"confirmations.delete.confirm": "Excluir",
|
||||
"confirmations.delete.message": "Você tem certeza de que deseja excluir esta publicação?",
|
||||
"confirmations.delete.title": "Excluir publicação?",
|
||||
"confirmations.delete_collection.confirm": "Deletar",
|
||||
"confirmations.delete_collection.confirm": "Excluir",
|
||||
"confirmations.delete_collection.message": "Esta ação não pode ser desfeita.",
|
||||
"confirmations.delete_collection.title": "Deletar \"{name}\"?",
|
||||
"confirmations.delete_collection.title": "Excluir \"{name}\"?",
|
||||
"confirmations.delete_list.confirm": "Excluir",
|
||||
"confirmations.delete_list.message": "Você tem certeza de que deseja excluir esta lista?",
|
||||
"confirmations.delete_list.title": "Excluir lista?",
|
||||
@@ -559,7 +561,7 @@
|
||||
"confirmations.remove_from_followers.message": "{name} vai parar de te seguir. Tem certeza de que deseja continuar?",
|
||||
"confirmations.remove_from_followers.title": "Remover seguidor?",
|
||||
"confirmations.revoke_collection_inclusion.confirm": "Remover-me",
|
||||
"confirmations.revoke_collection_inclusion.message": "Esta ação é permanente e o curador não poderá adicionar-lhe de volta à coleção mais tarde.",
|
||||
"confirmations.revoke_collection_inclusion.message": "Esta ação é permanente e o curador não poderá lhe adicionar de volta à coleção mais tarde.",
|
||||
"confirmations.revoke_collection_inclusion.title": "Remover-se desta coleção?",
|
||||
"confirmations.revoke_quote.confirm": "Remover publicação",
|
||||
"confirmations.revoke_quote.message": "Esta ação não pode ser desfeita.",
|
||||
@@ -595,7 +597,7 @@
|
||||
"domain_block_modal.block_account_instead": "Bloquear @{name} em vez disso",
|
||||
"domain_block_modal.they_can_interact_with_old_posts": "Pessoas deste servidor podem interagir com suas publicações antigas.",
|
||||
"domain_block_modal.they_cant_follow": "Ninguém deste servidor pode lhe seguir.",
|
||||
"domain_block_modal.they_wont_know": "Eles não saberão que foram bloqueados.",
|
||||
"domain_block_modal.they_wont_know": "O/A usuário/a não saberá que foi bloqueado/a.",
|
||||
"domain_block_modal.title": "Bloquear domínio?",
|
||||
"domain_block_modal.you_will_lose_num_followers": "Você perderá {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} e {followingCount, plural, one {{followingCountDisplay} pessoa que você segue} other {{followingCountDisplay} pessoas que você segue}}.",
|
||||
"domain_block_modal.you_will_lose_relationships": "Você irá perder todos os seguidores e pessoas que você segue neste servidor.",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Bu hesabın gizlilik durumu gizli olarak ayarlanmış. Sahibi, onu kimin takip edebileceğini elle onaylıyor.",
|
||||
"account.media": "Medya",
|
||||
"account.mention": "@{name} kişisinden bahset",
|
||||
"account.menu.add_to_collection": "Koleksiyona ekle…",
|
||||
"account.menu.add_to_list": "Listeye ekle…",
|
||||
"account.menu.block": "Hesabı engelle",
|
||||
"account.menu.block_domain": "{domain} alan adını engelle",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "{count} hesap ekleyebilirsiniz",
|
||||
"collections.accounts.empty_editor_title": "Koleksiyonda henüz kimse yok",
|
||||
"collections.accounts.empty_title": "Bu koleksiyon boş",
|
||||
"collections.add_to_collection": "{name} kişisini koleksiyonlara ekle",
|
||||
"collections.block_collection_owner": "Hesabı engelle",
|
||||
"collections.by_account": "Hazırlayan: {account_handle}",
|
||||
"collections.collection_description": "Açıklama",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "Đây là tài khoản riêng tư. Chủ tài khoản tự mình xét duyệt các yêu cầu theo dõi.",
|
||||
"account.media": "Phương tiện",
|
||||
"account.mention": "Nhắc đến @{name}",
|
||||
"account.menu.add_to_collection": "Thêm vào gói khởi đầu…",
|
||||
"account.menu.add_to_list": "Thêm vào danh sách",
|
||||
"account.menu.block": "Chặn tài khoản",
|
||||
"account.menu.block_domain": "Chặn {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "Thêm tối đa {count} tài khoản",
|
||||
"collections.accounts.empty_editor_title": "Chưa có ai trong gói khởi đầu này",
|
||||
"collections.accounts.empty_title": "Gói khởi đầu này trống",
|
||||
"collections.add_to_collection": "Thêm {name} vào gói khởi đầu",
|
||||
"collections.block_collection_owner": "Chặn tài khoản",
|
||||
"collections.by_account": "bởi {account_handle}",
|
||||
"collections.collection_description": "Mô tả",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"account.locked_info": "此帳號的隱私狀態被設為鎖定。該擁有者會手動審核能跟隨此帳號的人。",
|
||||
"account.media": "媒體",
|
||||
"account.mention": "提及 @{name}",
|
||||
"account.menu.add_to_collection": "加入至收藏名單...",
|
||||
"account.menu.add_to_list": "新增至列表...",
|
||||
"account.menu.block": "封鎖帳號",
|
||||
"account.menu.block_domain": "封鎖 {domain}",
|
||||
@@ -372,6 +373,7 @@
|
||||
"collections.accounts.empty_description": "加入最多 {count} 個帳號",
|
||||
"collections.accounts.empty_editor_title": "此收藏名單尚未有任何人",
|
||||
"collections.accounts.empty_title": "此收藏名單是空的",
|
||||
"collections.add_to_collection": "將 {name} 加入至收藏名單",
|
||||
"collections.block_collection_owner": "封鎖帳號",
|
||||
"collections.by_account": "來自 {account_handle}",
|
||||
"collections.collection_description": "說明",
|
||||
|
||||
21
app/javascript/mastodon/models/server.ts
Normal file
21
app/javascript/mastodon/models/server.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type {
|
||||
ApiInstanceJSON,
|
||||
ApiExtendedDescriptionJSON,
|
||||
ApiDomainBlockJSON,
|
||||
} from 'mastodon/api_types/instance';
|
||||
|
||||
export type Server = ApiInstanceJSON;
|
||||
|
||||
export const createServerFromServerJSON = (obj: ApiInstanceJSON): Server => obj;
|
||||
|
||||
export type ExtendedDescription = ApiExtendedDescriptionJSON;
|
||||
|
||||
export const createExtendedDescriptionFromServerJSON = (
|
||||
obj: ApiExtendedDescriptionJSON,
|
||||
): ExtendedDescription => obj;
|
||||
|
||||
export type DomainBlock = ApiDomainBlockJSON;
|
||||
|
||||
export const createDomainBlockFromServerJSON = (
|
||||
obj: ApiDomainBlockJSON,
|
||||
): DomainBlock => obj;
|
||||
@@ -30,7 +30,7 @@ import { pollsReducer } from './polls';
|
||||
import push_notifications from './push_notifications';
|
||||
import { relationshipsReducer } from './relationships';
|
||||
import { searchReducer } from './search';
|
||||
import server from './server';
|
||||
import { serverReducer } from './server';
|
||||
import settings from './settings';
|
||||
import { sliceReducers } from './slices';
|
||||
import status_lists from './status_lists';
|
||||
@@ -58,7 +58,7 @@ const reducers = {
|
||||
relationships: relationshipsReducer,
|
||||
settings,
|
||||
push_notifications,
|
||||
server,
|
||||
server: serverReducer,
|
||||
contexts: contextsReducer,
|
||||
compose: composeReducer,
|
||||
search: searchReducer,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
SERVER_FETCH_REQUEST,
|
||||
SERVER_FETCH_SUCCESS,
|
||||
SERVER_FETCH_FAIL,
|
||||
SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
|
||||
SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
|
||||
SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
|
||||
EXTENDED_DESCRIPTION_REQUEST,
|
||||
EXTENDED_DESCRIPTION_SUCCESS,
|
||||
EXTENDED_DESCRIPTION_FAIL,
|
||||
SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
|
||||
SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
|
||||
SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
|
||||
} from 'mastodon/actions/server';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
server: ImmutableMap({
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
extendedDescription: ImmutableMap({
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
domainBlocks: ImmutableMap({
|
||||
isLoading: false,
|
||||
isAvailable: true,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default function server(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SERVER_FETCH_REQUEST:
|
||||
return state.setIn(['server', 'isLoading'], true);
|
||||
case SERVER_FETCH_SUCCESS:
|
||||
return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false);
|
||||
case SERVER_FETCH_FAIL:
|
||||
return state.setIn(['server', 'isLoading'], false);
|
||||
case SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST:
|
||||
return state.setIn(['translationLanguages', 'isLoading'], true);
|
||||
case SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS:
|
||||
return state.setIn(['translationLanguages', 'items'], fromJS(action.translationLanguages)).setIn(['translationLanguages', 'isLoading'], false);
|
||||
case SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL:
|
||||
return state.setIn(['translationLanguages', 'isLoading'], false);
|
||||
case EXTENDED_DESCRIPTION_REQUEST:
|
||||
return state.setIn(['extendedDescription', 'isLoading'], true);
|
||||
case EXTENDED_DESCRIPTION_SUCCESS:
|
||||
return state.set('extendedDescription', fromJS(action.description)).setIn(['extendedDescription', 'isLoading'], false);
|
||||
case EXTENDED_DESCRIPTION_FAIL:
|
||||
return state.setIn(['extendedDescription', 'isLoading'], false);
|
||||
case SERVER_DOMAIN_BLOCKS_FETCH_REQUEST:
|
||||
return state.setIn(['domainBlocks', 'isLoading'], true);
|
||||
case SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS:
|
||||
return state.setIn(['domainBlocks', 'items'], fromJS(action.blocks)).setIn(['domainBlocks', 'isLoading'], false).setIn(['domainBlocks', 'isAvailable'], action.isAvailable);
|
||||
case SERVER_DOMAIN_BLOCKS_FETCH_FAIL:
|
||||
return state.setIn(['domainBlocks', 'isLoading'], false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
127
app/javascript/mastodon/reducers/server.ts
Normal file
127
app/javascript/mastodon/reducers/server.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
fetchServer,
|
||||
fetchServerTranslationLanguages,
|
||||
fetchExtendedDescription,
|
||||
fetchDomainBlocks,
|
||||
} from 'mastodon/actions/server';
|
||||
import type {
|
||||
Server,
|
||||
ExtendedDescription,
|
||||
DomainBlock,
|
||||
} from 'mastodon/models/server';
|
||||
import {
|
||||
createServerFromServerJSON,
|
||||
createExtendedDescriptionFromServerJSON,
|
||||
createDomainBlockFromServerJSON,
|
||||
} from 'mastodon/models/server';
|
||||
|
||||
interface State {
|
||||
server: {
|
||||
isLoading: boolean;
|
||||
item?: Server;
|
||||
};
|
||||
|
||||
extendedDescription: {
|
||||
isLoading: boolean;
|
||||
item?: ExtendedDescription;
|
||||
};
|
||||
|
||||
translationLanguages: {
|
||||
isLoading: boolean;
|
||||
item?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
domainBlocks: {
|
||||
isLoading: boolean;
|
||||
isAvailable: boolean;
|
||||
items: DomainBlock[];
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
server: {
|
||||
isLoading: false,
|
||||
item: undefined,
|
||||
},
|
||||
|
||||
extendedDescription: {
|
||||
isLoading: false,
|
||||
item: undefined,
|
||||
},
|
||||
|
||||
translationLanguages: {
|
||||
isLoading: false,
|
||||
item: undefined,
|
||||
},
|
||||
|
||||
domainBlocks: {
|
||||
isLoading: false,
|
||||
isAvailable: true,
|
||||
items: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const serverReducer = createReducer(initialState, (builder) => {
|
||||
builder.addCase(fetchServer.pending, (state) => {
|
||||
state.server.isLoading = true;
|
||||
});
|
||||
|
||||
builder.addCase(fetchServer.fulfilled, (state, action) => {
|
||||
state.server.item = createServerFromServerJSON(action.payload);
|
||||
state.server.isLoading = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchServer.rejected, (state) => {
|
||||
state.server.isLoading = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchExtendedDescription.pending, (state) => {
|
||||
state.extendedDescription.isLoading = true;
|
||||
});
|
||||
|
||||
builder.addCase(fetchExtendedDescription.fulfilled, (state, action) => {
|
||||
state.extendedDescription.item = createExtendedDescriptionFromServerJSON(
|
||||
action.payload,
|
||||
);
|
||||
state.extendedDescription.isLoading = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchExtendedDescription.rejected, (state) => {
|
||||
state.extendedDescription.isLoading = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchServerTranslationLanguages.pending, (state) => {
|
||||
state.translationLanguages.isLoading = true;
|
||||
});
|
||||
|
||||
builder.addCase(
|
||||
fetchServerTranslationLanguages.fulfilled,
|
||||
(state, action) => {
|
||||
state.translationLanguages.item = action.payload;
|
||||
state.translationLanguages.isLoading = false;
|
||||
},
|
||||
);
|
||||
|
||||
builder.addCase(fetchServerTranslationLanguages.rejected, (state) => {
|
||||
state.translationLanguages.isLoading = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchDomainBlocks.pending, (state) => {
|
||||
state.domainBlocks.isLoading = true;
|
||||
});
|
||||
|
||||
builder.addCase(fetchDomainBlocks.fulfilled, (state, action) => {
|
||||
state.domainBlocks.items = action.payload.map((obj) =>
|
||||
createDomainBlockFromServerJSON(obj),
|
||||
);
|
||||
state.domainBlocks.isLoading = false;
|
||||
state.domainBlocks.isAvailable = true;
|
||||
});
|
||||
|
||||
builder.addCase(fetchDomainBlocks.rejected, (state) => {
|
||||
state.domainBlocks.isLoading = false;
|
||||
state.domainBlocks.isAvailable = false;
|
||||
});
|
||||
});
|
||||
@@ -283,8 +283,12 @@ const collectionSlice = createSlice({
|
||||
builder.addCase(addCollectionItem.fulfilled, (state, action) => {
|
||||
const { collection_item } = action.payload;
|
||||
const { collectionId } = action.meta.arg;
|
||||
const collection = state.collections[collectionId];
|
||||
|
||||
state.collections[collectionId]?.items.push(collection_item);
|
||||
if (collection) {
|
||||
collection.items.push(collection_item);
|
||||
collection.item_count++;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -302,6 +306,7 @@ const collectionSlice = createSlice({
|
||||
collection.items = collection.items.filter(
|
||||
(item) => item.id !== itemId,
|
||||
);
|
||||
collection.item_count--;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -282,12 +282,8 @@ export const updateField = createAppAsyncThunk(
|
||||
throw new Error('Profile fields not found');
|
||||
}
|
||||
|
||||
const maxFields = getState().server.getIn([
|
||||
'server',
|
||||
'configuration',
|
||||
'accounts',
|
||||
'max_fields',
|
||||
]) as number | undefined;
|
||||
const maxFields =
|
||||
getState().server.server.item?.configuration.accounts.max_profile_fields;
|
||||
if (maxFields && fields.length >= maxFields && !arg.id) {
|
||||
throw new Error('Maximum number of profile fields reached');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@mixin search-input {
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
@@ -11,6 +10,10 @@
|
||||
font-size: 17px;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
outline: var(--outline-focus-default);
|
||||
outline-color: transparent;
|
||||
outline-offset: -1px;
|
||||
transition: outline-color 0.15s ease-out;
|
||||
}
|
||||
|
||||
@mixin search-popout {
|
||||
|
||||
@@ -136,6 +136,11 @@ $content-width: 840px;
|
||||
transition: all 100ms linear;
|
||||
transition-property: color, background-color;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus-default);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -1887,6 +1892,28 @@ a.sparkline {
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-skip-link {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
margin: 10px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-primary);
|
||||
box-shadow: var(--dropdown-shadow);
|
||||
|
||||
/* Hide visually when not focused */
|
||||
&:not(:focus-within) {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.section-skip-link {
|
||||
float: right;
|
||||
|
||||
|
||||
@@ -541,13 +541,15 @@ body > [data-popper-placement] {
|
||||
flex: 0 1 auto;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
transition: border-color 300ms linear;
|
||||
position: relative;
|
||||
background: var(--color-bg-secondary);
|
||||
outline: var(--outline-focus-default);
|
||||
outline-color: transparent;
|
||||
outline-offset: -1px;
|
||||
transition: outline-color 0.15s ease-out;
|
||||
|
||||
&.active {
|
||||
transition: none;
|
||||
border-color: var(--color-border-brand);
|
||||
&:has(textarea:focus) {
|
||||
outline-color: var(--color-border-brand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,8 +597,15 @@ body > [data-popper-placement] {
|
||||
|
||||
.autosuggest-input {
|
||||
flex: 1 1 auto;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-width: 1px 0;
|
||||
border-block: 1px solid var(--color-border-primary);
|
||||
padding-block: 1px;
|
||||
transition: border-color 0.15s ease-out;
|
||||
|
||||
&:focus-within {
|
||||
padding-block: 0;
|
||||
border-color: var(--color-border-brand);
|
||||
border-block-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,7 +630,7 @@ body > [data-popper-placement] {
|
||||
}
|
||||
|
||||
.spoiler-input__input {
|
||||
padding: 12px 12px - 5px;
|
||||
padding: 11px 7px;
|
||||
background: var(--color-bg-brand-softest);
|
||||
color: var(--color-text-brand);
|
||||
}
|
||||
@@ -2741,6 +2750,7 @@ a.account__display-name {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
outline-offset: 0;
|
||||
|
||||
.display-name__account {
|
||||
font-size: 14px;
|
||||
@@ -3655,7 +3665,11 @@ a.account__display-name {
|
||||
|
||||
.compose-panel {
|
||||
width: 285px;
|
||||
margin-top: 10px;
|
||||
margin-top: 8px;
|
||||
|
||||
// Padding prevents focus outlines from being cut off
|
||||
margin-inline-start: -2px;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 10px);
|
||||
@@ -5827,7 +5841,7 @@ a.status-card {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
margin-top: -2px;
|
||||
margin-top: 1px;
|
||||
width: 100%;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
@@ -5922,14 +5936,8 @@ a.status-card {
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.search__input {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.search__popout {
|
||||
display: block;
|
||||
}
|
||||
&.active .search__popout {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5941,14 +5949,8 @@ a.status-card {
|
||||
padding-inline-start: 16px + 15px + 8px;
|
||||
line-height: normal;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
&:focus {
|
||||
outline-color: var(--color-border-brand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9216,6 +9218,10 @@ noscript {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:has(input:focus) {
|
||||
outline-color: var(--color-border-brand);
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
@@ -9244,7 +9250,7 @@ noscript {
|
||||
}
|
||||
|
||||
.search__popout {
|
||||
margin-top: -1px;
|
||||
margin-top: 1px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
@@ -9257,10 +9263,6 @@ noscript {
|
||||
&.expanded .search__popout {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.expanded &__input {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__choices {
|
||||
|
||||
@@ -114,10 +114,9 @@
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border-width: 1px !important;
|
||||
outline: var(--outline-focus-default);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
@@ -141,10 +140,12 @@
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ class ActivityPub::LinkedDataSignature
|
||||
|
||||
def verify_actor!
|
||||
return unless @json['signature'].is_a?(Hash)
|
||||
return if unsupported_jsonld_features?(@json)
|
||||
|
||||
type = @json['signature']['type']
|
||||
creator_uri = @json['signature']['creator']
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PrivateAddressCheck
|
||||
IP4_CIDR_LIST = [
|
||||
CIDR_LIST = [
|
||||
# IPv4 addresses
|
||||
IPAddr.new('0.0.0.0/8'), # Current network (only valid as source address)
|
||||
IPAddr.new('100.64.0.0/10'), # Shared Address Space
|
||||
IPAddr.new('172.16.0.0/12'), # Private network
|
||||
@@ -14,10 +15,11 @@ module PrivateAddressCheck
|
||||
IPAddr.new('224.0.0.0/4'), # IP multicast (former Class D network)
|
||||
IPAddr.new('240.0.0.0/4'), # Reserved (former Class E network)
|
||||
IPAddr.new('255.255.255.255'), # Broadcast
|
||||
].freeze
|
||||
|
||||
CIDR_LIST = (IP4_CIDR_LIST + IP4_CIDR_LIST.map(&:ipv4_mapped) + [
|
||||
# IPv6 addresses
|
||||
IPAddr.new('::/128'), # Unspecified
|
||||
IPAddr.new('64:ff9b::/96'), # IPv4/IPv6 translation (RFC 6052)
|
||||
IPAddr.new('64:ff9b:1::/48'), # IPv4/IPv6 translation (RFC 8215)
|
||||
IPAddr.new('100::/64'), # Discard prefix (RFC 6666)
|
||||
IPAddr.new('2001::/32'), # Teredo tunneling
|
||||
IPAddr.new('2001:10::/28'), # Deprecated (previously ORCHID)
|
||||
@@ -25,12 +27,14 @@ module PrivateAddressCheck
|
||||
IPAddr.new('2001:db8::/32'), # Addresses used in documentation and example source code
|
||||
IPAddr.new('2002::/16'), # 6to4
|
||||
IPAddr.new('fc00::/7'), # Unique local address
|
||||
IPAddr.new('3fff::/20'), # Addresses used in documentation and example source code
|
||||
IPAddr.new('ff00::/8'), # Multicast
|
||||
]).freeze
|
||||
].freeze
|
||||
|
||||
module_function
|
||||
|
||||
def private_address?(address)
|
||||
address = address.native if address.ipv6? && address.ipv4_mapped?
|
||||
address.private? || address.loopback? || address.link_local? || CIDR_LIST.any? { |cidr| cidr.include?(address) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -290,7 +290,7 @@ class Request
|
||||
|
||||
addresses = []
|
||||
begin
|
||||
addresses = [IPAddr.new(host)]
|
||||
addresses = [IPAddr.new(host).to_s]
|
||||
rescue IPAddr::InvalidAddressError
|
||||
resolvers = [Resolv::Hosts.new, Resolv::DNS.new.tap { |dns| dns.timeouts = 5 }]
|
||||
addresses = Resolv.new(resolvers).getaddresses(host)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
# Table name: collections
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# deleted_at :datetime
|
||||
# description :text
|
||||
# description_html :text
|
||||
# discoverable :boolean not null
|
||||
@@ -32,7 +31,6 @@ class Collection < ApplicationRecord
|
||||
|
||||
has_many :collection_items, dependent: :delete_all
|
||||
has_many :accepted_collection_items, -> { accepted }, class_name: 'CollectionItem', inverse_of: :collection # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
has_many :top_items, -> { top_items }, class_name: 'CollectionItem', inverse_of: :collection # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
has_many :collection_reports, dependent: :delete_all
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
@@ -47,15 +47,6 @@ class CollectionItem < ApplicationRecord
|
||||
scope :local, -> { joins(:collection).merge(Collection.local) }
|
||||
scope :accepted_partial, ->(account) { joins(:account).merge(Account.local).accepted.where(uri: nil, account_id: account.id) }
|
||||
scope :pending_or_accepted, -> { where(state: [:pending, :accepted]) }
|
||||
scope :top_items, lambda { |limit = 4|
|
||||
subquery = where('collection_items.collection_id = collections.id')
|
||||
.accepted.ordered.limit(limit)
|
||||
.arel.lateral('top_items')
|
||||
collection_query = Collection
|
||||
.select('top_items.*')
|
||||
.from([Collection.arel_table, subquery])
|
||||
from(collection_query, 'collection_items')
|
||||
}
|
||||
|
||||
def with_local_account?
|
||||
account&.local?
|
||||
|
||||
@@ -205,7 +205,7 @@ class MediaAttachment < ApplicationRecord
|
||||
remotable_attachment :thumbnail, IMAGE_LIMIT, suppress_errors: true, download_on_assign: false
|
||||
|
||||
validates :account, presence: true
|
||||
validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }
|
||||
validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, if: :local?
|
||||
validates :file, presence: true, if: :local?
|
||||
validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? }
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CollectionsPresenter < ActiveModelSerializers::Model
|
||||
attributes :collections
|
||||
|
||||
def accounts
|
||||
owners = collections.map(&:account)
|
||||
top_accounts = collections.flat_map { |c| c.top_items.map(&:account) }
|
||||
(owners + top_accounts).uniq
|
||||
end
|
||||
end
|
||||
@@ -1,10 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::CollectionsWithAccountPreviewsSerializer < ActiveModel::Serializer
|
||||
has_many :collections, serializer: REST::CollectionSerializer
|
||||
has_many :partial_accounts, serializer: REST::PartialAccountSerializer
|
||||
|
||||
def partial_accounts
|
||||
object.accounts
|
||||
end
|
||||
end
|
||||
@@ -13,6 +13,10 @@ class ActivityPub::ProcessCollectionService < BaseService
|
||||
|
||||
begin
|
||||
@json = compact(@json) if @json['signature'].is_a?(Hash)
|
||||
if unsupported_jsonld_features?(@json)
|
||||
Rails.logger.debug { "JSON-LD document for #{value_or_id(@json['actor'])} contains unsupported JSON-LD features" }
|
||||
@json = original_json.without('signature')
|
||||
end
|
||||
rescue JSON::LD::JsonLdError => e
|
||||
Rails.logger.debug { "Error when compacting JSON-LD document for #{value_or_id(@json['actor'])}: #{e.message}" }
|
||||
@json = original_json.without('signature')
|
||||
|
||||
@@ -11,7 +11,10 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
@collection = collection
|
||||
@request_id = request_id
|
||||
@item_json = uri_or_object.is_a?(String) ? fetch_resource(uri_or_object, true) : uri_or_object
|
||||
@actor_uri = value_or_id(@item_json['featuredObject'])
|
||||
@approval_uri = value_or_id(@item_json['featureAuthorization'])
|
||||
return if non_matching_uri_hosts?(@collection.uri, @item_json['id'])
|
||||
return if non_matching_actor_and_approval_uris?
|
||||
|
||||
with_redis_lock("collection_item:#{@item_json['id']}") do
|
||||
@collection_item = existing_item || pre_approved_item || new_item
|
||||
@@ -22,8 +25,6 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
object_uri: value_or_id(@item_json['featuredObject'])
|
||||
)
|
||||
|
||||
@approval_uri = @item_json['featureAuthorization']
|
||||
|
||||
verify_authorization! unless @collection_item&.account&.local?
|
||||
|
||||
@collection_item
|
||||
@@ -48,6 +49,12 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
)
|
||||
end
|
||||
|
||||
def non_matching_actor_and_approval_uris?
|
||||
return false if ActivityPub::TagManager.instance.local_uri?(@actor_uri)
|
||||
|
||||
non_matching_uri_hosts?(@actor_uri, @approval_uri)
|
||||
end
|
||||
|
||||
def verify_authorization!
|
||||
ActivityPub::VerifyFeaturedItemService.new.call(@collection_item, @approval_uri, request_id: @request_id)
|
||||
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
|
||||
@@ -12,8 +12,11 @@ class ActivityPub::VerifyFeaturedItemService
|
||||
return
|
||||
end
|
||||
|
||||
return if non_matching_uri_hosts?(approval_uri, @authorization['interactionTarget'])
|
||||
return unless matching_type? && matching_collection_uri?
|
||||
@collection_uri = value_or_id(@authorization['interactingObject'])
|
||||
@actor_uri = value_or_id(@authorization['interactionTarget'])
|
||||
|
||||
return if non_matching_uri_hosts?(approval_uri, @actor_uri)
|
||||
return unless matching_type? && matching_collection_uri? && matching_actors?
|
||||
|
||||
account = Account.where(uri: @collection_item.object_uri).first
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(@collection_item.object_uri, request_id:)
|
||||
@@ -29,6 +32,10 @@ class ActivityPub::VerifyFeaturedItemService
|
||||
end
|
||||
|
||||
def matching_collection_uri?
|
||||
@collection_item.collection.uri == @authorization['interactingObject']
|
||||
@collection_item.collection.uri == @collection_uri
|
||||
end
|
||||
|
||||
def matching_actors?
|
||||
@collection_item.object_uri == @actor_uri
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
- content_for :body_classes, 'admin'
|
||||
|
||||
- content_for :content do
|
||||
%a.navigation-skip-link{ href: '#content' }= t('admin.skip_to_content')
|
||||
.admin-wrapper
|
||||
.sidebar-wrapper
|
||||
%nav.sidebar-wrapper
|
||||
.sidebar-wrapper__inner
|
||||
.sidebar
|
||||
= link_to root_path do
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
= render_navigation
|
||||
|
||||
.content-wrapper
|
||||
%main.content-wrapper#content
|
||||
.content
|
||||
.content__heading
|
||||
- if content_for?(:heading)
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
- content_for :content do
|
||||
.container-alt
|
||||
.logo-container
|
||||
%header.logo-container
|
||||
- if within_authorization_flow?
|
||||
= logo_as_symbol(:wordmark)
|
||||
- else
|
||||
= link_to root_path do
|
||||
= logo_as_symbol(:wordmark)
|
||||
|
||||
.form-container
|
||||
%main.form-container
|
||||
= render 'flashes'
|
||||
|
||||
= yield
|
||||
|
||||
@@ -51,17 +51,16 @@
|
||||
label_method: ->(contrast) { I18n.t("contrast.#{contrast}", default: contrast) },
|
||||
wrapper: :with_label,
|
||||
required: false
|
||||
|
||||
.fields-group
|
||||
= f.simple_fields_for :settings, current_user.settings do |ff|
|
||||
= ff.input :'web.emoji_style',
|
||||
collection: user_settings_collection('web.emoji_style'),
|
||||
include_blank: false,
|
||||
hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'),
|
||||
label: I18n.t('simple_form.labels.defaults.setting_emoji_style'),
|
||||
label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) },
|
||||
wrapper: :with_label,
|
||||
required: false
|
||||
.input.horizontal-options
|
||||
= ff.input :'web.emoji_style',
|
||||
as: :radio_buttons,
|
||||
collection: user_settings_collection('web.emoji_style'),
|
||||
include_blank: false,
|
||||
hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'),
|
||||
label: I18n.t('simple_form.labels.defaults.setting_emoji_style'),
|
||||
label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) },
|
||||
wrapper: :with_label,
|
||||
required: false
|
||||
|
||||
- unless I18n.locale == :en
|
||||
.flash-message.translation-prompt
|
||||
|
||||
@@ -45,7 +45,7 @@ pt-BR:
|
||||
import:
|
||||
attributes:
|
||||
data:
|
||||
malformed: está malformado
|
||||
malformed: está mal formado
|
||||
list_account:
|
||||
attributes:
|
||||
account_id:
|
||||
@@ -67,17 +67,17 @@ pt-BR:
|
||||
blocked: usa provedor de e-mail não permitido
|
||||
unreachable: parece não existir
|
||||
role_id:
|
||||
elevated: não pode ser maior que a sua função atual
|
||||
elevated: não pode ser maior que seu cargo atual
|
||||
user_role:
|
||||
attributes:
|
||||
permissions_as_keys:
|
||||
dangerous: incluir permissões que não são seguras para a função base
|
||||
elevated: não pode incluir permissões que a sua função atual não possui
|
||||
own_role: não pode ser alterado com sua função atual
|
||||
dangerous: incluir permissões que não são seguras para o cargo base
|
||||
elevated: não pode incluir permissões que o seu cargo atual não possui
|
||||
own_role: não pode ser alterado com seu cargo atual
|
||||
position:
|
||||
elevated: não pode ser maior do que sua função atual
|
||||
own_role: não pode ser alterado com sua função atual
|
||||
elevated: não pode ser maior do que seu cargo atual
|
||||
own_role: não pode ser alterado com seu cargo atual
|
||||
webhook:
|
||||
attributes:
|
||||
events:
|
||||
invalid_permissions: não pode incluir eventos que você não tem permissão para
|
||||
invalid_permissions: não pode incluir eventos para os quais você não tem permissão
|
||||
|
||||
@@ -357,6 +357,7 @@ be:
|
||||
back_to_report: Назад да старонкі скаргі
|
||||
batch:
|
||||
add_to_report: 'Дадаць да скаргі #%{id}'
|
||||
remove_from_report: Выдаліць са справаздачы
|
||||
report: Скарга
|
||||
collection_title: Калекцыя %{name}
|
||||
contents: Змесціва
|
||||
|
||||
@@ -176,21 +176,21 @@ pt-BR:
|
||||
read:accounts: ver informações das contas
|
||||
read:blocks: ver seus bloqueados
|
||||
read:bookmarks: ver seus salvos
|
||||
read:favourites: veja seus favoritos
|
||||
read:favourites: ver seus favoritos
|
||||
read:filters: ver seus filtros
|
||||
read:follows: ver quem você segue
|
||||
read:lists: ver suas listas
|
||||
read:mutes: ver seus silenciados
|
||||
read:notifications: ver suas notificações
|
||||
read:reports: ver suas denúncias
|
||||
read:search: buscar em seu nome
|
||||
read:search: buscar em seu lugar
|
||||
read:statuses: ver todos os toots
|
||||
write: alterar todos os dados da sua conta
|
||||
write:accounts: alterar seu perfil
|
||||
write:blocks: bloquear contas e domínios
|
||||
write:bookmarks: salvar toots
|
||||
write:conversations: silenciar e excluir conversas
|
||||
write:favourites: publicações favoritas
|
||||
write:favourites: favoritar publicações
|
||||
write:filters: criar filtros
|
||||
write:follows: seguir pessoas
|
||||
write:lists: criar listas
|
||||
|
||||
@@ -979,6 +979,7 @@ en:
|
||||
site_uploads:
|
||||
delete: Delete uploaded file
|
||||
destroyed_msg: Site upload successfully deleted!
|
||||
skip_to_content: Skip to content
|
||||
software_updates:
|
||||
critical_update: Critical — please update quickly
|
||||
description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your email notification preferences.
|
||||
|
||||
@@ -349,6 +349,7 @@ fr-CA:
|
||||
back_to_report: Retour à la page du signalement
|
||||
batch:
|
||||
add_to_report: 'Ajouter au signalement #%{id}'
|
||||
remove_from_report: Retirer du signalement
|
||||
report: Signaler
|
||||
collection_title: Collection par %{name}
|
||||
contents: Contenu
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user