Merge pull request #3525 from glitch-soc/glitch-soc/merge-upstream

Merge upstream changes up to ead13d4826a7ba1699d6bb310e58642b15687671
This commit is contained in:
Claire 2026-06-08 08:26:35 +02:00 committed by GitHub
commit 60608f54e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
191 changed files with 1548 additions and 616 deletions

View File

@ -22,6 +22,7 @@ on:
jobs:
security:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BUNDLE_ONLY: development

View File

@ -19,6 +19,7 @@ permissions:
jobs:
check-i18n:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

View File

@ -12,6 +12,7 @@ jobs:
pathcheck:
name: Check for relevant changes
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
changed: ${{ steps.filter.outputs.src }}
steps:
@ -38,6 +39,7 @@ jobs:
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
timeout-minutes: 15
needs: pathcheck
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
steps:

View File

@ -17,6 +17,7 @@ jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: read

View File

@ -10,6 +10,7 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository

View File

@ -27,6 +27,7 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository

View File

@ -25,6 +25,7 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BUNDLE_ONLY: development

View File

@ -33,6 +33,7 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository

View File

@ -27,6 +27,7 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BUNDLE_ONLY: development

View File

@ -31,6 +31,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository

View File

@ -25,6 +25,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false

View File

@ -19,6 +19,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: true
@ -75,6 +76,7 @@ jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
needs:
- build
@ -176,6 +178,7 @@ jobs:
test-e2e:
name: End to End testing
runs-on: ubuntu-latest
timeout-minutes: 15
needs:
- build
@ -279,6 +282,7 @@ jobs:
test-search:
name: Elastic Search integration testing
runs-on: ubuntu-latest
timeout-minutes: 15
needs:
- build

View File

@ -16,7 +16,7 @@ All notable changes to this project will be documented in this file.
- Add export for custom filters (#39085 by @arte7)
- Add ability to search email blocks by domain in admin UI (#38923 by @arte7)
- Add new endpoints for profile editing in REST API (#37912, #37934, #37932, #38221, and #38339 by @ClearlyClaire)
- Add `GET /api/v1/profile` and `PATCH /api/v1/profile` to replace the existing `update_credentials` endpoint. See [the documentation](https://www.notion.so/joinmastodon/Mastodon-v4-6-0-beta1-changelog-3656208ac91b8088a745d15a9e81f727) for more information.
- Add `GET /api/v1/profile` and `PATCH /api/v1/profile` to replace the existing `update_credentials` endpoint. See [the documentation](https://docs.joinmastodon.org/methods/profile/) for more information.
- Add `missing_attribution` boolean to preview cards in REST API (#38043 by @ClearlyClaire)
- Documentation: https://docs.joinmastodon.org/entities/PreviewCard/#missing_attribution
- Add `exclude_direct` flag to `/api/v1/accounts/:id/statuses` to exclude direct messages (#37763 by @ClearlyClaire)

View File

@ -10,7 +10,7 @@ GIT
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.18)
action_text-trix (2.1.19)
railties
actioncable (8.1.3)
actionpack (= 8.1.3)
@ -99,8 +99,8 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1255.0)
aws-sdk-core (3.250.0)
aws-partitions (1.1257.0)
aws-sdk-core (3.251.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@ -108,10 +108,10 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.128.0)
aws-sdk-kms (1.129.0)
aws-sdk-core (~> 3, >= 3.248.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.224.0)
aws-sdk-s3 (1.225.0)
aws-sdk-core (~> 3, >= 3.248.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@ -132,7 +132,7 @@ GEM
binding_of_caller (2.0.0)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.24.4)
bootsnap (1.24.6)
msgpack (~> 1.2)
brakeman (8.0.4)
racc
@ -159,7 +159,7 @@ GEM
cbor (0.5.10.2)
cgi (0.5.1)
charlock_holmes (0.7.9)
chewy (8.0.1)
chewy (8.3.1)
activesupport (>= 7.2)
elasticsearch (>= 8.14, < 9.0)
elasticsearch-dsl
@ -178,13 +178,14 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (2.1.0)
css_parser (3.0.0)
addressable
ssrf_filter (~> 1.5)
csv (3.3.5)
database_cleaner-active_record (2.2.2)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0)
database_cleaner-core (2.0.1)
database_cleaner-core (2.1.0)
date (3.5.1)
debug (1.11.1)
irb (~> 1.10)
@ -209,12 +210,12 @@ GEM
activerecord (>= 7.0, < 9.0)
docile (1.4.1)
domain_name (0.6.20240107)
doorkeeper (5.9.0)
doorkeeper (5.9.1)
railties (>= 5)
dotenv (3.2.0)
drb (2.2.3)
dry-cli (1.4.1)
elastic-transport (8.5.1)
elastic-transport (8.5.2)
faraday (< 3)
multi_json
elasticsearch (8.19.3)
@ -224,13 +225,11 @@ GEM
elasticsearch-api (8.19.3)
multi_json
elasticsearch-dsl (0.1.10)
email_validator (2.2.4)
activemodel
erb (6.0.4)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
excon (1.4.2)
excon (1.5.0)
logger
fabrication (3.0.0)
faker (3.8.0)
@ -243,12 +242,12 @@ GEM
faraday (>= 1, < 3)
faraday-httpclient (2.0.2)
httpclient (>= 2.2)
faraday-net_http (3.4.3)
faraday-net_http (3.4.4)
net-http (~> 0.5)
fast_blank (1.0.1)
fastimage (2.4.1)
ffi (1.17.4)
ffi-compiler (1.3.2)
ffi-compiler (1.4.2)
ffi (>= 1.15.5)
rake
flatware (2.4.0)
@ -273,15 +272,15 @@ GEM
formatador (1.2.3)
reline
forwardable (1.4.0)
fugit (1.12.1)
fugit (1.12.2)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
google-protobuf (4.34.1)
google-protobuf (4.35.0)
bigdecimal
rake (~> 13.3)
googleapis-common-protos-types (1.22.0)
googleapis-common-protos-types (1.23.0)
google-protobuf (~> 4.26)
haml (7.2.0)
temple (>= 0.8.2)
@ -354,9 +353,9 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.19.7)
json (2.19.8)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
json-jwt (1.17.1)
activesupport (>= 4.2)
aes_key_wrap
base64
@ -452,8 +451,8 @@ GEM
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
multi_json (1.20.1)
msgpack (1.8.1)
multi_json (1.21.1)
mutex_m (0.3.0)
net-http (0.9.1)
uri (>= 0.11.1)
@ -491,10 +490,9 @@ GEM
omniauth_openid_connect (0.8.0)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
openid_connect (2.3.1)
openid_connect (2.5.0)
activemodel
attr_required (>= 1.0.0)
email_validator
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
@ -504,7 +502,7 @@ GEM
tzinfo
validate_url
webfinger (~> 2.0)
openssl (4.0.1)
openssl (4.0.2)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.10.0)
@ -523,7 +521,7 @@ GEM
opentelemetry-helpers-sql-processor (0.5.0)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.8.0)
opentelemetry-instrumentation-action_mailer (0.8.1)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-action_pack (0.18.0)
opentelemetry-instrumentation-rack (~> 0.29)
@ -535,7 +533,7 @@ GEM
opentelemetry-instrumentation-active_support (>= 0.7.0)
opentelemetry-instrumentation-active_record (0.13.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-active_storage (0.5.0)
opentelemetry-instrumentation-active_storage (0.5.1)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-active_support (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
@ -582,7 +580,7 @@ GEM
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.37.1)
opentelemetry-semantic_conventions (1.39.0)
opentelemetry-api (~> 1.0)
orm_adapter (0.5.0)
ostruct (0.6.3)
@ -620,11 +618,11 @@ GEM
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.3.1)
psych (5.4.0)
date
stringio
public_suffix (7.0.5)
puma (8.0.1)
puma (8.0.2)
nio4r (~> 2.0)
pundit (2.5.2)
activesupport (>= 3.0.0)
@ -721,7 +719,8 @@ GEM
railties (>= 7.0)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.7.0)
rouge (5.0.0)
strscan (~> 3.1)
rpam2 (4.0.2)
rqrcode (3.2.0)
chunky_png (~> 1.0)
@ -844,6 +843,7 @@ GEM
simplecov-html (0.13.2)
simplecov-lcov (0.9.0)
simplecov_json_formatter (0.1.4)
ssrf_filter (1.5.0)
stackprof (0.2.28)
starry (0.2.0)
base64
@ -853,6 +853,7 @@ GEM
stringio (3.2.0)
strong_migrations (2.8.0)
activerecord (>= 7.2)
strscan (3.1.8)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
@ -929,7 +930,7 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2)
websocket-driver (0.8.0)
websocket-driver (0.8.1)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@ -1100,4 +1101,4 @@ RUBY VERSION
ruby 4.0.5
BUNDLED WITH
4.0.11
4.0.13

View File

@ -7,10 +7,13 @@ class AuthorizeInteractionsController < ApplicationController
before_action :set_resource
def show
if @resource.is_a?(Account)
case @resource
when Account
redirect_to web_url("@#{@resource.pretty_acct}")
elsif @resource.is_a?(Status)
when Status
redirect_to web_url("@#{@resource.account.pretty_acct}/#{@resource.id}")
when Collection
redirect_to web_url("collections/#{resource.id}")
else
not_found
end

View File

@ -199,8 +199,10 @@ module LanguagesHelper
kab: ['Kabyle', 'Taqbaylit'].freeze,
ldn: ['Láadan', 'Láadan'].freeze,
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
lzz: ['Lazuri', 'ლაზური ნენა'].freeze,
moh: ['Mohawk', 'Kanienʼkéha'].freeze,
nds: ['Low German', 'Plattdüütsch'].freeze,
ota: ['Ottoman Turkish', 'لسان عثمانی'].freeze,
pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
sco: ['Scots', 'Scots'].freeze,
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
@ -209,6 +211,7 @@ module LanguagesHelper
tok: ['Toki Pona', 'toki pona'].freeze,
vai: ['Vai', 'ꕙꔤ'].freeze,
xal: ['Kalmyk', 'Хальмг келн'].freeze,
xmf: ['Mingrelian', 'მარგალური ნინა'].freeze,
zba: ['Balaibalan', 'باليبلن'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
}.freeze

View File

@ -4,6 +4,7 @@ import { importAccounts } from './accounts';
import { importCustomEmoji } from './emoji';
import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';
import { fetchAccountsForCollectionPreview } from '@/flavours/glitch/reducers/slices/collections';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
@ -61,6 +62,7 @@ export function importFetchedStatuses(statuses, options = {}) {
const normalStatuses = [];
const polls = [];
const filters = [];
const collections = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), { ...options, settings: getState().get('local_settings') }));
@ -82,6 +84,10 @@ export function importFetchedStatuses(statuses, options = {}) {
pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id]));
}
if (status.tagged_collections.length) {
status.tagged_collections.forEach(collection => pushUnique(collections, collection));
}
if (status.card) {
status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
}
@ -97,5 +103,6 @@ export function importFetchedStatuses(statuses, options = {}) {
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
fetchAccountsForCollectionPreview(collections, dispatch);
};
}

View File

@ -5,6 +5,7 @@ import {
apiFetchNotificationGroups,
} from 'flavours/glitch/api/notifications';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
@ -26,6 +27,8 @@ import {
createDataLoadingThunk,
} from 'flavours/glitch/store/typed_functions';
import { fetchAccountsForCollectionPreview } from '../reducers/slices/collections';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';
@ -70,6 +73,7 @@ function dispatchAssociatedRecords(
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
const collections: ApiCollectionJSON[] = [];
notifications.forEach((notification) => {
if (notification.type === 'admin.report') {
@ -83,6 +87,10 @@ function dispatchAssociatedRecords(
if ('status' in notification && notification.status) {
fetchedStatuses.push(notification.status);
}
if ('collection' in notification && notification.collection) {
collections.push(notification.collection);
}
});
if (fetchedAccounts.length > 0)
@ -90,6 +98,9 @@ function dispatchAssociatedRecords(
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
if (collections.length > 0)
void fetchAccountsForCollectionPreview(collections, dispatch);
}
function selectNotificationGroupedTypes(state: RootState) {

View File

@ -12,6 +12,11 @@ import {
createAppAsyncThunk,
} from 'flavours/glitch/store/typed_functions';
import {
fetchAccountsForCollectionPreview,
importFetchedCollections,
} from '../reducers/slices/collections';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
@ -29,7 +34,7 @@ export const submitSearch = createDataLoadingThunk(
limit: 11,
});
},
(data, { dispatch }) => {
async (data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
@ -39,6 +44,11 @@ export const submitSearch = createDataLoadingThunk(
dispatch(importFetchedStatuses(data.statuses));
}
if (data.collections.length > 0) {
dispatch(importFetchedCollections(data.collections));
await fetchAccountsForCollectionPreview(data.collections, dispatch);
}
return data;
},
{
@ -60,7 +70,7 @@ export const expandSearch = createDataLoadingThunk(
offset,
});
},
(data, { dispatch }) => {
async (data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
@ -70,6 +80,11 @@ export const expandSearch = createDataLoadingThunk(
dispatch(importFetchedStatuses(data.statuses));
}
if (data.collections.length > 0) {
dispatch(importFetchedCollections(data.collections));
await fetchAccountsForCollectionPreview(data.collections, dispatch);
}
return data;
},
{

View File

@ -90,22 +90,22 @@ interface ReportNotificationJSON extends BaseNotificationJSON {
interface AddedToCollectionNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'added_to_collection';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
interface AddedToCollectionNotificationJSON extends BaseNotificationJSON {
type: 'added_to_collection';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
interface CollectionUpdateNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'collection_update';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
interface CollectionUpdateNotificationJSON extends BaseNotificationJSON {
type: 'collection_update';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';

View File

@ -29,7 +29,7 @@ import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { CollectionListItem } from '../collections/components/collection_list_item';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_you';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_account';
import { EmptyMessage } from './components/empty_message';
import { Subheading, SubheadingLink } from './components/subheading';

View File

@ -0,0 +1,91 @@
// app/javascript/flavours/glitch/features/alt_text_modal/__tests__/index-test.tsx
import { IntlProvider } from 'react-intl';
import { List, Map } from 'immutable';
import { render } from '@testing-library/react';
import { vi } from 'vitest';
import type { RootState } from 'flavours/glitch/store';
import { useAppSelector } from 'flavours/glitch/store';
import { AltTextModal } from '../index';
vi.mock('flavours/glitch/store', () => ({
useAppSelector: vi.fn(),
useAppDispatch: () => vi.fn(),
}));
describe('<AltTextModal />', () => {
const mediaId = '123';
const handleClose = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
const renderComponent = () => {
return render(
<IntlProvider locale='en' messages={{}}>
<AltTextModal mediaId={mediaId} onClose={handleClose} />
</IntlProvider>,
);
};
it('renders thumbnail upload button when video is unattached', () => {
vi.mocked(useAppSelector).mockImplementation(
(selector: (state: RootState) => unknown) => {
const mockState = {
compose: Map({
language: 'en',
media_attachments: List([
Map({
id: mediaId,
type: 'video',
unattached: true,
meta: Map({ focus: Map({ x: 0, y: 0 }) }),
}),
]),
}),
accounts: Map(),
} as unknown as RootState;
return selector(mockState);
},
);
const { container } = renderComponent();
const uploadInput = container.querySelector('#upload-modal__thumbnail');
expect(uploadInput).not.toBeNull();
});
it('hides thumbnail upload button when video is attached', () => {
vi.mocked(useAppSelector).mockImplementation(
(selector: (state: RootState) => unknown) => {
const mockState = {
compose: Map({
language: 'en',
media_attachments: List([
Map({
id: mediaId,
type: 'video',
unattached: false,
meta: Map({ focus: Map({ x: 0, y: 0 }) }),
}),
]),
}),
accounts: Map(),
} as unknown as RootState;
return selector(mockState);
},
);
const { container } = renderComponent();
const uploadInput = container.querySelector('#upload-modal__thumbnail');
expect(uploadInput).toBeNull();
});
});

View File

@ -283,6 +283,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
);
const type = media?.get('type') as string;
const valid = length(description) <= MAX_LENGTH;
const unattached = media?.get('unattached') as boolean | undefined;
const handleDescriptionChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
@ -433,7 +434,8 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
onPositionChange={handlePositionChange}
/>
{(type === 'audio' || type === 'video') && (
{/* This button is hidden for attached audio/video files, as they are already posted */}
{(type === 'audio' || type === 'video') && unattached && (
<UploadButton
onSelectFile={handleThumbnailChange}
mimeTypes='image/jpeg,image/png,image/gif,image/heic,image/heif,image/webp,image/avif'

View File

@ -15,7 +15,7 @@ import { IconButton } from 'flavours/glitch/components/icon_button';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { MAX_COLLECTION_ACCOUNT_COUNT } from '../collections/editor/accounts';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_you';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_account';
import { CollectionToggle } from './collection_toggle';

View File

@ -211,6 +211,12 @@ export const CollectionAccounts: React.FC<{
const hasItems = editorItems.length > 0;
const hasMaxItems = editorItems.length === MAX_COLLECTION_ACCOUNT_COUNT;
const wasAccountAdded = useCallback(
(account: ApiMutedAccountJSON) =>
!!editorItems.find((item) => item.account_id === account.id),
[editorItems],
);
const {
accounts: suggestedAccounts,
isLoading: isLoadingSuggestions,
@ -220,8 +226,7 @@ export const CollectionAccounts: React.FC<{
withRelationships: true,
withDefaultFollows: searchValue === '',
// Don't suggest accounts that were already added
filterResults: (account) =>
!editorItems.find((item) => item.account_id === account.id),
filterResults: (account) => !wasAccountAdded(account),
});
const relationships = useAppSelector((state) => state.relationships);
@ -259,23 +264,25 @@ export const CollectionAccounts: React.FC<{
const addAccountItem = useCallback(
(item: ApiMutedAccountJSON) => {
dispatch(
updateCollectionEditorField({
field: 'items',
value: [
...editorItems,
{
account_id: item.id,
state:
item.feature_approval.current_user === 'manual'
? 'pending'
: 'accepted',
},
],
}),
);
if (!wasAccountAdded(item)) {
dispatch(
updateCollectionEditorField({
field: 'items',
value: [
...editorItems,
{
account_id: item.id,
state:
item.feature_approval.current_user === 'manual'
? 'pending'
: 'accepted',
},
],
}),
);
}
},
[editorItems, dispatch],
[editorItems, wasAccountAdded, dispatch],
);
const instantRemoveAccountItem = useCallback(
@ -302,13 +309,13 @@ export const CollectionAccounts: React.FC<{
const instantAddAccountItem = useCallback(
(item: ApiMutedAccountJSON) => {
if (id) {
if (id && !wasAccountAdded(item)) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
},
[dispatch, id],
[dispatch, id, wasAccountAdded],
);
const handleRemoveAccountItem = useCallback(

View File

@ -13,20 +13,21 @@ import {
import { Helmet } from '@unhead/react/helmet';
import { Callout } from '@/flavours/glitch/components/callout';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { initialState } from '@/flavours/glitch/initial_state';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { Callout } from 'flavours/glitch/components/callout';
import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
import { useIdentity } from 'flavours/glitch/identity_context';
import { initialState } from 'flavours/glitch/initial_state';
import {
collectionEditorActions,
fetchCollection,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { useCollectionsCreatedBy } from '../overview/created_by_you';
import { useCollectionsCreatedBy } from '../overview/created_by_account';
import { CollectionAccounts } from './accounts';
import { CollectionDetails } from './details';
@ -75,7 +76,7 @@ export const CollectionEditorPage: React.FC<{
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const accountId = useCurrentAccountId();
const { accountId, signedIn } = useIdentity();
const { id = null } = useParams<{ id?: string }>();
const { path } = useRouteMatch();
const collection = useAppSelector((state) =>
@ -94,13 +95,13 @@ export const CollectionEditorPage: React.FC<{
(!isEditMode && collectionListStatus === 'loading');
const canCreateMoreCollections =
isEditMode || collectionList.length < userCollectionLimit;
signedIn && (isEditMode || collectionList.length < userCollectionLimit);
useEffect(() => {
if (id) {
if (id && signedIn) {
void dispatch(fetchCollection({ collectionId: id }));
}
}, [dispatch, id]);
}, [dispatch, id, signedIn]);
useEffect(() => {
if (id !== editorStateId) {
@ -129,6 +130,8 @@ export const CollectionEditorPage: React.FC<{
<div className='scrollable'>
{isLoading ? (
<LoadingIndicator />
) : !signedIn ? (
<NotSignedInIndicator />
) : canCreateMoreCollections ? (
<Switch>
<Route

View File

@ -4,18 +4,18 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { Helmet } from '@unhead/react/helmet';
import { TabLink, TabList } from '@/flavours/glitch/components/tab_list';
import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { DisplayNameSimple } from 'flavours/glitch/components/display_name/simple';
import { Scrollable } from 'flavours/glitch/components/scrollable_list/components';
import { TabLink, TabList } from 'flavours/glitch/components/tab_list';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import {
useAccountId,
useCurrentAccountId,
} from 'flavours/glitch/hooks/useAccountId';
import { CollectionsCreatedByYou } from './overview/created_by_you';
import { CollectionsCreatedByAccount } from './overview/created_by_account';
import { CollectionsFeaturingYou } from './overview/featuring_you';
import classes from './styles.module.scss';
@ -92,7 +92,7 @@ export const Collections: React.FC<{
</TabList>
</header>
<Switch>
<Route exact path={path} component={CollectionsCreatedByYou} />
<Route exact path={path} component={CollectionsCreatedByAccount} />
<Route
exact
path={`${path}/featuring-you`}

View File

@ -5,10 +5,12 @@ import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { EmptyState } from 'flavours/glitch/components/empty_state';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { ItemList } from 'flavours/glitch/components/scrollable_list/components';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import {
useAccountId,
useCurrentAccountId,
@ -61,9 +63,10 @@ export function useCollectionsCreatedBy(accountId: string | null | undefined) {
);
}
export const CollectionsCreatedByYou: React.FC = () => {
export const CollectionsCreatedByAccount: React.FC = () => {
const me = useCurrentAccountId();
const accountId = useAccountId();
const account = useAccount(accountId);
const { collections, status } = useCollectionsCreatedBy(accountId);
@ -81,24 +84,40 @@ export const CollectionsCreatedByYou: React.FC = () => {
}
if (collections.length === 0) {
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
<CreateButton />
</EmptyState>
);
if (isOwnCollectionPage) {
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
<CreateButton />
</EmptyState>
);
} else {
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.collections'
defaultMessage='{acct} has not created any collections yet.'
values={{
acct: <DisplayName variant='simple' account={account} />,
}}
/>
}
/>
);
}
}
return (
@ -116,7 +135,7 @@ export const CollectionsCreatedByYou: React.FC = () => {
{showCreateButton && <CreateButton />}
</div>
<ItemList>
{!canCreateMoreCollections && (
{isOwnCollectionPage && !canCreateMoreCollections && (
<MaxCollectionsCallout className={classes.maxCollectionsError} />
)}
{collections.map((item, index) => (

View File

@ -16,7 +16,7 @@ import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { CollectionListItem } from '../components/collection_list_item';
import classes from '../styles.module.scss';
import { CollectionListError } from './created_by_you';
import { CollectionListError } from './created_by_account';
function useCollectionsFeaturing(accountId: string | null | undefined) {
const dispatch = useAppDispatch();

View File

@ -6,6 +6,8 @@ import { Link } from 'react-router-dom';
import { Helmet } from '@unhead/react/helmet';
import { NotSignedInIndicator } from '@/flavours/glitch/components/not_signed_in_indicator';
import { useIdentity } from '@/flavours/glitch/identity_context';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -78,10 +80,13 @@ const Lists: React.FC<{
const dispatch = useAppDispatch();
const intl = useIntl();
const lists = useAppSelector((state) => getOrderedLists(state));
const { signedIn } = useIdentity();
useEffect(() => {
void dispatch(fetchLists());
}, [dispatch]);
if (signedIn) {
void dispatch(fetchLists());
}
}, [signedIn, dispatch]);
const emptyMessage = (
<>
@ -112,14 +117,16 @@ const Lists: React.FC<{
iconComponent={ListAltIcon}
multiColumn={multiColumn}
extraButton={
<Link
to='/lists/new'
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
>
<Icon id='plus' icon={AddIcon} />
</Link>
signedIn && (
<Link
to='/lists/new'
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
>
<Icon id='plus' icon={AddIcon} />
</Link>
)
}
/>
@ -128,9 +135,13 @@ const Lists: React.FC<{
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{lists.map((list) => (
<ListItem key={list.id} id={list.id} title={list.title} />
))}
{signedIn ? (
lists.map((list) => (
<ListItem key={list.id} id={list.id} title={list.title} />
))
) : (
<NotSignedInIndicator />
)}
</ScrollableList>
<Helmet>

View File

@ -8,6 +8,8 @@ import { isFulfilled } from '@reduxjs/toolkit';
import { Helmet } from '@unhead/react/helmet';
import { NotSignedInIndicator } from '@/flavours/glitch/components/not_signed_in_indicator';
import { useIdentity } from '@/flavours/glitch/identity_context';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchList } from 'flavours/glitch/actions/lists';
@ -250,16 +252,17 @@ const NewListWrapper: React.FC<{
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
const { id } = useParams<{ id?: string }>();
const list = useAppSelector((state) =>
id ? state.lists.get(id) : undefined,
);
useEffect(() => {
if (id) {
if (signedIn && id) {
dispatch(fetchList(id));
}
}, [dispatch, id]);
}, [dispatch, signedIn, id]);
const isLoading = id && !list;
@ -277,7 +280,13 @@ const NewListWrapper: React.FC<{
/>
<div className='scrollable'>
{isLoading ? <LoadingIndicator /> : <NewList list={list} />}
{!signedIn ? (
<NotSignedInIndicator />
) : isLoading ? (
<LoadingIndicator />
) : (
<NewList list={list} />
)}
</div>
<Helmet>

View File

@ -4,6 +4,7 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from '@unhead/react/helmet';
import CollectionsIcon from '@/material-icons/400-24px/category.svg?react';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
@ -23,6 +24,8 @@ import { useSearchParam } from 'flavours/glitch/hooks/useSearchParam';
import type { Hashtag as HashtagType } from 'flavours/glitch/models/tags';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { CollectionListItem } from '../collections/components/collection_list_item';
import { SearchSection } from './components/search_section';
const messages = defineMessages({
@ -131,7 +134,8 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
filteredResults =
results.accounts.length +
results.hashtags.length +
results.statuses.length >
results.statuses.length +
results.collections.length >
0 ? (
<>
{results.accounts.length > 0 && (
@ -154,6 +158,32 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
</SearchSection>
)}
{results.collections.length > 0 && (
<SearchSection
key='collections'
title={
<>
<Icon id='collections' icon={CollectionsIcon} />
<FormattedMessage
id='search_results.collections'
defaultMessage='Collections'
/>
</>
}
>
{results.collections
.slice(0, INITIAL_DISPLAY)
.map((collection, index, array) => (
<CollectionListItem
key={collection.id}
collection={collection}
listSize={array.length}
positionInList={index + 1}
/>
))}
</SearchSection>
)}
{results.hashtags.length > 0 && (
<SearchSection
key='hashtags'

View File

@ -89,7 +89,7 @@ import {
Quotes,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem, getFocusedColumnIndex } from './util/focusUtils';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import { CustomHomepage } from 'flavours/glitch/features/custom_homepage';
@ -552,20 +552,18 @@ class UI extends PureComponent {
handleMoveUp = () => {
const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) {
focusColumn(1);
return focusColumn(getFocusedColumnIndex());
} else {
const wasHandled = focusItemSibling(currentItemIndex, -1);
return wasHandled;
return focusItemSibling(currentItemIndex, -1);
}
};
handleMoveDown = () => {
const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) {
focusColumn(1);
return focusColumn(getFocusedColumnIndex());
} else {
const wasHandled = focusItemSibling(currentItemIndex, 1);
return wasHandled;
return focusItemSibling(currentItemIndex, 1);
}
};

View File

@ -62,18 +62,17 @@ export function focusColumn(index = 1) {
function fallback() {
focusColumnTitle(index + indexOffset, isMultiColumnLayout);
return false;
}
if (!column) {
fallback();
return;
return fallback();
}
const container = column.querySelector('.scrollable');
if (!container) {
fallback();
return;
return fallback();
}
const focusableItems = Array.from(
@ -86,8 +85,7 @@ export function focusColumn(index = 1) {
const itemToFocus = findFirstVisibleWithRect(focusableItems);
if (!itemToFocus) {
fallback();
return;
return fallback();
}
const viewportWidth =
@ -110,6 +108,7 @@ export function focusColumn(index = 1) {
itemToFocus.item.scrollIntoView(true);
}
itemToFocus.item.focus();
return true;
}
/**
@ -126,6 +125,18 @@ export function getFocusedItemIndex() {
return items.indexOf(focusedItem);
}
/**
* Get the index of the column that contains the user's focus
*/
export function getFocusedColumnIndex() {
const columnWithFocus = document.activeElement?.closest('.column');
if (!columnWithFocus) return 1;
const allColumns = Array.from(document.querySelectorAll('.column'));
return allColumns.indexOf(columnWithFocus) + 1;
}
/**
* Focus the topmost item of the column that currently has focus,
* or the first column if none
@ -159,12 +170,7 @@ export function focusItemSibling(index: number, direction: 1 | -1) {
);
if (!siblingItem) {
return false;
}
// If sibling element is empty, we skip it
if (siblingItem.matches(':empty')) {
return focusItemSibling(index + direction, direction);
return focusListSibling(direction);
}
// Check if the sibling is a post or a 'follow suggestions' widget
@ -177,6 +183,52 @@ export function focusItemSibling(index: number, direction: 1 | -1) {
targetElement = siblingItem;
}
// If sibling element is empty, we skip it
if (!targetElement || siblingItem.matches(':empty')) {
return focusItemSibling(index + direction, direction);
}
targetElement.scrollIntoView({
block: 'start',
});
targetElement.focus();
return true;
}
/**
* Finds the next or previous .item-list on the page or in the column,
* and focuses its first or last item.
*/
function focusListSibling(direction: 1 | -1) {
const container = document.activeElement?.closest('.item-list');
if (!container) {
return false;
}
// Get all item lists in the current column or page
const currentColumn = container.closest('.column') ?? document;
const columnItemLists = Array.from(
currentColumn.querySelectorAll('.item-list'),
);
const currentListIndex = columnItemLists.indexOf(container);
// Find the next or previous item-list
const listSibling = columnItemLists[currentListIndex + direction];
// Depending on the direction, find the first or last focusable in the list
let targetElement: HTMLElement | null | undefined;
if (direction > 0) {
targetElement = listSibling?.querySelector<HTMLElement>('.focusable');
} else {
const allFocusables =
listSibling?.querySelectorAll<HTMLElement>('.focusable');
targetElement = allFocusables?.[allFocusables.length - 1];
}
if (targetElement) {
targetElement.scrollIntoView({
block: 'start',

View File

@ -51,7 +51,7 @@ export function useSearchAccounts({
searchRequestRef.current = new AbortController();
try {
const data = await apiRequest<ApiAccountJSON[]>(
const accounts = await apiRequest<ApiAccountJSON[]>(
'GET',
'v1/accounts/search',
{
@ -62,7 +62,6 @@ export function useSearchAccounts({
},
},
);
const accounts = filterResults ? data.filter(filterResults) : data;
const accountIds = accounts.map((a) => a.id);
dispatch(importFetchedAccounts(accounts));
if (withRelationships) {
@ -95,6 +94,7 @@ export function useSearchAccounts({
const [defaultAccounts, setDefaultAccounts] = useState<
ApiAccountJSON[] | null
>(null);
useEffect(() => {
if (
!currentUserId ||
@ -108,12 +108,11 @@ export function useSearchAccounts({
async function doRequest() {
setLoadingState('loading');
try {
const data = await apiRequest<ApiAccountJSON[]>(
const accounts = await apiRequest<ApiAccountJSON[]>(
'GET',
`v1/accounts/${currentUserId}/following`,
{ params: { limit: 40 } },
);
const accounts = filterResults ? data.filter(filterResults) : data;
const accountIds = accounts.map((a) => a.id);
dispatch(importFetchedAccounts(accounts));
if (withRelationships) {
@ -137,13 +136,19 @@ export function useSearchAccounts({
withDefaultFollows,
]);
const accountsToReturn =
accounts.length === 0 && withDefaultFollows
? (defaultAccounts ?? [])
: accounts;
const filteredAccounts = filterResults
? accountsToReturn.filter(filterResults)
: accountsToReturn;
return {
searchAccounts: startSearch,
resetAccounts,
accounts:
accounts.length === 0 && withDefaultFollows
? (defaultAccounts ?? [])
: accounts,
accounts: filteredAccounts,
isLoading: loadingState === 'loading',
isError: loadingState === 'error',
};

View File

@ -1,5 +1,6 @@
import type { ApiSearchResultsJSON } from 'flavours/glitch/api_types/search';
import type { ApiHashtagJSON } from 'flavours/glitch/api_types/tags';
import type { ApiCollectionJSON } from '@/flavours/glitch/api_types/collections';
import type { ApiSearchResultsJSON } from '@/flavours/glitch/api_types/search';
import type { ApiHashtagJSON } from '@/flavours/glitch/api_types/tags';
export type SearchType = 'account' | 'hashtag' | 'accounts' | 'statuses';
@ -12,10 +13,12 @@ export interface SearchResults {
accounts: string[];
statuses: string[];
hashtags: ApiHashtagJSON[];
collections: ApiCollectionJSON[];
}
export const createSearchResults = (serverJSON: ApiSearchResultsJSON) => ({
accounts: serverJSON.accounts.map((account) => account.id),
statuses: serverJSON.statuses.map((status) => status.id),
hashtags: serverJSON.hashtags,
collections: serverJSON.collections,
});

View File

@ -52,6 +52,9 @@ export const searchReducer = createReducer(initialState, (builder) => {
hashtags: state.results
? [...state.results.hashtags, ...results.hashtags]
: results.hashtags,
collections: state.results
? [...state.results.collections, ...results.collections]
: results.collections,
};
state.loading = false;
});

View File

@ -27,6 +27,7 @@ import {
createAppSelector,
createDataLoadingThunk,
} from '@/flavours/glitch/store/typed_functions';
import { batchArray } from '@/flavours/glitch/utils/batch_array';
import { inputToHashtag } from '@/flavours/glitch/utils/hashtags';
type QueryStatus = 'idle' | 'loading' | 'error';
@ -120,6 +121,15 @@ const collectionSlice = createSlice({
const { field, value } = action.payload;
state.editor[field] = value;
},
importFetchedCollections(
state,
action: PayloadAction<ApiCollectionJSON[]>,
) {
const collections = action.payload;
collections.forEach((collection) => {
state.collections[collection.id] = collection;
});
},
},
extraReducers(builder) {
/**
@ -327,7 +337,7 @@ const collectionSlice = createSlice({
/**
* Prefetch accounts whose avatars will be displayed in the collection list
*/
async function importAccountsForPreviewCard(
export async function fetchAccountsForCollectionPreview(
collections: ApiCollectionJSON[],
dispatch: AppDispatch,
) {
@ -337,11 +347,17 @@ async function importAccountsForPreviewCard(
)
.filter((id): id is string => !!id);
await dispatch(
fetchAccounts({
accountIds: previewAccountIds,
}),
);
if (previewAccountIds.length > 0) {
// fetchAccounts can only process up to 40 item ids, so we'll
// batch the list of ids
const batchedAccountIdLists = batchArray(previewAccountIds, 40);
await Promise.allSettled(
batchedAccountIdLists.map((accountIds) =>
dispatch(fetchAccounts({ accountIds })),
),
);
}
}
export const fetchCollectionsCreatedByAccount = createDataLoadingThunk(
@ -349,7 +365,7 @@ export const fetchCollectionsCreatedByAccount = createDataLoadingThunk(
({ accountId }: { accountId: string }) =>
apiGetCollectionsCreatedByAccount(accountId),
async ({ collections }, { dispatch }) => {
await importAccountsForPreviewCard(collections, dispatch);
await fetchAccountsForCollectionPreview(collections, dispatch);
},
);
@ -358,7 +374,7 @@ export const fetchCollectionsFeaturingAccount = createDataLoadingThunk(
({ accountId }: { accountId: string }) =>
apiGetCollectionsFeaturingAccount(accountId),
async ({ collections }, { dispatch }) => {
await importAccountsForPreviewCard(collections, dispatch);
await fetchAccountsForCollectionPreview(collections, dispatch);
},
);
@ -414,6 +430,8 @@ export const collections = collectionSlice.reducer;
export const collectionEditorActions = collectionSlice.actions;
export const updateCollectionEditorField =
collectionSlice.actions.updateEditorField;
export const importFetchedCollections =
collectionSlice.actions.importFetchedCollections;
/**
* Selectors

View File

@ -486,6 +486,8 @@ $content-width: 840px;
.title {
font-weight: 600;
margin-bottom: 8px;
font-size: inherit;
line-height: inherit;
}
a {

View File

@ -558,7 +558,6 @@ code {
display: block;
width: 100%;
font-family: inherit;
resize: vertical;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);
border-radius: 4px;
@ -585,6 +584,10 @@ code {
}
}
textarea {
resize: vertical;
}
input[type='text'],
input[type='number'],
input[type='email'],

View File

@ -0,0 +1,15 @@
/**
* Splits a long array so that the resulting nested arrays
* never exceed the `maxLength` provided.
* Useful when dealing with endpoints that accept a limited number
* of parameters
*/
export function batchArray<T>(array: T[], maxLength: number) {
const result: T[][] = [];
for (let i = 0; i < array.length; i += maxLength) {
result.push(array.slice(i, i + maxLength));
}
return result;
}

View File

@ -4,6 +4,7 @@ import { importAccounts } from './accounts';
import { importCustomEmoji } from './emoji';
import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';
import { fetchAccountsForCollectionPreview } from '@/mastodon/reducers/slices/collections';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
@ -61,6 +62,7 @@ export function importFetchedStatuses(statuses, options = {}) {
const normalStatuses = [];
const polls = [];
const filters = [];
const collections = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), options));
@ -82,6 +84,10 @@ export function importFetchedStatuses(statuses, options = {}) {
pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id]));
}
if (status.tagged_collections.length) {
status.tagged_collections.forEach(collection => pushUnique(collections, collection));
}
if (status.card) {
status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
}
@ -97,5 +103,6 @@ export function importFetchedStatuses(statuses, options = {}) {
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
fetchAccountsForCollectionPreview(collections, dispatch);
};
}

View File

@ -5,6 +5,7 @@ import {
apiFetchNotificationGroups,
} from 'mastodon/api/notifications';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
@ -26,6 +27,8 @@ import {
createDataLoadingThunk,
} from 'mastodon/store/typed_functions';
import { fetchAccountsForCollectionPreview } from '../reducers/slices/collections';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';
@ -70,6 +73,7 @@ function dispatchAssociatedRecords(
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
const collections: ApiCollectionJSON[] = [];
notifications.forEach((notification) => {
if (notification.type === 'admin.report') {
@ -83,6 +87,10 @@ function dispatchAssociatedRecords(
if ('status' in notification && notification.status) {
fetchedStatuses.push(notification.status);
}
if ('collection' in notification && notification.collection) {
collections.push(notification.collection);
}
});
if (fetchedAccounts.length > 0)
@ -90,6 +98,9 @@ function dispatchAssociatedRecords(
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
if (collections.length > 0)
void fetchAccountsForCollectionPreview(collections, dispatch);
}
function selectNotificationGroupedTypes(state: RootState) {

View File

@ -12,6 +12,11 @@ import {
createAppAsyncThunk,
} from 'mastodon/store/typed_functions';
import {
fetchAccountsForCollectionPreview,
importFetchedCollections,
} from '../reducers/slices/collections';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
@ -29,7 +34,7 @@ export const submitSearch = createDataLoadingThunk(
limit: 11,
});
},
(data, { dispatch }) => {
async (data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
@ -39,6 +44,11 @@ export const submitSearch = createDataLoadingThunk(
dispatch(importFetchedStatuses(data.statuses));
}
if (data.collections.length > 0) {
dispatch(importFetchedCollections(data.collections));
await fetchAccountsForCollectionPreview(data.collections, dispatch);
}
return data;
},
{
@ -60,7 +70,7 @@ export const expandSearch = createDataLoadingThunk(
offset,
});
},
(data, { dispatch }) => {
async (data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
@ -70,6 +80,11 @@ export const expandSearch = createDataLoadingThunk(
dispatch(importFetchedStatuses(data.statuses));
}
if (data.collections.length > 0) {
dispatch(importFetchedCollections(data.collections));
await fetchAccountsForCollectionPreview(data.collections, dispatch);
}
return data;
},
{

View File

@ -90,22 +90,22 @@ interface ReportNotificationJSON extends BaseNotificationJSON {
interface AddedToCollectionNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'added_to_collection';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
interface AddedToCollectionNotificationJSON extends BaseNotificationJSON {
type: 'added_to_collection';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
interface CollectionUpdateNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'collection_update';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
interface CollectionUpdateNotificationJSON extends BaseNotificationJSON {
type: 'collection_update';
collection: ApiCollectionJSON;
collection: ApiCollectionJSON | null;
}
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';

View File

@ -29,7 +29,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { CollectionListItem } from '../collections/components/collection_list_item';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_you';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_account';
import { EmptyMessage } from './components/empty_message';
import { Subheading, SubheadingLink } from './components/subheading';

View File

@ -0,0 +1,91 @@
// app/javascript/mastodon/features/alt_text_modal/__tests__/index-test.tsx
import { IntlProvider } from 'react-intl';
import { List, Map } from 'immutable';
import { render } from '@testing-library/react';
import { vi } from 'vitest';
import type { RootState } from 'mastodon/store';
import { useAppSelector } from 'mastodon/store';
import { AltTextModal } from '../index';
vi.mock('mastodon/store', () => ({
useAppSelector: vi.fn(),
useAppDispatch: () => vi.fn(),
}));
describe('<AltTextModal />', () => {
const mediaId = '123';
const handleClose = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
const renderComponent = () => {
return render(
<IntlProvider locale='en' messages={{}}>
<AltTextModal mediaId={mediaId} onClose={handleClose} />
</IntlProvider>,
);
};
it('renders thumbnail upload button when video is unattached', () => {
vi.mocked(useAppSelector).mockImplementation(
(selector: (state: RootState) => unknown) => {
const mockState = {
compose: Map({
language: 'en',
media_attachments: List([
Map({
id: mediaId,
type: 'video',
unattached: true,
meta: Map({ focus: Map({ x: 0, y: 0 }) }),
}),
]),
}),
accounts: Map(),
} as unknown as RootState;
return selector(mockState);
},
);
const { container } = renderComponent();
const uploadInput = container.querySelector('#upload-modal__thumbnail');
expect(uploadInput).not.toBeNull();
});
it('hides thumbnail upload button when video is attached', () => {
vi.mocked(useAppSelector).mockImplementation(
(selector: (state: RootState) => unknown) => {
const mockState = {
compose: Map({
language: 'en',
media_attachments: List([
Map({
id: mediaId,
type: 'video',
unattached: false,
meta: Map({ focus: Map({ x: 0, y: 0 }) }),
}),
]),
}),
accounts: Map(),
} as unknown as RootState;
return selector(mockState);
},
);
const { container } = renderComponent();
const uploadInput = container.querySelector('#upload-modal__thumbnail');
expect(uploadInput).toBeNull();
});
});

View File

@ -283,6 +283,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
);
const type = media?.get('type') as string;
const valid = length(description) <= MAX_LENGTH;
const unattached = media?.get('unattached') as boolean | undefined;
const handleDescriptionChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
@ -433,7 +434,8 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
onPositionChange={handlePositionChange}
/>
{(type === 'audio' || type === 'video') && (
{/* This button is hidden for attached audio/video files, as they are already posted */}
{(type === 'audio' || type === 'video') && unattached && (
<UploadButton
onSelectFile={handleThumbnailChange}
mimeTypes='image/jpeg,image/png,image/gif,image/heic,image/heif,image/webp,image/avif'

View File

@ -15,7 +15,7 @@ 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 { useCollectionsCreatedBy } from '../collections/overview/created_by_account';
import { CollectionToggle } from './collection_toggle';

View File

@ -208,6 +208,12 @@ export const CollectionAccounts: React.FC<{
const hasItems = editorItems.length > 0;
const hasMaxItems = editorItems.length === MAX_COLLECTION_ACCOUNT_COUNT;
const wasAccountAdded = useCallback(
(account: ApiMutedAccountJSON) =>
!!editorItems.find((item) => item.account_id === account.id),
[editorItems],
);
const {
accounts: suggestedAccounts,
isLoading: isLoadingSuggestions,
@ -217,8 +223,7 @@ export const CollectionAccounts: React.FC<{
withRelationships: true,
withDefaultFollows: searchValue === '',
// Don't suggest accounts that were already added
filterResults: (account) =>
!editorItems.find((item) => item.account_id === account.id),
filterResults: (account) => !wasAccountAdded(account),
});
const relationships = useAppSelector((state) => state.relationships);
@ -256,23 +261,25 @@ export const CollectionAccounts: React.FC<{
const addAccountItem = useCallback(
(item: ApiMutedAccountJSON) => {
dispatch(
updateCollectionEditorField({
field: 'items',
value: [
...editorItems,
{
account_id: item.id,
state:
item.feature_approval.current_user === 'manual'
? 'pending'
: 'accepted',
},
],
}),
);
if (!wasAccountAdded(item)) {
dispatch(
updateCollectionEditorField({
field: 'items',
value: [
...editorItems,
{
account_id: item.id,
state:
item.feature_approval.current_user === 'manual'
? 'pending'
: 'accepted',
},
],
}),
);
}
},
[editorItems, dispatch],
[editorItems, wasAccountAdded, dispatch],
);
const instantRemoveAccountItem = useCallback(
@ -299,13 +306,13 @@ export const CollectionAccounts: React.FC<{
const instantAddAccountItem = useCallback(
(item: ApiMutedAccountJSON) => {
if (id) {
if (id && !wasAccountAdded(item)) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
},
[dispatch, id],
[dispatch, id, wasAccountAdded],
);
const handleRemoveAccountItem = useCallback(

View File

@ -13,20 +13,21 @@ import {
import { Helmet } from '@unhead/react/helmet';
import { Callout } from '@/mastodon/components/callout';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { initialState } from '@/mastodon/initial_state';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { Callout } from 'mastodon/components/callout';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { useIdentity } from 'mastodon/identity_context';
import { initialState } from 'mastodon/initial_state';
import {
collectionEditorActions,
fetchCollection,
} from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useCollectionsCreatedBy } from '../overview/created_by_you';
import { useCollectionsCreatedBy } from '../overview/created_by_account';
import { CollectionAccounts } from './accounts';
import { CollectionDetails } from './details';
@ -75,7 +76,7 @@ export const CollectionEditorPage: React.FC<{
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const accountId = useCurrentAccountId();
const { accountId, signedIn } = useIdentity();
const { id = null } = useParams<{ id?: string }>();
const { path } = useRouteMatch();
const collection = useAppSelector((state) =>
@ -94,13 +95,13 @@ export const CollectionEditorPage: React.FC<{
(!isEditMode && collectionListStatus === 'loading');
const canCreateMoreCollections =
isEditMode || collectionList.length < userCollectionLimit;
signedIn && (isEditMode || collectionList.length < userCollectionLimit);
useEffect(() => {
if (id) {
if (id && signedIn) {
void dispatch(fetchCollection({ collectionId: id }));
}
}, [dispatch, id]);
}, [dispatch, id, signedIn]);
useEffect(() => {
if (id !== editorStateId) {
@ -129,6 +130,8 @@ export const CollectionEditorPage: React.FC<{
<div className='scrollable'>
{isLoading ? (
<LoadingIndicator />
) : !signedIn ? (
<NotSignedInIndicator />
) : canCreateMoreCollections ? (
<Switch>
<Route

View File

@ -4,15 +4,15 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { Helmet } from '@unhead/react/helmet';
import { TabLink, TabList } from '@/mastodon/components/tab_list';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { DisplayNameSimple } from 'mastodon/components/display_name/simple';
import { Scrollable } from 'mastodon/components/scrollable_list/components';
import { TabLink, TabList } from 'mastodon/components/tab_list';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useAccountId, useCurrentAccountId } from 'mastodon/hooks/useAccountId';
import { CollectionsCreatedByYou } from './overview/created_by_you';
import { CollectionsCreatedByAccount } from './overview/created_by_account';
import { CollectionsFeaturingYou } from './overview/featuring_you';
import classes from './styles.module.scss';
@ -89,7 +89,7 @@ export const Collections: React.FC<{
</TabList>
</header>
<Switch>
<Route exact path={path} component={CollectionsCreatedByYou} />
<Route exact path={path} component={CollectionsCreatedByAccount} />
<Route
exact
path={`${path}/featuring-you`}

View File

@ -5,10 +5,12 @@ import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { DisplayName } from 'mastodon/components/display_name';
import { EmptyState } from 'mastodon/components/empty_state';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ItemList } from 'mastodon/components/scrollable_list/components';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useAccountId, useCurrentAccountId } from 'mastodon/hooks/useAccountId';
import {
fetchCollectionsCreatedByAccount,
@ -58,9 +60,10 @@ export function useCollectionsCreatedBy(accountId: string | null | undefined) {
);
}
export const CollectionsCreatedByYou: React.FC = () => {
export const CollectionsCreatedByAccount: React.FC = () => {
const me = useCurrentAccountId();
const accountId = useAccountId();
const account = useAccount(accountId);
const { collections, status } = useCollectionsCreatedBy(accountId);
@ -78,24 +81,40 @@ export const CollectionsCreatedByYou: React.FC = () => {
}
if (collections.length === 0) {
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
<CreateButton />
</EmptyState>
);
if (isOwnCollectionPage) {
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
<CreateButton />
</EmptyState>
);
} else {
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.collections'
defaultMessage='{acct} has not created any collections yet.'
values={{
acct: <DisplayName variant='simple' account={account} />,
}}
/>
}
/>
);
}
}
return (
@ -113,7 +132,7 @@ export const CollectionsCreatedByYou: React.FC = () => {
{showCreateButton && <CreateButton />}
</div>
<ItemList>
{!canCreateMoreCollections && (
{isOwnCollectionPage && !canCreateMoreCollections && (
<MaxCollectionsCallout className={classes.maxCollectionsError} />
)}
{collections.map((item, index) => (

View File

@ -16,7 +16,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { CollectionListItem } from '../components/collection_list_item';
import classes from '../styles.module.scss';
import { CollectionListError } from './created_by_you';
import { CollectionListError } from './created_by_account';
function useCollectionsFeaturing(accountId: string | null | undefined) {
const dispatch = useAppDispatch();

View File

@ -6,6 +6,8 @@ import { Link } from 'react-router-dom';
import { Helmet } from '@unhead/react/helmet';
import { NotSignedInIndicator } from '@/mastodon/components/not_signed_in_indicator';
import { useIdentity } from '@/mastodon/identity_context';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -78,10 +80,13 @@ const Lists: React.FC<{
const dispatch = useAppDispatch();
const intl = useIntl();
const lists = useAppSelector((state) => getOrderedLists(state));
const { signedIn } = useIdentity();
useEffect(() => {
void dispatch(fetchLists());
}, [dispatch]);
if (signedIn) {
void dispatch(fetchLists());
}
}, [signedIn, dispatch]);
const emptyMessage = (
<>
@ -112,14 +117,16 @@ const Lists: React.FC<{
iconComponent={ListAltIcon}
multiColumn={multiColumn}
extraButton={
<Link
to='/lists/new'
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
>
<Icon id='plus' icon={AddIcon} />
</Link>
signedIn && (
<Link
to='/lists/new'
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
>
<Icon id='plus' icon={AddIcon} />
</Link>
)
}
/>
@ -128,9 +135,13 @@ const Lists: React.FC<{
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{lists.map((list) => (
<ListItem key={list.id} id={list.id} title={list.title} />
))}
{signedIn ? (
lists.map((list) => (
<ListItem key={list.id} id={list.id} title={list.title} />
))
) : (
<NotSignedInIndicator />
)}
</ScrollableList>
<Helmet>

View File

@ -8,6 +8,8 @@ import { isFulfilled } from '@reduxjs/toolkit';
import { Helmet } from '@unhead/react/helmet';
import { NotSignedInIndicator } from '@/mastodon/components/not_signed_in_indicator';
import { useIdentity } from '@/mastodon/identity_context';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchList } from 'mastodon/actions/lists';
@ -250,16 +252,17 @@ const NewListWrapper: React.FC<{
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
const { id } = useParams<{ id?: string }>();
const list = useAppSelector((state) =>
id ? state.lists.get(id) : undefined,
);
useEffect(() => {
if (id) {
if (signedIn && id) {
dispatch(fetchList(id));
}
}, [dispatch, id]);
}, [dispatch, signedIn, id]);
const isLoading = id && !list;
@ -277,7 +280,13 @@ const NewListWrapper: React.FC<{
/>
<div className='scrollable'>
{isLoading ? <LoadingIndicator /> : <NewList list={list} />}
{!signedIn ? (
<NotSignedInIndicator />
) : isLoading ? (
<LoadingIndicator />
) : (
<NewList list={list} />
)}
</div>
<Helmet>

View File

@ -4,6 +4,7 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from '@unhead/react/helmet';
import CollectionsIcon from '@/material-icons/400-24px/category.svg?react';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
@ -23,6 +24,8 @@ import { useSearchParam } from 'mastodon/hooks/useSearchParam';
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { CollectionListItem } from '../collections/components/collection_list_item';
import { SearchSection } from './components/search_section';
const messages = defineMessages({
@ -131,7 +134,8 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
filteredResults =
results.accounts.length +
results.hashtags.length +
results.statuses.length >
results.statuses.length +
results.collections.length >
0 ? (
<>
{results.accounts.length > 0 && (
@ -154,6 +158,32 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
</SearchSection>
)}
{results.collections.length > 0 && (
<SearchSection
key='collections'
title={
<>
<Icon id='collections' icon={CollectionsIcon} />
<FormattedMessage
id='search_results.collections'
defaultMessage='Collections'
/>
</>
}
>
{results.collections
.slice(0, INITIAL_DISPLAY)
.map((collection, index, array) => (
<CollectionListItem
key={collection.id}
collection={collection}
listSize={array.length}
positionInList={index + 1}
/>
))}
</SearchSection>
)}
{results.hashtags.length > 0 && (
<SearchSection
key='hashtags'

View File

@ -86,7 +86,7 @@ import {
Quotes,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem, getFocusedColumnIndex } from './util/focusUtils';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import { CustomHomepage } from 'mastodon/features/custom_homepage';
@ -506,20 +506,18 @@ class UI extends PureComponent {
handleMoveUp = () => {
const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) {
focusColumn(1);
return focusColumn(getFocusedColumnIndex());
} else {
const wasHandled = focusItemSibling(currentItemIndex, -1);
return wasHandled;
return focusItemSibling(currentItemIndex, -1);
}
};
handleMoveDown = () => {
const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) {
focusColumn(1);
return focusColumn(getFocusedColumnIndex());
} else {
const wasHandled = focusItemSibling(currentItemIndex, 1);
return wasHandled;
return focusItemSibling(currentItemIndex, 1);
}
};

View File

@ -62,18 +62,17 @@ export function focusColumn(index = 1) {
function fallback() {
focusColumnTitle(index + indexOffset, isMultiColumnLayout);
return false;
}
if (!column) {
fallback();
return;
return fallback();
}
const container = column.querySelector('.scrollable');
if (!container) {
fallback();
return;
return fallback();
}
const focusableItems = Array.from(
@ -86,8 +85,7 @@ export function focusColumn(index = 1) {
const itemToFocus = findFirstVisibleWithRect(focusableItems);
if (!itemToFocus) {
fallback();
return;
return fallback();
}
const viewportWidth =
@ -110,6 +108,7 @@ export function focusColumn(index = 1) {
itemToFocus.item.scrollIntoView(true);
}
itemToFocus.item.focus();
return true;
}
/**
@ -126,6 +125,18 @@ export function getFocusedItemIndex() {
return items.indexOf(focusedItem);
}
/**
* Get the index of the column that contains the user's focus
*/
export function getFocusedColumnIndex() {
const columnWithFocus = document.activeElement?.closest('.column');
if (!columnWithFocus) return 1;
const allColumns = Array.from(document.querySelectorAll('.column'));
return allColumns.indexOf(columnWithFocus) + 1;
}
/**
* Focus the topmost item of the column that currently has focus,
* or the first column if none
@ -159,12 +170,7 @@ export function focusItemSibling(index: number, direction: 1 | -1) {
);
if (!siblingItem) {
return false;
}
// If sibling element is empty, we skip it
if (siblingItem.matches(':empty')) {
return focusItemSibling(index + direction, direction);
return focusListSibling(direction);
}
// Check if the sibling is a post or a 'follow suggestions' widget
@ -177,6 +183,52 @@ export function focusItemSibling(index: number, direction: 1 | -1) {
targetElement = siblingItem;
}
// If sibling element is empty, we skip it
if (!targetElement || siblingItem.matches(':empty')) {
return focusItemSibling(index + direction, direction);
}
targetElement.scrollIntoView({
block: 'start',
});
targetElement.focus();
return true;
}
/**
* Finds the next or previous .item-list on the page or in the column,
* and focuses its first or last item.
*/
function focusListSibling(direction: 1 | -1) {
const container = document.activeElement?.closest('.item-list');
if (!container) {
return false;
}
// Get all item lists in the current column or page
const currentColumn = container.closest('.column') ?? document;
const columnItemLists = Array.from(
currentColumn.querySelectorAll('.item-list'),
);
const currentListIndex = columnItemLists.indexOf(container);
// Find the next or previous item-list
const listSibling = columnItemLists[currentListIndex + direction];
// Depending on the direction, find the first or last focusable in the list
let targetElement: HTMLElement | null | undefined;
if (direction > 0) {
targetElement = listSibling?.querySelector<HTMLElement>('.focusable');
} else {
const allFocusables =
listSibling?.querySelectorAll<HTMLElement>('.focusable');
targetElement = allFocusables?.[allFocusables.length - 1];
}
if (targetElement) {
targetElement.scrollIntoView({
block: 'start',

View File

@ -51,7 +51,7 @@ export function useSearchAccounts({
searchRequestRef.current = new AbortController();
try {
const data = await apiRequest<ApiAccountJSON[]>(
const accounts = await apiRequest<ApiAccountJSON[]>(
'GET',
'v1/accounts/search',
{
@ -62,7 +62,6 @@ export function useSearchAccounts({
},
},
);
const accounts = filterResults ? data.filter(filterResults) : data;
const accountIds = accounts.map((a) => a.id);
dispatch(importFetchedAccounts(accounts));
if (withRelationships) {
@ -95,6 +94,7 @@ export function useSearchAccounts({
const [defaultAccounts, setDefaultAccounts] = useState<
ApiAccountJSON[] | null
>(null);
useEffect(() => {
if (
!currentUserId ||
@ -108,12 +108,11 @@ export function useSearchAccounts({
async function doRequest() {
setLoadingState('loading');
try {
const data = await apiRequest<ApiAccountJSON[]>(
const accounts = await apiRequest<ApiAccountJSON[]>(
'GET',
`v1/accounts/${currentUserId}/following`,
{ params: { limit: 40 } },
);
const accounts = filterResults ? data.filter(filterResults) : data;
const accountIds = accounts.map((a) => a.id);
dispatch(importFetchedAccounts(accounts));
if (withRelationships) {
@ -137,13 +136,19 @@ export function useSearchAccounts({
withDefaultFollows,
]);
const accountsToReturn =
accounts.length === 0 && withDefaultFollows
? (defaultAccounts ?? [])
: accounts;
const filteredAccounts = filterResults
? accountsToReturn.filter(filterResults)
: accountsToReturn;
return {
searchAccounts: startSearch,
resetAccounts,
accounts:
accounts.length === 0 && withDefaultFollows
? (defaultAccounts ?? [])
: accounts,
accounts: filteredAccounts,
isLoading: loadingState === 'loading',
isError: loadingState === 'error',
};

View File

@ -2,7 +2,6 @@
"about.blocks": "Gemodereerde bedieners",
"about.contact": "Kontak:",
"about.default_locale": "Verstek",
"about.disclaimer": "Mastodon is gratis oopbronsagteware en n handelsmerk van Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Rede nie beskikbaar nie",
"about.domain_blocks.preamble": "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.",
"about.domain_blocks.silenced.title": "Beperk",
@ -200,7 +199,6 @@
"lightbox.next": "Volgende",
"lightbox.previous": "Vorige",
"limited_account_hint.action": "Vertoon profiel in elk geval",
"limited_account_hint.title": "Hierdie profiel is deur moderators van {domain} versteek.",
"link_preview.author": "Deur {name}",
"lists.delete": "Verwyder lys",
"lists.edit": "Redigeer lys",

View File

@ -1,7 +1,6 @@
{
"about.blocks": "Servidors moderaus",
"about.contact": "Contacto:",
"about.disclaimer": "Mastodon ye software de codigo ubierto, y una marca comercial de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Razón no disponible",
"about.domain_blocks.preamble": "Mastodon normalment te permite veyer lo conteniu y interactuar con os usuarios de qualsequier atro servidor en o fediverso. Estas son las excepcions que s'han feito en este servidor en particular.",
"about.domain_blocks.silenced.explanation": "Normalment no veyerás perfils y conteniu d'este servidor, de no estar que lo busques explicitament u sigas bella cuenta.",
@ -264,7 +263,6 @@
"lightbox.next": "Siguient",
"lightbox.previous": "Anterior",
"limited_account_hint.action": "Amostrar perfil de totz modos",
"limited_account_hint.title": "Este perfil ha estau amagau per los moderadors de {domain}.",
"lists.delete": "Borrar lista",
"lists.edit": "Editar lista",
"lists.replies_policy.followed": "Qualsequier usuario seguiu",

View File

@ -2,7 +2,6 @@
"about.blocks": "خوادم تحت الإشراف",
"about.contact": "للاتصال:",
"about.default_locale": "افتراضي",
"about.disclaimer": "ماستدون برنامج حر ومفتوح المصدر وعلامة تجارية لـ Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "السبب غير متوفر",
"about.domain_blocks.preamble": "يتيح مَستُدون عمومًا لمستخدميه مطالعة المحتوى من المستخدمين من الخواديم الأخرى في الفدرالية والتفاعل معهم. وهذه هي الاستثناءات التي وضعت على هذا الخادوم.",
"about.domain_blocks.silenced.explanation": "لن تظهر لك ملفات التعريف الشخصية والمحتوى من هذا الخادوم، إلا إن بحثت عنه عمدًا أو تابعته.",
@ -773,7 +772,6 @@
"lightbox.zoom_in": "التكبير إلى الحجم الفعلي",
"lightbox.zoom_out": "التكبير ليناسب الحجم",
"limited_account_hint.action": "إظهار الملف التعريفي على أي حال",
"limited_account_hint.title": "تم إخفاء هذا الملف الشخصي من قبل مشرفي {domain}.",
"link_preview.author": "مِن {name}",
"link_preview.more_from_author": "المزيد من {name}",
"link_preview.shares": "{count, plural, zero {{counter} منشور}one {{counter} منشور} two {{counter} منشور} few {{counter} منشور} many {{counter} منشور} other {{counter} منشور}}",

View File

@ -1,7 +1,6 @@
{
"about.blocks": "Sirvidores moderaos",
"about.contact": "Contautu:",
"about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles y el conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",

View File

@ -2,7 +2,7 @@
"about.blocks": "Moderasiya edilmiş serverlər",
"about.contact": "Əlaqə:",
"about.default_locale": "İlkin",
"about.disclaimer": "Mastodon ödənişsiz, açıq-mənbəli yazılımdır və Mastodon gGmbH-nin əmtəə nişanıdır.",
"about.disclaimer": "Mastodon ödənişsiz, açıq-mənbəli yazılımdır və Mastodon GmbH-nin əmtəə nişanıdır.",
"about.domain_blocks.no_reason_available": "Səbəb mövcud deyil",
"about.domain_blocks.preamble": "Mastodon, adətən fediverse-dəki hər hansısa bir serverdən məzmuna baxmağınıza və istifadəçilərlə qarşılıqlı əlaqədə olmağınıza imkanı verir. Bunlar, bu serverdə edilmiş istisnalardır.",
"about.domain_blocks.silenced.explanation": "Siz bu serverdəki profilləri və məzmunu xüsusi olaraq axtarmasanız və ya izləməsəniz ümumiyyətlə görməyəcəksiniz.",
@ -86,6 +86,7 @@
"account.locked_info": "Bu hesabın məxfilik statusu kilidlənib. Sahibi, onu kimin izləyə biləcəyini manual olaraq incələyir.",
"account.media": "Media",
"account.mention": "@{name} istifadəçisini teq et",
"account.menu.add_to_collection": "Kolleksiyaya əlavə et…",
"account.menu.add_to_list": "Siyahıya əlavə et…",
"account.menu.block": "Hesabı əngəllə",
"account.menu.block_domain": "{domain} - əngəllə",
@ -102,14 +103,26 @@
"account.menu.share": "Paylaş…",
"account.menu.show_reblogs": "Zaman xəttində təkrar paylaşmaları göstər",
"account.menu.unblock": "Hesabın əngəlini götür",
"account.menu.unblock_domain": "{domain} - əngəli götür",
"account.menu.unmute": "Hesabın səsini aç",
"account.moved_to": "{name} onun yeni hesabının artıq bu olduğunu bildirdi:",
"account.mute": "@{name} istifadəçisini susdur",
"account.mute_notifications_short": "Bildirişləri səssizləşdir",
"account.mute_short": "Səssizləşdir",
"account.muted": "Səssizləşdirilib",
"account.mutual": "Bir-birinizi izləyirsiniz",
"account.name.help.domain": "{domain}, istifadəçinin profil və göndərişlərinin yerləşdiyi serverdir.",
"account.name.help.domain_self": "{domain}, profil və göndərişlərinizin yerləşdiyi serverdir.",
"account.name.help.footer": "Necə ki, fərqli e-poçt provayderlərini istifadə edən insanlara e-poçt göndərə bilirsiniz, eynilə digər Mastodon serverlərindəki insanlarla və digər Fediverse tətbiqlərindəki hər kəslə qarşılıqlı əlaqə qura bilərsiniz.",
"account.name.help.username": "{username} bu hesabın öz serverindəki istifadəçi adıdır. Başqa bir serverdəki başqa biri də eyni istifadəçi adına sahib ola bilər.",
"account.name.help.username_self": "{username} sizin bu serverdəki istifadəçi adınızdır. Başqa bir serverdəki başqa biri də eyni istifadəçi adına sahib ola bilər.",
"account.name_info": "Bu nə deməkdir?",
"account.no_bio": "Təsvir göstərilməyib.",
"account.node_modal.error_unknown": "Not saxlanıla bilmədi",
"account.node_modal.save": "Saxla",
"account.note.edit_button": "Düzəliş",
"account.open_original_page": "Orijinal səhifəni aç",
"account.pending": "Gözləmədə",
"account.posts": "Paylaşım",
"account.remove_from_followers": "{name} - izləyicilərdən çıxart",
"account.report": "@{name} istifadəçisini şikayət et",
@ -117,6 +130,8 @@
"account.share": "@{name} profilini paylaş",
"account.show_reblogs": "@{name} - təkrar paylaşımlarını göstər",
"account.statuses_counter": "{count, plural, one {{counter} paylaşım} other {{counter} paylaşım}}",
"account.timeline.pinned": "Sancıldı",
"account.timeline.pinned.view_all": "Bütün sancılmış göndərişlərə bax",
"account.unblock": "@{name} blokunu aç",
"account.unblock_domain": "{domain} domeninin blokunu aç",
"account.unblock_domain_short": "Əngəldən çıxart",
@ -126,7 +141,33 @@
"account.unmute": "@{name} səssizləşdirmədən çıxart",
"account.unmute_notifications_short": "Bildirişlərin səsini aç",
"account.unmute_short": "Səssizləşdirmədən çıxart",
"account_edit.advanced_settings.bot_label": "Avtomatlaşdırılmış hesab",
"account_edit.advanced_settings.title": "Qabaqcıl ayarlar",
"account_edit.bio.add_label": "Bio əlavə et",
"account_edit.bio.edit_label": "Bioya düzəliş",
"account_edit.bio.placeholder": "Başqalarının sizi tanımasına kömək edəcək qısa bir açıqlama əlavə edin.",
"account_edit.bio.title": "Bio",
"account_edit.bio_modal.add_title": "Bio əlavə et",
"account_edit.bio_modal.edit_title": "Bioya düzəliş",
"account_edit.column_button": "Hazırdır",
"account_edit.column_title": "Profilə düzəliş",
"account_edit.custom_fields.add_label": "Xana əlavə et",
"account_edit.custom_fields.edit_label": "Xanaya düzəliş",
"account_edit.custom_fields.placeholder": "Müraciət formalarını, xarici keçidləri və ya paylaşmaq istədiyiniz digər məlumatları əlavə edin.",
"account_edit.custom_fields.reorder_button": "Xanaları təkrar sırala",
"account_edit.custom_fields.tip_content": "Sahibi olduğunuz veb saytlara keçidləri doğrulayaraq Mastodon hesabınıza asanlıqla etibarlılıq qazandıra bilərsiniz.",
"account_edit.custom_fields.tip_title": "İpucu: Doğrulanmış keçidləri əlavə etmə",
"account_edit.custom_fields.title": "Özəl xanalar",
"account_edit.custom_fields.verified_hint": "Doğrulanmış keçid necə əlavə olunur?",
"account_edit.display_name.add_label": "Ekran adı əlavə et",
"account_edit.display_name.edit_label": "Ekran adına düzəliş",
"account_edit.display_name.placeholder": "Ekran adınız, profilinizdə və zaman xəttində adınızın görünmə formasıdır.",
"account_edit.display_name.title": "Ekran adı",
"account_edit.featured_hashtags.edit_label": "Mövzu etiketi əlavə et",
"account_edit.featured_hashtags.placeholder": "Başqalarının sevimli mövzularınızı tanımasına və onlara cəld erişməsinə kömək edin.",
"account_edit.featured_hashtags.title": "Seçilmiş mövzu etiketləri",
"account_edit.field_actions.delete": "Xananı sil",
"account_edit.field_actions.edit": "Xanaya düzəliş",
"account_edit.field_delete_modal.title": "Özəl xananı sil?",
"account_edit.field_edit_modal.add_title": "Özəl xana əlavə et",
"account_edit.field_edit_modal.discard_confirm": "İmtina",
@ -505,7 +546,6 @@
"lightbox.zoom_in": "Həqiqi ölçüyə qayıt",
"lightbox.zoom_out": "Sığacaq şəkildə yaxınlaşdır",
"limited_account_hint.action": "Yenə də profili göstər",
"limited_account_hint.title": "Bu profil, {domain} moderatorları tərəfindən gizlədildi.",
"link_preview.author": "Müəllif: {name}",
"link_preview.more_from_author": "{name} - daha çox",
"link_preview.shares": "{count, plural, one {{counter} göndəriş} other {{counter} göndəriş}}",

View File

@ -2,7 +2,7 @@
"about.blocks": "Мадэраваныя серверы",
"about.contact": "Кантакт:",
"about.default_locale": "Прадвызначаная",
"about.disclaimer": "Mastodon - свабоднае праграмнае забеспячэнне, з адкрытым зыходным кодам, і гандлёвай маркай Mastodon gGmbH.",
"about.disclaimer": "Mastodon - свабоднае праграмнае забеспячэнне, з адкрытым зыходным кодам, і гандлёвай маркай Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Прычына недаступная",
"about.domain_blocks.preamble": "Mastodon, у асноўным, дазваляе вам праглядаць кантэнт і ўзаемадзейнічаць з карыстальнікамі з іншых сервераў у федэсвету. Гэтыя выключэнні былі зроблены дакладна на гэтым серверы.",
"about.domain_blocks.silenced.explanation": "Вы не будзеце бачыць профілі і змесціва гэтага серверу, калі не шукаеце іх мэтанакіравана ці не падпісаны на карыстальнікаў адтуль.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Профіль недаступны",
"empty_column.blocks": "Вы яшчэ нікога не заблакіравалі.",
"empty_column.bookmarked_statuses": "У Вашых закладках яшчэ няма допісаў. Калі Вы дадасце закладку, яна з’явіцца тут.",
"empty_column.collections": "{acct} пакуль не стварыў(-ла) аніводнай калекцыі.",
"empty_column.collections.featured_in": "Вас пакуль не дадалі ў ніякія калекцыі.",
"empty_column.collections.featured_in_undiscoverable": "Каб людзі маглі дадаваць Вас у калекцыі, Вам трэба даць ім дазвол знаходзіць Вас у <link>Налады > Прыватнасць і пошук</link>",
"empty_column.community": "Мясцовая стужка пустая. Напішыце нешта публічнае, каб разварушыць справу!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Спалучэнні клавіш",
"keyboard_shortcuts.home": "Адкрыць хатнюю храналагічную стужку",
"keyboard_shortcuts.hotkey": "Спалучэнне клавіш",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Backspace",
"keyboard_shortcuts.keys.enter": "Enter",
"keyboard_shortcuts.keys.esc": "Esc",
"keyboard_shortcuts.keys.page_down": "Page Down",
"keyboard_shortcuts.keys.page_up": "Page Up",
"keyboard_shortcuts.legend": "Паказаць легенду",
"keyboard_shortcuts.load_more": "Навесці на кнопку \"Загрузіць болей\"",
"keyboard_shortcuts.local": "Адкрыць хатнюю храналагічную стужку",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Маштабаваць да фактычнага памеру",
"lightbox.zoom_out": "Дапасаваць усё змесціва пад памеры экрана",
"limited_account_hint.action": "Усе роўна паказваць профіль",
"limited_account_hint.title": "Гэты профіль быў схаваны мадэратарамі {domain}.",
"limited_account_hint.title": "Гэты профіль або сервер быў схаваны мадэратарамі {domain}.",
"link_preview.author": "Ад {name}",
"link_preview.more_from_author": "Больш ад {name}",
"link_preview.shares": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "карыстальнік",
"search_results.accounts": "Профілі",
"search_results.all": "Усё",
"search_results.collections": "Калекцыі",
"search_results.hashtags": "Хэштэгі",
"search_results.no_results": "Няма вынікаў.",
"search_results.no_search_yet": "Паспрабуйце пашукаць допісы, профілі або хэштэгі.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Модерирани сървъри",
"about.contact": "За контакти:",
"about.default_locale": "По подразбиране",
"about.disclaimer": "Mastodon е безплатен софтуер с отворен изходен код и търговска марка на Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Няма налична причина",
"about.domain_blocks.preamble": "Mastodon обикновено позволява да разглеждате съдържание и да взаимодействате с други потребители от всякакви сървъри във Федивселената. Има изключения, направени конкретно за този сървър.",
"about.domain_blocks.silenced.explanation": "Обикновено няма да виждате профили и съдържание, освен ако изрично не го потърсите или се включете в него, следвайки го.",
@ -464,7 +463,6 @@
"lightbox.zoom_in": "Увеличение до действителната големина",
"lightbox.zoom_out": "Увеличение до побиране",
"limited_account_hint.action": "Показване на профила въпреки това",
"limited_account_hint.title": "Този профил е бил скрит от модераторите на {domain}.",
"link_preview.author": "От {name}",
"link_preview.more_from_author": "Още от {name}",
"link_preview.shares": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",

View File

@ -1,7 +1,6 @@
{
"about.blocks": "অনুপলব্ধ সার্ভার",
"about.contact": "যোগাযোগ:",
"about.disclaimer": "ম্যাস্টোডন একটি ফ্রি, ওপেন সোর্স সফটওয়্যার এবং ম্যাস্টোডন জিজিএমবিএইচ এর একটি ট্রেডমার্ক।",
"about.domain_blocks.no_reason_available": "কারণ দর্শানো যাচ্ছে না",
"about.domain_blocks.preamble": "ম্যাস্টোডন সাধারণত আপনাকে ফেদিভার্স এ অন্য কোনও সার্ভারের ব্যবহারকারীদের থেকে সামগ্রী দেখতে এবং তাদের সাথে আলাপচারিতা করার সুযোগ দেয়। এই ব্যতিক্রম যে এই বিশেষ সার্ভারে তৈরি করা হয়েছে।",
"about.domain_blocks.silenced.explanation": "আপনি সাধারণত এই সার্ভার থেকে প্রোফাইল এবং বিষয়বস্তু দেখতে পারবেন না, যদি না আপনি নিজে থেকেই এটাকে ফলো না করেন.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Servijerioù evezhiet",
"about.contact": "Darempred :",
"about.default_locale": "Dre ziouer",
"about.disclaimer": "Mastodon zo ur meziant frank, open-source hag ur merk marilhet eus Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Abeg dihegerz",
"about.domain_blocks.preamble": "Gant Mastodon e c'hellit gwelet danvez hag eskemm gant implijerien·ezed eus forzh peseurt servijer er fedibed peurliesañ. Setu an nemedennoù a zo bet graet evit ar servijer-mañ e-unan.",
"about.domain_blocks.silenced.explanation": "Ne vo ket gwelet profiloù eus ar servijer-mañ ganeoc'h peurliesañ, nemet ma vefec'h o klask war o lec'h pe choazfec'h o heuliañ.",
@ -442,7 +441,6 @@
"lightbox.next": "Da-heul",
"lightbox.previous": "A-raok",
"limited_account_hint.action": "Diskouez ar profil memes tra",
"limited_account_hint.title": "Kuzhet eo bet ar profil-mañ gant an evezhierien eus {domain}.",
"link_preview.author": "Gant {name}",
"link_preview.more_from_author": "Muioc'h gant {name}",
"link_preview.shares": "{count, plural, one {{counter} embannadur} two {{counter} embannadur} few {{counter} embannadur} many {{counter} embannadur} other {{counter} embannadur}}",

View File

@ -2,7 +2,6 @@
"about.blocks": "Servidors moderats",
"about.contact": "Contacte:",
"about.default_locale": "Per defecte",
"about.disclaimer": "Mastodon és programari lliure de codi obert i una marca comercial de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "No es disposa del motiu",
"about.domain_blocks.preamble": "En general, Mastodon permet de veure el contingut i interaccionar amb els usuaris de qualsevol altre servidor del fedivers. Aquestes són les excepcions d'aquest servidor en particular.",
"about.domain_blocks.silenced.explanation": "Generalment no veuràs perfils ni contingut d'aquest servidor, a menys que el cerquis explícitament o optis per seguir-lo.",
@ -559,7 +558,6 @@
"lightbox.zoom_in": "Amplia fins a la mida real",
"lightbox.zoom_out": "Amplia fins a encabir",
"limited_account_hint.action": "Mostra el perfil de totes maneres",
"limited_account_hint.title": "Aquest perfil l'han amagat els moderadors de {domain}.",
"link_preview.author": "Per {name}",
"link_preview.more_from_author": "Més de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicació} other {{counter} publicacions}}",

View File

@ -1,7 +1,6 @@
{
"about.blocks": "ڕاژە سەرپەرشتیکراو",
"about.contact": "پەیوەندی کردن:",
"about.disclaimer": "ماستودۆن بە خۆڕایە، پرۆگرامێکی سەرچاوە کراوەیە، وە نیشانە بازرگانیەکەی ماستودۆن (gGmbH)ە",
"about.domain_blocks.no_reason_available": "هۆکار بەردەست نیە",
"about.domain_blocks.preamble": "ماستۆدۆن بە گشتی ڕێگەت پێدەدات بە پیشاندانی ناوەڕۆکەکان و کارلێک کردن لەگەڵ بەکارهێنەران لە هەر ڕاژەیەکی تر بە گشتی. ئەمانە ئەو بەدەرکردنانەن کە کراون لەسەر ئەم ڕاژە تایبەتە.",
"about.domain_blocks.silenced.explanation": "بە گشتی ناتوانی زانیاریە تایبەتەکان و ناوەڕۆکی ئەم ڕاژەیە ببینی، مەگەر بە ڕوونی بەدوایدا بگەڕێیت یان هەڵیبژێریت بۆ شوێنکەوتنی.",
@ -306,7 +305,6 @@
"lightbox.next": "داهاتوو",
"lightbox.previous": "پێشوو",
"limited_account_hint.action": "بەهەر حاڵ پڕۆفایلی پیشان بدە",
"limited_account_hint.title": "ئەم پرۆفایلە لەلایەن بەڕێوەبەرانی {domain} شاراوەتەوە.",
"lists.delete": "سڕینەوەی لیست",
"lists.edit": "دەستکاری لیست",
"lists.replies_policy.followed": "هەر بەکارهێنەرێکی بەدواکەوتوو",

View File

@ -2,7 +2,6 @@
"about.blocks": "Moderované servery",
"about.contact": "Kontakt:",
"about.default_locale": "Výchozí",
"about.disclaimer": "Mastodon je svobodný software s otevřeným zdrojovým kódem a ochranná známka společnosti Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Důvod není k dispozici",
"about.domain_blocks.preamble": "Mastodon umožňuje prohlížet obsah a komunikovat s uživateli jakéhokoliv serveru ve fediversu. Pro tento konkrétní server se vztahují následující výjimky.",
"about.domain_blocks.silenced.explanation": "Uživatele a obsah tohoto serveru neuvidíte, pokud je nebudete výslovně hledat nebo je nezačnete sledovat.",
@ -619,7 +618,6 @@
"lightbox.zoom_in": "Přiblížit na skutečnou velikost",
"lightbox.zoom_out": "Přizpůsobit velikost",
"limited_account_hint.action": "Přesto profil zobrazit",
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
"link_preview.author": "Od {name}",
"link_preview.more_from_author": "Více od {name}",
"link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",

View File

@ -2,7 +2,6 @@
"about.blocks": "Gweinyddion wedi'u cymedroli",
"about.contact": "Cysylltiad:",
"about.default_locale": "Rhagosodedig",
"about.disclaimer": "Mae Mastodon yn feddalwedd cod agored rhydd ac o dan hawlfraint Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Dyw'r rheswm ddim ar gael",
"about.domain_blocks.preamble": "Fel rheol, mae Mastodon yn caniatáu i chi weld cynnwys gan unrhyw weinyddwr arall yn y ffedysawd a rhyngweithio â hi. Dyma'r eithriadau a wnaed ar y gweinydd penodol hwn.",
"about.domain_blocks.silenced.explanation": "Fel rheol, fyddwch chi ddim yn gweld proffiliau a chynnwys o'r gweinydd hwn, oni bai eich bod yn chwilio'n benodol amdano neu yn ymuno drwy ei ddilyn.",
@ -840,7 +839,6 @@
"lightbox.zoom_in": "Chwyddo i faint gwirioneddol",
"lightbox.zoom_out": "Chwyddo i ffitio",
"limited_account_hint.action": "Dangos y proffil beth bynnag",
"limited_account_hint.title": "Mae'r proffil hwn wedi cael ei guddio gan gymedrolwyr {domain}.",
"link_preview.author": "Gan {name}",
"link_preview.more_from_author": "Mwy gan {name}",
"link_preview.shares": "{count, plural, one {{counter} postiad } two {{counter} bostiad } few {{counter} postiad} many {{counter} postiad} other {{counter} postiad}}",

View File

@ -2,7 +2,7 @@
"about.blocks": "Modererede servere",
"about.contact": "Kontakt:",
"about.default_locale": "Standard",
"about.disclaimer": "Mastodon er gratis, open-source software og et varemærke tilhørende Mastodon gGmbH.",
"about.disclaimer": "Mastodon er gratis, open-source software og et varemærke tilhørende Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Begrundelse ikke tilgængelig",
"about.domain_blocks.preamble": "Mastodon tillader generelt, at du ser indhold og interagere med brugere fra enhver anden server i fediverset. Disse er undtagelserne, som er implementeret på netop denne server.",
"about.domain_blocks.silenced.explanation": "Du vil generelt ikke se profiler og indhold fra denne server, medmindre du udtrykkeligt slår den op eller vælger den ved at følge.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Profil utilgængelig",
"empty_column.blocks": "Du har ikke blokeret nogle brugere endnu.",
"empty_column.bookmarked_statuses": "Du har ingen bogmærkede indlæg endnu. Når du bogmærker ét, vil det dukke op hér.",
"empty_column.collections": "{acct} har endnu ikke oprettet nogen samlinger.",
"empty_column.collections.featured_in": "Du er ikke blevet tilføjet til nogen samlinger endnu.",
"empty_column.collections.featured_in_undiscoverable": "For at andre kan føje dig til samlinger, skal du give tilladelse til at blive vist i opdagelsesfunktioner under <link>Præferencer > Fortrolighed og rækkevidde</link>",
"empty_column.community": "Den lokale tidslinje er tom. Skriv noget offentligt for at sætte tingene i gang!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Tastaturgenveje",
"keyboard_shortcuts.home": "Åbn hjem-tidslinje",
"keyboard_shortcuts.hotkey": "Hurtigtast",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Backspace",
"keyboard_shortcuts.keys.enter": "Enter",
"keyboard_shortcuts.keys.esc": "Esc",
"keyboard_shortcuts.keys.page_down": "Side ned",
"keyboard_shortcuts.keys.page_up": "Side op",
"keyboard_shortcuts.legend": "Vis dette symbol",
"keyboard_shortcuts.load_more": "Fokusér knappen \"Indlæs flere\"",
"keyboard_shortcuts.local": "Åbn lokal tidslinje",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Zoom til faktisk størrelse",
"lightbox.zoom_out": "Zoom for at tilpasse",
"limited_account_hint.action": "Vis profil alligevel",
"limited_account_hint.title": "Denne profil er blevet skjult af {domain}-moderatorerne.",
"limited_account_hint.title": "Denne profil eller server er blevet skjult af moderatorerne på {domain}.",
"link_preview.author": "Af {name}",
"link_preview.more_from_author": "Mere fra {name}",
"link_preview.shares": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "bruger",
"search_results.accounts": "Profiler",
"search_results.all": "Alle",
"search_results.collections": "Samlinger",
"search_results.hashtags": "Hashtags",
"search_results.no_results": "Ingen resultater.",
"search_results.no_search_yet": "Prøv at søge efter indlæg, profiler eller hashtags.",

View File

@ -2,7 +2,7 @@
"about.blocks": "Eingeschränkte Server",
"about.contact": "Kontakt:",
"about.default_locale": "Standard",
"about.disclaimer": "Mastodon ist eine freie, quelloffene Software und eine Marke der Mastodon gGmbH.",
"about.disclaimer": "Mastodon ist eine freie, quelloffene Software und eine Marke der Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Keinen Grund angegeben",
"about.domain_blocks.preamble": "Mastodon erlaubt es dir grundsätzlich, alle Inhalte von allen Nutzer*innen auf allen Servern im Fediverse zu sehen und mit ihnen zu interagieren. Für diesen Server gibt es aber ein paar Ausnahmen.",
"about.domain_blocks.silenced.explanation": "Standardmäßig werden von diesem Server keine Inhalte oder Profile angezeigt. Du kannst die Profile und Inhalte aber dennoch sehen, wenn du explizit nach diesen suchst oder diesen folgst.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Profil nicht verfügbar",
"empty_column.blocks": "Du hast bisher keine Profile blockiert.",
"empty_column.bookmarked_statuses": "Du hast bisher keine Beiträge als Lesezeichen abgelegt. Sobald du einen Beitrag als Lesezeichen speicherst, wird er hier erscheinen.",
"empty_column.collections": "{acct} hat noch keine Sammlungen erstellt.",
"empty_column.collections.featured_in": "Du wurdest noch keiner Sammlung hinzugefügt.",
"empty_column.collections.featured_in_undiscoverable": "Damit du zu Sammlungen hinzugefügt werden kannst, muss „Mich beim Entdecken berücksichtigen“ unter <link>Einstellungen > Datenschutz und Reichweite</link> aktiviert werden",
"empty_column.community": "Die lokale Timeline ist leer. Schreibe einen öffentlichen Beitrag, um den Stein ins Rollen zu bringen!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Tastenkürzel",
"keyboard_shortcuts.home": "Startseite öffnen",
"keyboard_shortcuts.hotkey": "Tastenkürzel",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Backspace",
"keyboard_shortcuts.keys.enter": "Eingabetaste",
"keyboard_shortcuts.keys.esc": "Esc",
"keyboard_shortcuts.keys.page_down": "Bild ab",
"keyboard_shortcuts.keys.page_up": "Bild auf",
"keyboard_shortcuts.legend": "Tastenkürzel anzeigen (diese Seite)",
"keyboard_shortcuts.load_more": "Schaltfläche „Mehr laden“ fokussieren",
"keyboard_shortcuts.local": "Lokale Timeline öffnen",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "In Originalgröße anzeigen",
"lightbox.zoom_out": "In angepasster Größe anzeigen",
"limited_account_hint.action": "Profil trotzdem anzeigen",
"limited_account_hint.title": "Dieses Profil wurde von den Moderator*innen von {domain} ausgeblendet.",
"limited_account_hint.title": "Dieses Profil oder dieser Server wurde von den Moderator*innen von {domain} ausgeblendet.",
"link_preview.author": "Von {name}",
"link_preview.more_from_author": "Mehr von {name}",
"link_preview.shares": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "Profil",
"search_results.accounts": "Profile",
"search_results.all": "Alles",
"search_results.collections": "Sammlungen",
"search_results.hashtags": "Hashtags",
"search_results.no_results": "Keine Ergebnisse.",
"search_results.no_search_yet": "Suche nach Beiträgen, Profilen oder Hashtags.",

View File

@ -2,7 +2,7 @@
"about.blocks": "Συντονιζόμενοι διακομιστές",
"about.contact": "Επικοινωνία:",
"about.default_locale": "Προεπιλογή",
"about.disclaimer": "Το Mastodon είναι ελεύθερο λογισμικό ανοιχτού κώδικα και εμπορικό σήμα της Mastodon gGmbH.",
"about.disclaimer": "Το Mastodon είναι ελεύθερο λογισμικό ανοιχτού κώδικα και εμπορικό σήμα της Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Αιτιολογία μη διαθέσιμη",
"about.domain_blocks.preamble": "Σε γενικές γραμμές το Mastodon σου επιτρέπει να βλέπεις περιεχόμενο και να αλληλεπιδράς με χρήστες από οποιονδήποτε άλλο διακομιστή σε ένα διασυνδεδεμένο σύμπαν διακομιστών (fediverse). Ακολουθούν οι εξαιρέσεις που ισχύουν για τον συγκεκριμένο διακομιστή.",
"about.domain_blocks.silenced.explanation": "Συνήθως δε θα βλέπεις προφίλ και περιεχόμενο απ' αυτόν τον διακομιστή, εκτός αν κάνεις συγκεκριμένη αναζήτηση ή επιλέξεις να τον ακολουθήσεις.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Μη διαθέσιμο προφίλ",
"empty_column.blocks": "Δεν έχεις αποκλείσει κανέναν χρήστη ακόμη.",
"empty_column.bookmarked_statuses": "Δεν έχεις καμία ανάρτηση με σελιδοδείκτη ακόμη. Μόλις βάλεις κάποιον, θα εμφανιστεί εδώ.",
"empty_column.collections": "Ο χρήστης {acct} δεν έχει δημιουργήσει ακόμη καμία συλλογή.",
"empty_column.collections.featured_in": "Δεν έχετε προστεθεί ακόμη σε καμία συλλογή.",
"empty_column.collections.featured_in_undiscoverable": "Προκειμένου ο κόσμος να σας προσθέσει σε συλλογές, πρέπει να επιτρέψετε την ανάδειξή σας σε εμπειρίες ανακάλυψης από τις <link>Προτιμήσεις > Ιδιωτικότητα και προσιτότητα</link>",
"empty_column.community": "Η τοπική ροή είναι κενή. Γράψε κάτι δημόσια για να αρχίσει να κυλά η μπάλα!",
@ -760,10 +761,10 @@
"hashtag.counter_by_uses": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}} σήμερα",
"hashtag.feature": "Ανάδειξη στο προφίλ",
"hashtag.follow": "Παρακολούθηση ετικέτας",
"hashtag.follow": "Ακολούθηση ετικέτας",
"hashtag.mute": "Σίγαση #{hashtag}",
"hashtag.unfeature": "Να μην αναδεικνύεται στο προφίλ",
"hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας",
"hashtag.unfollow": "Άρση ακολούθησης ετικέτας",
"hashtags.and_other": "…και {count, plural, other {# ακόμα}}",
"hints.profiles.followers_may_be_missing": "Μπορεί να λείπουν ακόλουθοι για αυτό το προφίλ.",
"hints.profiles.follows_may_be_missing": "Άτομα που ακολουθούνται μπορεί να λείπουν απ' αυτό το προφίλ.",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Συντομεύσεις πληκτρολογίου",
"keyboard_shortcuts.home": "Άνοιγμα ροής αρχικής σελίδας",
"keyboard_shortcuts.hotkey": "Συντόμευση",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Backspace",
"keyboard_shortcuts.keys.enter": "Enter",
"keyboard_shortcuts.keys.esc": "Esc",
"keyboard_shortcuts.keys.page_down": "Page Down",
"keyboard_shortcuts.keys.page_up": "Page Up",
"keyboard_shortcuts.legend": "Εμφάνιση αυτού του οδηγού",
"keyboard_shortcuts.load_more": "Εστίαση στο κουμπί \"Φόρτωση περισσότερων\"",
"keyboard_shortcuts.local": "Άνοιγμα τοπικής ροής",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Εστίαση στο πραγματικό μέγεθος",
"lightbox.zoom_out": "Εστίαση για προσαρμογή",
"limited_account_hint.action": "Εμφάνιση προφίλ ούτως ή άλλως",
"limited_account_hint.title": "Αυτό το προφίλ έχει αποκρυφτεί από τους διαχειριστές του διακομιστή {domain}.",
"limited_account_hint.title": "Αυτό το προφίλ ή διακομιστής έχει κρυφτεί από τους διαχειριστές του {domain}.",
"link_preview.author": "Από {name}",
"link_preview.more_from_author": "Περισσότερα από {name}",
"link_preview.shares": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "χρήστης",
"search_results.accounts": "Προφίλ",
"search_results.all": "Όλα",
"search_results.collections": "Συλλογές",
"search_results.hashtags": "Ετικέτες",
"search_results.no_results": "Κανένα αποτέλεσμα.",
"search_results.no_search_yet": "Δοκίμασε να ψάξεις για αναρτήσεις, προφίλ ή ετικέτες.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Moderated servers",
"about.contact": "Contact:",
"about.default_locale": "Default",
"about.disclaimer": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Reason not available",
"about.domain_blocks.preamble": "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.",
"about.domain_blocks.silenced.explanation": "You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.",
@ -833,7 +832,6 @@
"lightbox.zoom_in": "Zoom to actual size",
"lightbox.zoom_out": "Zoom to fit",
"limited_account_hint.action": "Show profile anyway",
"limited_account_hint.title": "This profile or server has been hidden by the moderators of {domain}.",
"link_preview.author": "By {name}",
"link_preview.more_from_author": "More from {name}",
"link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}",

View File

@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "You haven't blocked any users yet.",
"empty_column.bookmarked_statuses": "You don't have any bookmarked posts yet. When you bookmark one, it will show up here.",
"empty_column.collections": "{acct} has not created any collections yet.",
"empty_column.collections.featured_in": "You have not been added to any collections yet.",
"empty_column.collections.featured_in_undiscoverable": "In order for people to add you to collections, you need to allow featuring in discovery experiences from <link>Preferences > Privacy and reach</link>",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
@ -1198,6 +1199,7 @@
"search_popout.user": "user",
"search_results.accounts": "Profiles",
"search_results.all": "All",
"search_results.collections": "Collections",
"search_results.hashtags": "Hashtags",
"search_results.no_results": "No results.",
"search_results.no_search_yet": "Try searching for posts, profiles or hashtags.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Reguligitaj serviloj",
"about.contact": "Kontakto:",
"about.default_locale": "Defaŭlta",
"about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Kialo ne disponeblas",
"about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.",
"about.domain_blocks.silenced.explanation": "Vi ne ĝenerale vidos profilojn kaj enhavojn de ĉi tiu servilo, krom se vi eksplice trovas aŭ estas permesita de via sekvato.",
@ -478,7 +477,6 @@
"lightbox.zoom_in": "Zomi al reala grandeco",
"lightbox.zoom_out": "Zomi por konveni",
"limited_account_hint.action": "Montru profilon ĉiukaze",
"limited_account_hint.title": "La profilo estas kaŝita de la moderigantoj de {domain}.",
"link_preview.author": "De {name}",
"link_preview.more_from_author": "Pli de {name}",
"link_preview.shares": "{count, plural, one {{counter} afiŝo} other {{counter} afiŝoj}}",

View File

@ -2,7 +2,7 @@
"about.blocks": "Servidores moderados",
"about.contact": "Contacto:",
"about.default_locale": "Predeterminado",
"about.disclaimer": "Mastodon es software libre y de código abierto y una marca comercial de Mastodon gGmbH.",
"about.disclaimer": "Mastodon es software libre y de código abierto, y una marca comercial de Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Motivo no disponible",
"about.domain_blocks.preamble": "Mastodon normalmente te permite ver el contenido e interactuar con los usuarios de cualquier otro servidor en el fediverso. Estas son las excepciones que se han hecho en este servidor en particular.",
"about.domain_blocks.silenced.explanation": "Normalmente no verás perfiles y contenido de este servidor, a menos que lo busqués explícitamente o sigás alguna cuenta.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Perfil no disponible",
"empty_column.blocks": "Todavía no bloqueaste a ningún usuario.",
"empty_column.bookmarked_statuses": "Todavía no tenés mensajes guardados en \"Marcadores\". Cuando guardés uno en \"Marcadores\", se mostrará acá.",
"empty_column.collections": "{acct} todavía no creó ninguna colección.",
"empty_column.collections.featured_in": "Todavía no te agregaron a ninguna colección.",
"empty_column.collections.featured_in_undiscoverable": "Para que la gente pueda agregarte a colecciones, tenés que permitir que te destacen en las experiencias de descubrimiento en <link>Configuración > Privacidad y alcance</link>",
"empty_column.community": "La línea temporal local está vacía. ¡Escribí algo en modo público para que se empiece a correr la bola!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Atajos de teclado",
"keyboard_shortcuts.home": "Abrir línea temporal principal",
"keyboard_shortcuts.hotkey": "Atajo",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "⌫ Retroceso",
"keyboard_shortcuts.keys.enter": "↳ Intro",
"keyboard_shortcuts.keys.esc": "Escape",
"keyboard_shortcuts.keys.page_down": "Re Pág",
"keyboard_shortcuts.keys.page_up": "Av Pág",
"keyboard_shortcuts.legend": "Mostrar este texto",
"keyboard_shortcuts.load_more": "Focalizar el botón «Cargar más»",
"keyboard_shortcuts.local": "Abrirlínea temporal local",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Ampliar al tamaño real",
"lightbox.zoom_out": "Ampliar hasta ajustar",
"limited_account_hint.action": "Mostrar perfil de todos modos",
"limited_account_hint.title": "Este perfil fue ocultado por los moderadores de {domain}.",
"limited_account_hint.title": "Este perfil del servidor fue ocultado por los moderadores de {domain}.",
"link_preview.author": "Por {name}",
"link_preview.more_from_author": "Más de {name}",
"link_preview.shares": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "usuario",
"search_results.accounts": "Perfiles",
"search_results.all": "Todos",
"search_results.collections": "Colecciones",
"search_results.hashtags": "Etiquetas",
"search_results.no_results": "Sin resultados.",
"search_results.no_search_yet": "Intentá buscar publicaciones, perfiles o etiquetas.",

View File

@ -2,7 +2,7 @@
"about.blocks": "Servidores moderados",
"about.contact": "Contacto:",
"about.default_locale": "Por defecto",
"about.disclaimer": "Mastodon es software libre de código abierto, y una marca comercial de Mastodon gGmbH.",
"about.disclaimer": "Mastodon es un software libre, de código abierto, y una marca comercial de Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Motivo no disponible",
"about.domain_blocks.preamble": "Mastodon generalmente te permite ver contenido e interactuar con usuarios de cualquier otro servidor del fediverso. Estas son las excepciones que se han hecho en este servidor en particular.",
"about.domain_blocks.silenced.explanation": "Por lo general, no verás perfiles ni contenidos de este servidor, a menos que los busques explícitamente o que optes por seguirlo.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Perfil no disponible",
"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": "{acct} aún no ha creado ninguna colección.",
"empty_column.collections.featured_in": "Aún no te han añadido a ninguna colección.",
"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!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Atajos de teclado",
"keyboard_shortcuts.home": "Abrir cronología principal",
"keyboard_shortcuts.hotkey": "Tecla de acceso rápido",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Retroceso",
"keyboard_shortcuts.keys.enter": "Enter",
"keyboard_shortcuts.keys.esc": "Escape",
"keyboard_shortcuts.keys.page_down": "Re Pág",
"keyboard_shortcuts.keys.page_up": "Av Pág",
"keyboard_shortcuts.legend": "Mostrar esta leyenda",
"keyboard_shortcuts.load_more": "Enfoque en el botón \"Cargar más\"",
"keyboard_shortcuts.local": "Abrir cronología local",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Ampliar al tamaño real",
"lightbox.zoom_out": "Ampliar para ajustar",
"limited_account_hint.action": "Mostrar perfil de todos modos",
"limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.",
"limited_account_hint.title": "Este perfil o servidor ha sido ocultado por los moderadores de {domain}.",
"link_preview.author": "Por {name}",
"link_preview.more_from_author": "Más de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "usuario",
"search_results.accounts": "Perfiles",
"search_results.all": "Todos",
"search_results.collections": "Colecciones",
"search_results.hashtags": "Etiquetas",
"search_results.no_results": "No hay resultados.",
"search_results.no_search_yet": "Intenta buscar publicaciones, perfiles o etiquetas.",

View File

@ -2,7 +2,7 @@
"about.blocks": "Servidores moderados",
"about.contact": "Contacto:",
"about.default_locale": "Por defecto",
"about.disclaimer": "Mastodon es software libre, de código abierto, y una marca comercial de Mastodon gGmbH.",
"about.disclaimer": "Mastodon es un software libre, de código abierto, y una marca comercial de Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Razón no disponible",
"about.domain_blocks.preamble": "Mastodon normalmente te permite ver el contenido e interactuar con los usuarios de cualquier otro servidor en el fediverso. Estas son las excepciones que se han hecho en este servidor en particular.",
"about.domain_blocks.silenced.explanation": "Normalmente no verás perfiles y contenido de este servidor, a menos que lo busques explícitamente o sigas alguna cuenta.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Perfil no disponible",
"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": "{acct} aún no ha creado ninguna colección.",
"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.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "Abrir cronología principal",
"keyboard_shortcuts.hotkey": "Tecla rápida",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Retroceso",
"keyboard_shortcuts.keys.enter": "Enter",
"keyboard_shortcuts.keys.esc": "Escape",
"keyboard_shortcuts.keys.page_down": "Re Pág",
"keyboard_shortcuts.keys.page_up": "Av Pág",
"keyboard_shortcuts.legend": "Mostrar esta leyenda",
"keyboard_shortcuts.load_more": "Poner el foco en el botón \"Cargar más\"",
"keyboard_shortcuts.local": "Abrir cronología local",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Ampliar al tamaño real",
"lightbox.zoom_out": "Ampliar para ajustar",
"limited_account_hint.action": "Mostrar perfil de todos modos",
"limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.",
"limited_account_hint.title": "Los moderadores de {domain} han ocultado este perfil o servidor.",
"link_preview.author": "Por {name}",
"link_preview.more_from_author": "Más de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "usuario",
"search_results.accounts": "Perfiles",
"search_results.all": "Todos",
"search_results.collections": "Colecciones",
"search_results.hashtags": "Etiquetas",
"search_results.no_results": "Sin resultados.",
"search_results.no_search_yet": "Intenta buscar publicaciones, perfiles o etiquetas.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Modereeritavad serverid",
"about.contact": "Kontakt:",
"about.default_locale": "Vaikimisi",
"about.disclaimer": "Mastodon on vaba, tasuta ja avatud lähtekoodiga tarkvara ning Mastodon gGmbH kaubamärk.",
"about.domain_blocks.no_reason_available": "Põhjus on teadmata",
"about.domain_blocks.preamble": "Mastodon lubab üldiselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest födiversumi serverist. Need on erandid, mis kehtivad selles kindlas serveris.",
"about.domain_blocks.silenced.explanation": "Sa üldjuhul ei näe profiile ja sisu sellest serverist, kui sa just tahtlikult neid ei otsi või jälgimise moel nõusolekut ei anna.",
@ -850,7 +849,7 @@
"lightbox.zoom_in": "Näita algsuuruses",
"lightbox.zoom_out": "Näita kõik",
"limited_account_hint.action": "Näita profilli sellegipoolest",
"limited_account_hint.title": "See profiil on peidetud {domain} moderaatorite poolt.",
"limited_account_hint.title": "See profiil või server on {domain}-i moderaatorite poolt peidetud.",
"link_preview.author": "{name} poolt",
"link_preview.more_from_author": "Veel kasutajalt {name}",
"link_preview.shares": "{count, plural, one {{counter} postitus} other {{counter} postitust}}",

View File

@ -2,7 +2,6 @@
"about.blocks": "Moderatutako zerbitzariak",
"about.contact": "Kontaktua:",
"about.default_locale": "Lehenetsia",
"about.disclaimer": "Mastodon software libre eta kode irekikoa da, eta Mastodon gGmbH-ren marka erregistratua.",
"about.domain_blocks.no_reason_available": "Arrazoia ez dago eskuragarri",
"about.domain_blocks.preamble": "Mastodonek orokorrean aukera ematen dizu fedibertsoko beste zerbitzarietako erabiltzaileen edukia ikusi eta haiekin komunikatzeko. Zerbitzari zehatz honi ezarritako salbuespenak hauek dira.",
"about.domain_blocks.silenced.explanation": "Orokorrean ez duzu zerbitzari honetako profil eta edukirik ikusiko. Profilak jarraitzen badituzu edo edukia esplizituki bilatzen baduzu bai.",
@ -527,7 +526,6 @@
"lightbox.zoom_in": "Zooma egungo tamainara",
"lightbox.zoom_out": "Zooma egokitzeko",
"limited_account_hint.action": "Erakutsi profila hala ere",
"limited_account_hint.title": "Profil hau ezkutatu egin dute {domain} zerbitzariko moderatzaileek.",
"link_preview.author": "Egilea: {name}",
"link_preview.more_from_author": "{name} erabiltzaileaz gehiago jakin",
"link_preview.shares": "{count, plural, one {{counter} bidalketa} other {{counter} bidalketa}}",

View File

@ -2,7 +2,6 @@
"about.blocks": "کارسازهای نظارت شده",
"about.contact": "تماس:",
"about.default_locale": "پیش‌گزیده",
"about.disclaimer": "ماستودون نرم‌افزار آزاد و نشان تجاری یک شرکت غیر انتفاعی با مسئولیت محدود آلمانی است.",
"about.domain_blocks.no_reason_available": "دلیلی موجود نیست",
"about.domain_blocks.preamble": "ماستودون عموماً می‌گذارد محتوا را از هر کارساز دیگری در دنیای شبکه‌های اجتماعی غیرمتمرکز دیده و با آنان برهم‌کنش داشته باشید. این‌ها استثناهایی هستند که روی این کارساز خاص وضع شده‌اند.",
"about.domain_blocks.silenced.explanation": "عموماً نمایه‌ها و محتوا از این کارساز را نمی‌بینید، مگر این که به طور خاص دنبالشان گشته یا با پی گیری، داوطلب دیدنشان شوید.",
@ -833,7 +832,6 @@
"lightbox.zoom_in": "بزرگ‌نمایی به اندازهٔ اصلی",
"lightbox.zoom_out": "بزرگ نمایی برای برازش",
"limited_account_hint.action": "به هر روی نمایه نشان داده شود",
"limited_account_hint.title": "این نمایه از سوی ناظم‌های {domain} پنهان شده.",
"link_preview.author": "از {name}",
"link_preview.more_from_author": "بیش‌تر از {name}",
"link_preview.shares": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",

View File

@ -2,7 +2,7 @@
"about.blocks": "Moderoidut palvelimet",
"about.contact": "Yhteydenotto:",
"about.default_locale": "Oletus",
"about.disclaimer": "Mastodon on vapaa avoimen lähdekoodin ohjelmisto ja Mastodon gGmbH:n tavaramerkki.",
"about.disclaimer": "Mastodon on vapaa avoimen lähdekoodin ohjelmisto ja Mastodon GmbH:n tavaramerkki.",
"about.domain_blocks.no_reason_available": "Syy ei ole tiedossa",
"about.domain_blocks.preamble": "Mastodonin avulla voi yleensä tarkastella minkä tahansa fediversumiin kuuluvan palvelimen sisältöä ja olla yhteyksissä eri palvelinten käyttäjien kanssa. Nämä poikkeukset koskevat yksin tätä palvelinta.",
"about.domain_blocks.silenced.explanation": "Et yleensä näe tämän palvelimen profiileja ja sisältöä, jollet erityisesti etsi juuri sitä tai liity siihen seuraamalla.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Profiili ei saatavilla",
"empty_column.blocks": "Et ole vielä estänyt käyttäjiä.",
"empty_column.bookmarked_statuses": "Et ole vielä lisännyt julkaisuja kirjanmerkkeihisi. Kun lisäät yhden, se näkyy tässä.",
"empty_column.collections": "{acct} ei ole luonut vielä yhtään kokoelmaa.",
"empty_column.collections.featured_in": "Sinua ei ole vielä lisätty mihinkään kokoelmaan.",
"empty_column.collections.featured_in_undiscoverable": "Jotta sinut voi lisätä kokoelmiin, esittely löydettävyyskokemuksissa on sallittava kohdassa <link>Asetukset > Yksityisyys ja tavoittavuus</link>",
"empty_column.community": "Paikallinen aikajana on tyhjä. Kirjoita jotain julkista, niin homma lähtee käyntiin!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Pikanäppäimet",
"keyboard_shortcuts.home": "Avaa kotiaikajana",
"keyboard_shortcuts.hotkey": "Pikanäppäin",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Askelpalautin",
"keyboard_shortcuts.keys.enter": "Enter",
"keyboard_shortcuts.keys.esc": "Esc",
"keyboard_shortcuts.keys.page_down": "Page Down",
"keyboard_shortcuts.keys.page_up": "Page Up",
"keyboard_shortcuts.legend": "Näytä tämä ohje",
"keyboard_shortcuts.load_more": "Kohdista ”Lataa lisää” -painikkeeseen",
"keyboard_shortcuts.local": "Avaa paikallinen aikajana",
@ -1191,6 +1198,7 @@
"search_popout.user": "käyttäjä",
"search_results.accounts": "Profiilit",
"search_results.all": "Kaikki",
"search_results.collections": "Kokoelmat",
"search_results.hashtags": "Aihetunnisteet",
"search_results.no_results": "Ei tuloksia.",
"search_results.no_search_yet": "Kokeile hakea julkaisuja, profiileja tai aihetunnisteita.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Mga pinatimping server",
"about.contact": "Kontak:",
"about.default_locale": "Default",
"about.disclaimer": "Ang Mastodon ay software na malaya at bukas-na-pinagmulan, at isang tatak-pangkalakal ng Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Hindi makuha ang dahilan",
"about.domain_blocks.preamble": "Sa kadalasan, hinahayaan ka ng Mastodon na makita ang mga content sa, at makipag-interact sa users ng, ibang servers sa fediverse. Narito ang exceptions na ginawa sa partikular na server na ito.",
"about.domain_blocks.silenced.explanation": "Sa kadalasan, hindi mo makikita ang profiles at content mula sa server na ito, maliban na lang kung sasadyain mo silang hanapin o piliing magawa ito sa pamamagitan ng mga sumusunod.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Tálmaðir ambætarar",
"about.contact": "Samband:",
"about.default_locale": "Sjálvvirði",
"about.disclaimer": "Mastodon er fríur ritbúnaður við opnari keldu og eitt vørumerki hjá Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Ókent orsøk",
"about.domain_blocks.preamble": "Yvirhøvur, so loyvir Mastodon tær at síggja innihald frá og at samvirka við brúkarar frá ein og hvørjum ambætara í fediverse. Undantøkini, sum eru gjørd á júst hesum ambætaranum, eru hesi.",
"about.domain_blocks.silenced.explanation": "Yvirhøvur, so sært tú ikki vangar og innihald frá hesum ambætaranum, uttan so at tú skilliga leitar hesi upp ella velur tey við at fylgja teimum.",
@ -662,7 +661,6 @@
"lightbox.zoom_in": "Suma til veruliga stødd",
"lightbox.zoom_out": "Suma, so tað passar",
"limited_account_hint.action": "Vís vangamynd kortini",
"limited_account_hint.title": "Hesin vangin er fjaldur av kjakleiðarunum á {domain}.",
"link_preview.author": "Av {name}",
"link_preview.more_from_author": "Meira frá {name}",
"link_preview.shares": "{count, plural, one {{counter} postur} other {{counter} postar}}",

View File

@ -2,7 +2,7 @@
"about.blocks": "Serveurs modérés",
"about.contact": "Contact :",
"about.default_locale": "Par défaut",
"about.disclaimer": "Mastodon est un logiciel open-source gratuit et une marque déposée de Mastodon gGmbH.",
"about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Raison non disponible",
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec des comptes de n'importe quel serveur dans le fediverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.",
"about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas de profils ou de contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Profil non disponible",
"empty_column.blocks": "Vous navez bloqué aucun compte pour le moment.",
"empty_column.bookmarked_statuses": "Vous n'avez pas de publications parmi vos signets. Lorsque vous en ajouterez une, elle apparaîtra ici.",
"empty_column.collections": "{acct} n'a pas encore créé de collection.",
"empty_column.collections.featured_in": "Vous n'avez pas encore été ajouté·e à une collection.",
"empty_column.collections.featured_in_undiscoverable": "Afin que l'on puisse vous ajouter à des collections, vous devez autoriser d'apparaître dans les expériences de découverte depuis <link>Vie privée et visibilité</link>",
"empty_column.community": "Le fil local est vide. Écrivez donc quelque chose pour le remplir!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Raccourcis clavier",
"keyboard_shortcuts.home": "Ouvrir le fil daccueil",
"keyboard_shortcuts.hotkey": "Raccourci clavier",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Retour arrière",
"keyboard_shortcuts.keys.enter": "Entrée",
"keyboard_shortcuts.keys.esc": "Échap",
"keyboard_shortcuts.keys.page_down": "Page bas",
"keyboard_shortcuts.keys.page_up": "Page haut",
"keyboard_shortcuts.legend": "Afficher cette légende",
"keyboard_shortcuts.load_more": "Bouton Focus \"Charger plus\"",
"keyboard_shortcuts.local": "Ouvrir le fil local",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Zoomer sur la taille réelle",
"lightbox.zoom_out": "Zoomer pour adapter",
"limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"limited_account_hint.title": "Ce profil ou serveur a été masqué par l'équipe de modération de {domain}.",
"link_preview.author": "Par {name}",
"link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "utilisateur·ice",
"search_results.accounts": "Profils",
"search_results.all": "Tout",
"search_results.collections": "Collections",
"search_results.hashtags": "Hashtags",
"search_results.no_results": "Aucun résultat.",
"search_results.no_search_yet": "Essayez de rechercher des messages, des profils ou des hashtags.",
@ -1333,12 +1341,12 @@
"upload_button.label": "Ajouter des images, une vidéo ou un fichier audio",
"upload_error.limit": "Taille maximale d'envoi de fichier dépassée.",
"upload_error.poll": "Lenvoi de fichiers nest pas autorisé avec les sondages.",
"upload_error.quote": "Lenvoi de fichiers nest pas autorisé avec les sondages.",
"upload_form.drag_and_drop.instructions": "Pour choisir un média joint, appuyez sur la touche espace ou entrée. Tout en faisant glisser, utilisez les touches fléchées pour déplacer le fichier média dans une direction donnée. Appuyez à nouveau sur la touche espace ou entrée pour déposer le fichier média dans sa nouvelle position, ou appuyez sur la touche Echap pour annuler.",
"upload_form.drag_and_drop.on_drag_cancel": "Le glissement a été annulé. La pièce jointe {item} n'a pas été ajoutée.",
"upload_form.drag_and_drop.on_drag_end": "La pièce jointe du média {item} a été déplacée.",
"upload_error.quote": "Lenvoi de fichiers nest pas autorisé avec les citations.",
"upload_form.drag_and_drop.instructions": "Pour choisir un média joint, appuyez sur la touche espace ou entrée. Pour glisser, utilisez les touches fléchées pour déplacer le fichier média dans une direction donnée. Appuyez à nouveau sur la touche espace ou entrée pour déposer le fichier média dans sa nouvelle position, ou appuyez sur la touche Echap pour annuler.",
"upload_form.drag_and_drop.on_drag_cancel": "Déplacement annulé. La pièce jointe {item} n'a pas été ajoutée.",
"upload_form.drag_and_drop.on_drag_end": "La pièce jointe du média {item} a été déposée.",
"upload_form.drag_and_drop.on_drag_over": "La pièce jointe du média {item} a été déplacée.",
"upload_form.drag_and_drop.on_drag_start": "A récupéré la pièce jointe {item}.",
"upload_form.drag_and_drop.on_drag_start": "Sélection de la pièce jointe {item}.",
"upload_form.edit": "Modifier",
"upload_progress.label": "Envoi en cours...",
"upload_progress.processing": "Traitement en cours…",

View File

@ -2,7 +2,7 @@
"about.blocks": "Serveurs modérés",
"about.contact": "Contact :",
"about.default_locale": "Défaut",
"about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon gGmbH.",
"about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Raison non disponible",
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateurs et utilisatrices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur.",
"about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas les profils et le contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Profil non disponible",
"empty_column.blocks": "Vous navez bloqué aucun compte pour le moment.",
"empty_column.bookmarked_statuses": "Vous n'avez pas de message en marque-page. Lorsque vous en ajouterez un, il apparaîtra ici.",
"empty_column.collections": "{acct} n'a pas encore créé de collection.",
"empty_column.collections.featured_in": "Vous n'avez pas encore été ajouté·e à une collection.",
"empty_column.collections.featured_in_undiscoverable": "Afin que l'on puisse vous ajouter à des collections, vous devez autoriser d'apparaître dans les expériences de découverte depuis <link>Vie privée et visibilité</link>",
"empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Raccourcis clavier",
"keyboard_shortcuts.home": "Ouvrir le fil daccueil",
"keyboard_shortcuts.hotkey": "Raccourci clavier",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Retour arrière",
"keyboard_shortcuts.keys.enter": "Entrée",
"keyboard_shortcuts.keys.esc": "Échap",
"keyboard_shortcuts.keys.page_down": "Page bas",
"keyboard_shortcuts.keys.page_up": "Page haut",
"keyboard_shortcuts.legend": "Afficher cet aide-mémoire",
"keyboard_shortcuts.load_more": "Bouton Focus \"Charger plus\"",
"keyboard_shortcuts.local": "Ouvrir le fil public local",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Zoomer sur la taille réelle",
"lightbox.zoom_out": "Zoomer pour adapter",
"limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"limited_account_hint.title": "Ce profil ou serveur a été masqué par l'équipe de modération de {domain}.",
"link_preview.author": "Par {name}",
"link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "utilisateur·ice",
"search_results.accounts": "Profils",
"search_results.all": "Tous les résultats",
"search_results.collections": "Collections",
"search_results.hashtags": "Hashtags",
"search_results.no_results": "Aucun résultat.",
"search_results.no_search_yet": "Essayez de rechercher des messages, des profils ou des hashtags.",
@ -1333,12 +1341,12 @@
"upload_button.label": "Ajouter des images, une vidéo ou un fichier audio",
"upload_error.limit": "Taille maximale d'envoi de fichier dépassée.",
"upload_error.poll": "Lenvoi de fichiers nest pas autorisé avec les sondages.",
"upload_error.quote": "Lenvoi de fichiers nest pas autorisé avec les sondages.",
"upload_form.drag_and_drop.instructions": "Pour choisir un média joint, appuyez sur la touche espace ou entrée. Tout en faisant glisser, utilisez les touches fléchées pour déplacer le fichier média dans une direction donnée. Appuyez à nouveau sur la touche espace ou entrée pour déposer le fichier média dans sa nouvelle position, ou appuyez sur la touche Echap pour annuler.",
"upload_form.drag_and_drop.on_drag_cancel": "Le glissement a été annulé. La pièce jointe {item} n'a pas été ajoutée.",
"upload_form.drag_and_drop.on_drag_end": "La pièce jointe du média {item} a été déplacée.",
"upload_error.quote": "Lenvoi de fichiers nest pas autorisé avec les citations.",
"upload_form.drag_and_drop.instructions": "Pour choisir un média joint, appuyez sur la touche espace ou entrée. Pour glisser, utilisez les touches fléchées pour déplacer le fichier média dans une direction donnée. Appuyez à nouveau sur la touche espace ou entrée pour déposer le fichier média dans sa nouvelle position, ou appuyez sur la touche Echap pour annuler.",
"upload_form.drag_and_drop.on_drag_cancel": "Déplacement annulé. La pièce jointe {item} n'a pas été ajoutée.",
"upload_form.drag_and_drop.on_drag_end": "La pièce jointe du média {item} a été déposée.",
"upload_form.drag_and_drop.on_drag_over": "La pièce jointe du média {item} a été déplacée.",
"upload_form.drag_and_drop.on_drag_start": "A récupéré la pièce jointe {item}.",
"upload_form.drag_and_drop.on_drag_start": "Sélection de la pièce jointe {item}.",
"upload_form.edit": "Modifier",
"upload_progress.label": "Envoi en cours…",
"upload_progress.processing": "En cours…",

View File

@ -2,7 +2,6 @@
"about.blocks": "Moderearre servers",
"about.contact": "Kontakt:",
"about.default_locale": "Standert",
"about.disclaimer": "Mastodon is frije, iepenboarnesoftware en in hannelsmerk fan Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Reden net beskikber",
"about.domain_blocks.preamble": "Yn it algemien kinne jo mei Mastodon berjochten ûntfange fan, en ynteraksje hawwe mei brûkers fan elke server yn de fediverse. Dit binne de útsûnderingen dyt op dizze spesifike server jilde.",
"about.domain_blocks.silenced.explanation": "Yn it algemien sjogge jo gjin berjochten en accounts fan dizze server, útsein as jo berjochten eksplisyt opsikje of derfoar kieze om in account fan dizze server te folgjen.",
@ -443,7 +442,6 @@
"lightbox.zoom_in": "Oarspronklike grutte toane",
"lightbox.zoom_out": "Passend toane",
"limited_account_hint.action": "Profyl dochs besjen",
"limited_account_hint.title": "Dit profyl is troch de behearders fan {domain} ferstoppe.",
"link_preview.author": "Troch {name}",
"link_preview.more_from_author": "Mear fan {name}",
"link_preview.shares": "{count, plural, one {{counter} berjocht} other {{counter} berjochten}}",

View File

@ -2,7 +2,7 @@
"about.blocks": "Freastalaithe modhnaithe",
"about.contact": "Teagmháil:",
"about.default_locale": "Réamhshocrú",
"about.disclaimer": "Bogearra foinse oscailte saor in aisce is ea Mastodon, agus is le Mastodon gGmbH an trádmharc.",
"about.disclaimer": "Is bogearraí foinse oscailte saor in aisce é Mastodon, agus trádmharc de chuid Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Cúis nach bhfuil ar fáil",
"about.domain_blocks.preamble": "Go ginearálta, tugann Mastodon deis duit ábhar a fheiceáil ó aon fhreastalaí eile sa fediverse agus idirghníomhú leo. Seo iad na heisceachtaí atá déanta ar an bhfreastalaí seo.",
"about.domain_blocks.silenced.explanation": "Go hiondúil ní fheicfidh tú próifílí ná inneachar ón bhfreastalaí seo, ach amháin má bhíonn tú á lorg nó má ghlacann tú lena leanúint d'aon ghnó.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Níl an phróifíl ar fáil",
"empty_column.blocks": "Níl aon úsáideoir bactha agat fós.",
"empty_column.bookmarked_statuses": "Níl aon phostáil leabharmharcaithe agat fós. Nuair a dhéanann tú leabharmharc, beidh sé le feiceáil anseo.",
"empty_column.collections": "Níl aon bhailiúcháin cruthaithe ag {acct} go fóill.",
"empty_column.collections.featured_in": "Níor cuireadh le haon bhailiúchán thú go fóill.",
"empty_column.collections.featured_in_undiscoverable": "Chun go mbeidh daoine in ann tú a chur le bailiúcháin, ní mór duit cead a thabhairt duit a bheith le feiceáil i dtaithí fionnachtana ó <link>Sainroghanna > Príobháideacht agus raon feidhme</link>",
"empty_column.community": "Tá an amlíne áitiúil folamh. Foilsigh rud éigin go poiblí le tús a chur le cúrsaí!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Aicearraí méarchláir",
"keyboard_shortcuts.home": "Oscail amlíne bhaile",
"keyboard_shortcuts.hotkey": "Eochair aicearra",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Cúlspás",
"keyboard_shortcuts.keys.enter": "Iontráil",
"keyboard_shortcuts.keys.esc": "Esc",
"keyboard_shortcuts.keys.page_down": "Leathanach Síos",
"keyboard_shortcuts.keys.page_up": "Leathanach Suas",
"keyboard_shortcuts.legend": "Taispeáin an finscéal seo",
"keyboard_shortcuts.load_more": "Dírigh ar an gcnaipe \"Lódáil níos mó\"",
"keyboard_shortcuts.local": "Oscail an amlíne áitiúil",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Súmáil chuig an méid iarbhír",
"lightbox.zoom_out": "Súmáil a d'oirfeadh",
"limited_account_hint.action": "Taispeáin an phróifíl ar aon nós",
"limited_account_hint.title": "Tá an phróifíl seo curtha i bhfolach ag na modhnóra {domain}.",
"limited_account_hint.title": "Tá an phróifíl nó an freastalaí seo i bhfolach ag modhnóirí {domain}.",
"link_preview.author": "Le {name}",
"link_preview.more_from_author": "Tuilleadh ó {name}",
"link_preview.shares": "{count, plural, one {{counter} post} other {{counter} poist}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "úsáideoir",
"search_results.accounts": "Próifílí",
"search_results.all": "Gach",
"search_results.collections": "Bailiúcháin",
"search_results.hashtags": "Haischlib",
"search_results.no_results": "Gan torthaí.",
"search_results.no_search_yet": "Bain triail as postálacha, próifílí nó hashtags a chuardach.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Frithealaichean fo mhaorsainneachd",
"about.contact": "Fios thugainn:",
"about.default_locale": "Bun-roghainn",
"about.disclaimer": "S e bathar-bog saor le bun-tùs fosgailte a th ann am Mastodon agus na chomharra-mhalairt aig Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Chan eil an t-adhbhar ga thoirt seachad",
"about.domain_blocks.preamble": "San fharsaingeachd, leigidh Mastodon leat susbaint o fhrithealaiche sam bith sa cho-shaoghal a shealltainn agus eadar-ghìomh a ghabhail leis na cleachdaichean uapa-san. Seo na h-easgaidhean a tha an sàs air an fhrithealaiche shònraichte seo.",
"about.domain_blocks.silenced.explanation": "San fharsaingeachd, chan fhaic thu pròifilean agus susbaint an fhrithealaiche seo ach ma nì thu lorg no ma tha thu ga leantainn.",
@ -824,7 +823,6 @@
"lightbox.zoom_in": "Sùm dhan fhìor-mheud",
"lightbox.zoom_out": "Sùm fèin-obrachail",
"limited_account_hint.action": "Seall a phròifil co-dhiù",
"limited_account_hint.title": "Chaidh a phròifil seo fhalach le maoir {domain}.",
"link_preview.author": "Le {name}",
"link_preview.more_from_author": "Barrachd le {name}",
"link_preview.shares": "{count, plural, one {{counter} phost} two {{counter} phost} few {{counter} postaichean} other {{counter} post}}",

View File

@ -2,7 +2,7 @@
"about.blocks": "Servidores suxeitos a moderación",
"about.contact": "Contacto:",
"about.default_locale": "Por defecto",
"about.disclaimer": "Mastodon é software libre, de código aberto, e unha marca comercial de Mastodon gGmbH.",
"about.disclaimer": "Mastodon é software libre, de código aberto, e unha marca comercial de Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "Motivo non indicado",
"about.domain_blocks.preamble": "Mastodon de xeito xeral permíteche ver contidos doutros servidores do fediverso e interactuar coas súas usuarias. Estas son as excepcións que se estabeleceron neste servidor en particular.",
"about.domain_blocks.silenced.explanation": "Por defecto non verás perfís e contido desde este servidor, a menos que mires de xeito explícito ou optes por seguir ese contido ou usuaria.",
@ -646,6 +646,7 @@
"empty_column.account_unavailable": "Perfil non dispoñible",
"empty_column.blocks": "Aínda non bloqueaches a ningún usuaria.",
"empty_column.bookmarked_statuses": "Aínda non marcaches ningunha publicación. Cando o fagas, aparecerán aquí.",
"empty_column.collections": "{acct} aínda non creou ningunha colección.",
"empty_column.collections.featured_in": "Non te engadiron a ningunha colección.",
"empty_column.collections.featured_in_undiscoverable": "Para que outras persoas poidan engadirte ás súas coleccións ter que conceder permiso para descubrimento da conta en <link>Preferencias > Privacidade e alcance</link>",
"empty_column.community": "A cronoloxía local está baleira. Escribe algo de xeito público para espallalo!",
@ -819,6 +820,12 @@
"keyboard_shortcuts.heading": "Atallos do teclado",
"keyboard_shortcuts.home": "Para abrir a cronoloxía inicial",
"keyboard_shortcuts.hotkey": "Tecla de atallo",
"keyboard_shortcuts.keys.alt": "Alt",
"keyboard_shortcuts.keys.backspace": "Retroceso",
"keyboard_shortcuts.keys.enter": "Entrar",
"keyboard_shortcuts.keys.esc": "Escape",
"keyboard_shortcuts.keys.page_down": "Frecha abaixo",
"keyboard_shortcuts.keys.page_up": "Frecha arriba",
"keyboard_shortcuts.legend": "Para amosar esta lenda",
"keyboard_shortcuts.load_more": "Foco no botón \"Cargar máis\"",
"keyboard_shortcuts.local": "Para abrir a cronoloxía local",
@ -850,7 +857,7 @@
"lightbox.zoom_in": "Ver tamaño real",
"lightbox.zoom_out": "Ver tamaño axustado",
"limited_account_hint.action": "Mostrar perfil igualmente",
"limited_account_hint.title": "Este perfil foi agochado pola moderación de {domain}.",
"limited_account_hint.title": "A moderación de {domain} ocultou este perfil ou servidor.",
"link_preview.author": "Por {name}",
"link_preview.more_from_author": "Máis de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}",
@ -1192,6 +1199,7 @@
"search_popout.user": "usuaria",
"search_results.accounts": "Perfís",
"search_results.all": "Todo",
"search_results.collections": "Coleccións",
"search_results.hashtags": "Cancelos",
"search_results.no_results": "Sen resultados.",
"search_results.no_search_yet": "Intenta buscando publicacións, perfís ou cancelos.",

View File

@ -2,7 +2,6 @@
"about.blocks": "שרתים תחת פיקוח תוכן",
"about.contact": "יצירת קשר:",
"about.default_locale": "ברירת המחדל",
"about.disclaimer": "מסטודון היא תוכנת קוד פתוח חינמית וסימן מסחרי של Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "הסיבה אינה זמינה",
"about.domain_blocks.preamble": "ככלל מסטודון מאפשרת לך לצפות בתוכן ולתקשר עם משתמשים מכל שרת בפדיברס. אלו הם היוצאים מן הכלל שהוגדרו עבור השרת המסוים הזה.",
"about.domain_blocks.silenced.explanation": "ככלל פרופילים ותוכן משרת זה לא יוצגו, אלא אם חיפשת אותם באופן מפורש או בחרת להשתתף בו על ידי מעקב.",
@ -850,7 +849,7 @@
"lightbox.zoom_in": "הגדלה לגודל מלא",
"lightbox.zoom_out": "התאמה לגודל המסך",
"limited_account_hint.action": "הצג חשבון בכל זאת",
"limited_account_hint.title": "פרופיל המשתמש הזה הוסתר על ידי מנחי הדיון של {domain}.",
"limited_account_hint.title": "פרופיל המשתמש או השרת כולו הוסתר על ידי מנחי הדיון של {domain}.",
"link_preview.author": "מאת {name}",
"link_preview.more_from_author": "עוד מאת {name}",
"link_preview.shares": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{counter} הודעות} other {{counter} הודעות}}",

View File

@ -2,7 +2,6 @@
"about.blocks": "मॉडरेट सर्वर",
"about.contact": "कांटेक्ट:",
"about.default_locale": "Default",
"about.disclaimer": "मास्टोडन एक ओपन सोर्स सॉफ्टवेयर है, और मास्टोडन gGmbH का ट्रेडमार्क है।",
"about.domain_blocks.no_reason_available": "कारण उपलब्ध नहीं है!",
"about.domain_blocks.preamble": "मास्टोडन आम तौर पर आपको कंटेंट को देखने और फेडिवेर्से में किसी अन्य सर्वर से उपयोगकर्ताओं के साथ बातचीत करने की अनुमति देता है। ये अपवाद हैं जो इस विशेष सर्वर पर बनाए गए हैं।",
"about.domain_blocks.silenced.explanation": "आप आमतौर पर इस सर्वर से प्रोफ़ाइल और कंटेंट नहीं देख पाएंगे, जब तक कि आप इसे स्पष्ट रूप से नहीं देखते या इसका अनुसरण करके इसका चयन नहीं करते।",
@ -326,7 +325,6 @@
"lightbox.next": "अगला",
"lightbox.previous": "पिछला",
"limited_account_hint.action": "फिर भी प्रोफाइल दिखाओ",
"limited_account_hint.title": "यह प्रोफ़ाइल {domain} के मॉडरेटर द्वारा छिपाई गई है.",
"lists.delete": "सूची हटाएँ",
"lists.edit": "सूची संपादित करें",
"lists.replies_policy.followed": "अन्य फोल्लोवेद यूजर",

View File

@ -1,7 +1,6 @@
{
"about.blocks": "Moderirani poslužitelji",
"about.contact": "Kontakt:",
"about.disclaimer": "Mastodon je besplatan softver otvorenog koda i zaštitni znak tvrtke Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Razlog nije dostupan",
"about.domain_blocks.preamble": "Mastodon vam općenito omogućuje pregled sadržaja i interakciju s korisnicima s bilo kojeg drugog poslužitelja u fediverse. Ovo su iznimke napravljene na ovom poslužitelju.",
"about.domain_blocks.silenced.explanation": "Obično nećete vidjeti profile i sadržaj s ovog poslužitelja, osim ako ga izričito ne potražite ili uključite u njega slijedeći ga.",

View File

@ -2,7 +2,6 @@
"about.blocks": "Moderált kiszolgálók",
"about.contact": "Kapcsolat:",
"about.default_locale": "Alapértelmezett",
"about.disclaimer": "A Mastodon ingyenes, nyílt forráskódú szoftver, a Mastodon gGmbH védjegye.",
"about.domain_blocks.no_reason_available": "Nem áll rendelkezésre indoklás",
"about.domain_blocks.preamble": "A Mastodon általában mindenféle tartalomcserét és interakciót lehetővé tesz bármelyik másik kiszolgálóval a födiverzumban. Ezek azok a kivételek, amelyek a mi kiszolgálónkon érvényben vannak.",
"about.domain_blocks.silenced.explanation": "Általában nem fogsz profilokat és tartalmat látni erről a kiszolgálóról, hacsak közvetlenül fel nem keresed vagy követed.",
@ -850,7 +849,7 @@
"lightbox.zoom_in": "Nagyítás a tényleges méretre",
"lightbox.zoom_out": "Méretre igazítás",
"limited_account_hint.action": "Profil megjelenítése mindenképpen",
"limited_account_hint.title": "Ezt a profilt {domain} moderátorai elrejtették.",
"limited_account_hint.title": "Ezt a profilt vagy kiszolgálót a(z) {domain} moderátorai elrejtették.",
"link_preview.author": "{name} szerint",
"link_preview.more_from_author": "Több tőle: {name}",
"link_preview.shares": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}}",

View File

@ -1,7 +1,6 @@
{
"about.blocks": "Մոդերացուող սպասարկիչներ",
"about.contact": "Կապ՝",
"about.disclaimer": "Մաստոդոնը ազատ, բաց ելակոդով ծրագրակազմ է, յայտնի Mastodon gGmbH ապրանքանշանով։",
"about.domain_blocks.silenced.title": "Սահմանափակ",
"about.domain_blocks.suspended.title": "Սպասող",
"about.not_available": "Այս տեղեկութիւնը տեսանելի չի այս սերուերում։",

View File

@ -2,7 +2,6 @@
"about.blocks": "Servitores moderate",
"about.contact": "Contacto:",
"about.default_locale": "Default",
"about.disclaimer": "Mastodon es software libere, de codice aperte, e un marca de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Ration non disponibile",
"about.domain_blocks.preamble": "Mastodon generalmente permitte vider le contento de, e interager con, usatores de qualcunque altere servitor in le fediverso. Istes es le exceptiones que ha essite facite sur iste servitor particular.",
"about.domain_blocks.silenced.explanation": "Generalmente, tu non videra le profilos e le contento de iste servitor, excepte si tu expressemente cerca le contento o seque le profilos.",
@ -483,7 +482,6 @@
"lightbox.zoom_in": "Aggrandir al dimension real",
"lightbox.zoom_out": "Diminuer pro adaptar",
"limited_account_hint.action": "Monstrar profilo in omne caso",
"limited_account_hint.title": "Iste profilo ha essite celate per le moderatores de {domain}.",
"link_preview.author": "Per {name}",
"link_preview.more_from_author": "Plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",

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