Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dalite 2024-12-03 15:22:44 +01:00
commit 792fbd44bd
320 changed files with 4674 additions and 5772 deletions

View file

@ -36,4 +36,4 @@ jobs:
bundler-cache: true
- name: Run bundler-audit
run: bundle exec bundler-audit check --update
run: bin/bundler-audit check --update

View file

@ -35,18 +35,18 @@ jobs:
git diff --exit-code
- name: Check locale file normalization
run: bundle exec i18n-tasks check-normalized
run: bin/i18n-tasks check-normalized
- name: Check for unused strings
run: bundle exec i18n-tasks unused
run: bin/i18n-tasks unused
- name: Check for missing strings in English YML
run: |
bundle exec i18n-tasks add-missing -l en
bin/i18n-tasks add-missing -l en
git diff --exit-code
- name: Check for wrong string interpolations
run: bundle exec i18n-tasks check-consistent-interpolations
run: bin/i18n-tasks check-consistent-interpolations
- name: Check that all required locale files exist
run: bundle exec rake repo:check_locales_files
run: bin/rake repo:check_locales_files

View file

@ -46,7 +46,7 @@ jobs:
uses: ./.github/actions/setup-ruby
- name: Run i18n normalize task
run: bundle exec i18n-tasks normalize
run: bin/i18n-tasks normalize
# Create or update the pull request
- name: Create Pull Request

View file

@ -48,7 +48,7 @@ jobs:
uses: ./.github/actions/setup-ruby
- name: Run i18n normalize task
run: bundle exec i18n-tasks normalize
run: bin/i18n-tasks normalize
# Create or update the pull request
- name: Create Pull Request

View file

@ -43,4 +43,4 @@ jobs:
- name: Run haml-lint
run: |
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
bundle exec haml-lint --reporter github
bin/haml-lint --reporter github

View file

@ -68,7 +68,7 @@ The following changelog entries focus on changes visible to users, administrator
- `GET /api/v2/notifications`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped
- `GET /api/v2/notifications/:group_key`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group
- `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts
- `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group
- `POST /api/v2/notifications/:group_key/dismiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group
- `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count
- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, #31723, #32062 and #32281 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\
The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\
@ -399,7 +399,7 @@ The following changelog entries focus on changes visible to users, administrator
- Fix empty environment variables not using default nil value (#27400 by @renchap)
- Fix language sorting in settings (#27158 by @gunchleoc)
## |4.2.11] - 2024-08-16
## [4.2.11] - 2024-08-16
### Added

View file

@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.10
# syntax=docker/dockerfile:1.12
# This file is designed for production server deployment, not local development work
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker

View file

@ -54,7 +54,7 @@ GEM
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
active_model_serializers (0.10.14)
active_model_serializers (0.10.15)
actionpack (>= 4.1)
activemodel (>= 4.1)
case_transform (>= 0.2)
@ -93,10 +93,9 @@ GEM
annotaterb (4.13.0)
ast (2.4.2)
attr_required (1.0.2)
awrence (1.2.1)
aws-eventstream (1.3.0)
aws-partitions (1.1012.0)
aws-sdk-core (3.213.0)
aws-partitions (1.1015.0)
aws-sdk-core (3.214.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@ -104,7 +103,7 @@ GEM
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.173.0)
aws-sdk-s3 (1.175.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@ -346,10 +345,12 @@ GEM
json-ld-preloaded (3.3.1)
json-ld (~> 3.3)
rdf (~> 3.3)
json-schema (5.1.0)
json-schema (5.1.1)
addressable (~> 2.8)
bigdecimal (~> 3.1)
jsonapi-renderer (0.2.2)
jwt (2.7.1)
jwt (2.9.3)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@ -407,8 +408,8 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2024.1105)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.25.1)
mini_portile2 (2.8.8)
minitest (5.25.2)
msgpack (1.7.5)
multi_json (1.15.0)
mutex_m (0.3.0)
@ -425,7 +426,7 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.16.7)
nokogiri (1.16.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.7)
@ -472,7 +473,7 @@ GEM
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.2)
opentelemetry-semantic_conventions
opentelemetry-helpers-sql-obfuscation (0.2.0)
opentelemetry-helpers-sql-obfuscation (0.2.1)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.2.0)
opentelemetry-api (~> 1.0)
@ -492,7 +493,7 @@ GEM
opentelemetry-instrumentation-active_model_serializers (0.20.2)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_record (0.8.0)
opentelemetry-instrumentation-active_record (0.8.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_support (0.6.0)
@ -505,29 +506,29 @@ GEM
opentelemetry-instrumentation-concurrent_ruby (0.21.4)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-excon (0.22.4)
opentelemetry-instrumentation-excon (0.22.5)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-faraday (0.24.6)
opentelemetry-instrumentation-faraday (0.24.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http (0.23.4)
opentelemetry-instrumentation-http (0.23.5)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http_client (0.22.7)
opentelemetry-instrumentation-http_client (0.22.8)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-net_http (0.22.7)
opentelemetry-instrumentation-net_http (0.22.8)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-pg (0.29.0)
opentelemetry-instrumentation-pg (0.29.1)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (0.25.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.33.0)
opentelemetry-instrumentation-rails (0.33.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.2.0)
opentelemetry-instrumentation-action_pack (~> 0.10.0)
@ -752,7 +753,7 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.3.2)
selenium-webdriver (4.26.0)
selenium-webdriver (4.27.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@ -844,9 +845,8 @@ GEM
public_suffix
warden (1.2.9)
rack (>= 2.0.9)
webauthn (3.1.0)
webauthn (3.2.2)
android_key_attestation (~> 0.3.0)
awrence (~> 1.1)
bindata (~> 2.4)
cbor (~> 0.5.9)
cose (~> 1.1)
@ -1031,7 +1031,7 @@ DEPENDENCIES
xorcist (~> 1.1)
RUBY VERSION
ruby 3.3.5p100
ruby 3.3.6p108
BUNDLED WITH
2.5.22
2.5.23

View file

@ -22,7 +22,6 @@ class ApplicationController < ActionController::Base
helper_method :use_seamless_external_login?
helper_method :sso_account_settings
helper_method :limited_federation_mode?
helper_method :body_class_string
helper_method :skip_csrf_meta_tags?
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
@ -158,10 +157,6 @@ class ApplicationController < ActionController::Base
current_user.setting_theme
end
def body_class_string
@body_classes || ''
end
def respond_with_error(code)
respond_to do |format|
format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }

View file

@ -28,7 +28,7 @@ module CacheConcern
def render_with_cache(**options)
raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given?
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields]&.join(',')].compact.join(':')
expires_in = options.delete(:expires_in) || 3.minutes
body = Rails.cache.read(key, raw: true)

View file

@ -143,10 +143,11 @@ module ApplicationHelper
end
def body_classes
output = body_class_string.split
output = []
output << content_for(:body_classes)
output << "theme-#{current_theme.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
output << 'rtl' if locale_direction == 'rtl'
output.compact_blank.join(' ')

View file

@ -230,62 +230,6 @@ function loaded() {
}
},
);
Rails.delegate(
document,
'button.status__content__spoiler-link',
'click',
function () {
if (!(this instanceof HTMLButtonElement)) return;
const statusEl = this.parentNode?.parentNode;
if (
!(
statusEl instanceof HTMLDivElement &&
statusEl.classList.contains('.status__content')
)
)
return;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = new IntlMessageFormat(
localeData['status.show_more'] ?? 'Show more',
locale,
).format() as string;
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = new IntlMessageFormat(
localeData['status.show_less'] ?? 'Show less',
locale,
).format() as string;
}
},
);
document
.querySelectorAll<HTMLButtonElement>('button.status__content__spoiler-link')
.forEach((spoilerLink) => {
const statusEl = spoilerLink.parentNode?.parentNode;
if (
!(
statusEl instanceof HTMLDivElement &&
statusEl.classList.contains('.status__content')
)
)
return;
const message =
statusEl.dataset.spoiler === 'expanded'
? (localeData['status.show_less'] ?? 'Show less')
: (localeData['status.show_more'] ?? 'Show more');
spoilerLink.textContent = new IntlMessageFormat(
message,
locale,
).format() as string;
});
}
Rails.delegate(
@ -439,6 +383,24 @@ Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
});
});
Rails.delegate(document, '.rules-list button', 'click', ({ target }) => {
if (!(target instanceof HTMLElement)) {
return;
}
const button = target.closest('button');
if (!button) {
return;
}
if (button.ariaExpanded === 'true') {
button.ariaExpanded = 'false';
} else {
button.ariaExpanded = 'true';
}
});
function main() {
ready(loaded).catch((error: unknown) => {
console.error(error);

View file

@ -1,66 +0,0 @@
import { defineMessages } from 'react-intl';
import { AxiosError } from 'axios';
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' },
rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' },
});
export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
export const dismissAlert = alert => ({
type: ALERT_DISMISS,
alert,
});
export const clearAlert = () => ({
type: ALERT_CLEAR,
});
export const showAlert = alert => ({
type: ALERT_SHOW,
alert,
});
export const showAlertForError = (error, skipNotFound = false) => {
if (error.response) {
const { data, status, statusText, headers } = error.response;
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
return { type: ALERT_NOOP };
}
// Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) {
return showAlert({
title: messages.rateLimitedTitle,
message: messages.rateLimitedMessage,
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
});
}
return showAlert({
title: `${status}`,
message: data.error || statusText,
});
}
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
};

View file

@ -0,0 +1,90 @@
import { defineMessages } from 'react-intl';
import type { MessageDescriptor } from 'react-intl';
import { AxiosError } from 'axios';
import type { AxiosResponse } from 'axios';
interface Alert {
title: string | MessageDescriptor;
message: string | MessageDescriptor;
values?: Record<string, string | number | Date>;
}
interface ApiErrorResponse {
error?: string;
}
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: {
id: 'alert.unexpected.message',
defaultMessage: 'An unexpected error occurred.',
},
rateLimitedTitle: {
id: 'alert.rate_limited.title',
defaultMessage: 'Rate limited',
},
rateLimitedMessage: {
id: 'alert.rate_limited.message',
defaultMessage: 'Please retry after {retry_time, time, medium}.',
},
});
export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
export const dismissAlert = (alert: Alert) => ({
type: ALERT_DISMISS,
alert,
});
export const clearAlert = () => ({
type: ALERT_CLEAR,
});
export const showAlert = (alert: Alert) => ({
type: ALERT_SHOW,
alert,
});
export const showAlertForError = (error: unknown, skipNotFound = false) => {
if (error instanceof AxiosError && error.response) {
const { status, statusText, headers } = error.response;
const { data } = error.response as AxiosResponse<ApiErrorResponse>;
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
return { type: ALERT_NOOP };
}
// Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) {
return showAlert({
title: messages.rateLimitedTitle,
message: messages.rateLimitedMessage,
values: {
retry_time: new Date(headers['x-ratelimit-reset'] as string),
},
});
}
return showAlert({
title: `${status}`,
message: data.error ?? statusText,
});
}
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
};

View file

@ -1,58 +0,0 @@
import api from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
export function fetchSuggestions(withRelationships = false) {
return (dispatch) => {
dispatch(fetchSuggestionsRequest());
api().get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => {
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchSuggestionsSuccess(response.data));
if (withRelationships) {
dispatch(fetchRelationships(response.data.map(item => item.account.id)));
}
}).catch(error => dispatch(fetchSuggestionsFail(error)));
};
}
export function fetchSuggestionsRequest() {
return {
type: SUGGESTIONS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchSuggestionsSuccess(suggestions) {
return {
type: SUGGESTIONS_FETCH_SUCCESS,
suggestions,
skipLoading: true,
};
}
export function fetchSuggestionsFail(error) {
return {
type: SUGGESTIONS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
}
export const dismissSuggestion = accountId => (dispatch) => {
dispatch({
type: SUGGESTIONS_DISMISS,
id: accountId,
});
api().delete(`/api/v1/suggestions/${accountId}`).catch(() => {});
};

View file

@ -0,0 +1,24 @@
import {
apiGetSuggestions,
apiDeleteSuggestion,
} from 'mastodon/api/suggestions';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
export const fetchSuggestions = createDataLoadingThunk(
'suggestions/fetch',
() => apiGetSuggestions(20),
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data.map((x) => x.account)));
dispatch(fetchRelationships(data.map((x) => x.account.id)));
return data;
},
);
export const dismissSuggestion = createDataLoadingThunk(
'suggestions/dismiss',
({ accountId }: { accountId: string }) => apiDeleteSuggestion(accountId),
);

View file

@ -5,3 +5,16 @@ export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
comment: value,
});
export const apiFollowAccount = (
id: string,
params?: {
reblogs: boolean;
},
) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/follow`, {
...params,
});
export const apiUnfollowAccount = (id: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/unfollow`);

View file

@ -0,0 +1,8 @@
import { apiRequestGet, apiRequestDelete } from 'mastodon/api';
import type { ApiSuggestionJSON } from 'mastodon/api_types/suggestions';
export const apiGetSuggestions = (limit: number) =>
apiRequestGet<ApiSuggestionJSON[]>('v2/suggestions', { limit });
export const apiDeleteSuggestion = (accountId: string) =>
apiRequestDelete(`v1/suggestions/${accountId}`);

View file

@ -0,0 +1,13 @@
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
export type ApiSuggestionSourceJSON =
| 'featured'
| 'most_followed'
| 'most_interactions'
| 'similar_to_recently_followed'
| 'friends_of_friends';
export interface ApiSuggestionJSON {
sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]];
account: ApiAccountJSON;
}

View file

@ -1,181 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { EmptyAccount } from 'mastodon/components/empty_account';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { me } from '../initial_state';
import { Avatar } from './avatar';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
});
const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => {
const intl = useIntl();
const handleFollow = useCallback(() => {
onFollow(account);
}, [onFollow, account]);
const handleBlock = useCallback(() => {
onBlock(account);
}, [onBlock, account]);
const handleMute = useCallback(() => {
onMute(account);
}, [onMute, account]);
const handleMuteNotifications = useCallback(() => {
onMuteNotifications(account, true);
}, [onMuteNotifications, account]);
const handleUnmuteNotifications = useCallback(() => {
onMuteNotifications(account, false);
}, [onMuteNotifications, account]);
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
}
if (hidden) {
return (
<>
{account.get('display_name')}
{account.get('username')}
</>
);
}
let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={handleFollow} />;
} else if (blocking) {
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
} else if (muting) {
let menu;
if (account.getIn(['relationship', 'muting_notifications'])) {
menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
} else {
menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
}
buttons = (
<>
<DropdownMenuContainer
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<Button text={intl.formatMessage(messages.unmute)} onClick={handleMute} />
</>
);
} else if (defaultAction === 'mute') {
buttons = <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
} else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={handleFollow} />;
}
}
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`} data-hover-card-account={account.get('id')}>
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
</Link>
{!minimal && (
<div className='account__relationship'>
{buttons}
</div>
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
);
};
Account.propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.record,
onFollow: PropTypes.func,
onBlock: PropTypes.func,
onMute: PropTypes.func,
onMuteNotifications: PropTypes.func,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
withBio: PropTypes.bool,
};
export default Account;

View file

@ -0,0 +1,235 @@
import { useCallback } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import {
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { initMuteModal } from 'mastodon/actions/mutes';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
import { me } from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
cancel_follow_request: {
id: 'account.cancel_follow_request',
defaultMessage: 'Withdraw follow request',
},
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
mute_notifications: {
id: 'account.mute_notifications_short',
defaultMessage: 'Mute notifications',
},
unmute_notifications: {
id: 'account.unmute_notifications_short',
defaultMessage: 'Unmute notifications',
},
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
});
export const Account: React.FC<{
size?: number;
id: string;
hidden?: boolean;
minimal?: boolean;
defaultAction?: 'block' | 'mute';
withBio?: boolean;
}> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(id));
const relationship = useAppSelector((state) => state.relationships.get(id));
const dispatch = useAppDispatch();
const handleBlock = useCallback(() => {
if (relationship?.blocking) {
dispatch(unblockAccount(id));
} else {
dispatch(blockAccount(id));
}
}, [dispatch, id, relationship]);
const handleMute = useCallback(() => {
if (relationship?.muting) {
dispatch(unmuteAccount(id));
} else {
dispatch(initMuteModal(account));
}
}, [dispatch, id, account, relationship]);
const handleMuteNotifications = useCallback(() => {
dispatch(muteAccount(id, true));
}, [dispatch, id]);
const handleUnmuteNotifications = useCallback(() => {
dispatch(muteAccount(id, false));
}, [dispatch, id]);
if (hidden) {
return (
<>
{account?.display_name}
{account?.username}
</>
);
}
let buttons;
if (account && account.id !== me && relationship) {
const { requested, blocking, muting } = relationship;
if (requested) {
buttons = <FollowButton accountId={id} />;
} else if (blocking) {
buttons = (
<Button
text={intl.formatMessage(messages.unblock)}
onClick={handleBlock}
/>
);
} else if (muting) {
const menu = [
{
text: intl.formatMessage(
relationship.muting_notifications
? messages.unmute_notifications
: messages.mute_notifications,
),
action: relationship.muting_notifications
? handleUnmuteNotifications
: handleMuteNotifications,
},
];
buttons = (
<>
<DropdownMenu
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<Button
text={intl.formatMessage(messages.unmute)}
onClick={handleMute}
/>
</>
);
} else if (defaultAction === 'mute') {
buttons = (
<Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />
);
} else if (defaultAction === 'block') {
buttons = (
<Button
text={intl.formatMessage(messages.block)}
onClick={handleBlock}
/>
);
} else {
buttons = <FollowButton accountId={id} />;
}
} else {
buttons = <FollowButton accountId={id} />;
}
let muteTimeRemaining;
if (account?.mute_expires_at) {
muteTimeRemaining = (
<>
· <RelativeTimestamp timestamp={account.mute_expires_at} futureDate />
</>
);
}
let verification;
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.value} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link
className='account__display-name'
title={account?.acct}
to={`/@${account?.acct}`}
data-hover-card-account={id}
>
<div className='account__avatar-wrapper'>
{account ? (
<Avatar account={account} size={size} />
) : (
<Skeleton width={size} height={size} />
)}
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
{account ? (
<>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>{' '}
{verification} {muteTimeRemaining}
</>
) : (
<Skeleton width='7ch' />
)}
</div>
)}
</div>
</Link>
{!minimal && <div className='account__relationship'>{buttons}</div>}
</div>
{account &&
withBio &&
(account.note.length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
/>
) : (
<div className='account__note account__note--missing'>
<FormattedMessage
id='account.no_bio'
defaultMessage='No description provided.'
/>
</div>
))}
</div>
);
};

View file

@ -1,72 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollTop } from '../scroll';
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
export default class Column extends PureComponent {
static propTypes = {
children: PropTypes.node,
label: PropTypes.string,
bindToDocument: PropTypes.bool,
};
scrollTop () {
let scrollable = null;
if (this.props.bindToDocument) {
scrollable = document.scrollingElement;
} else {
scrollable = this.node.querySelector('.scrollable');
}
if (!scrollable) {
return;
}
this._interruptScrollAnimation = scrollTop(scrollable);
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
};
setRef = c => {
this.node = c;
};
componentDidMount () {
if (this.props.bindToDocument) {
document.addEventListener('wheel', this.handleWheel, listenerOptions);
} else {
this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
}
}
componentWillUnmount () {
if (this.props.bindToDocument) {
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
} else {
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
}
}
render () {
const { label, children } = this.props;
return (
<div role='region' aria-label={label} className='column' ref={this.setRef}>
{children}
</div>
);
}
}

View file

@ -0,0 +1,52 @@
import { forwardRef, useRef, useImperativeHandle } from 'react';
import type { Ref } from 'react';
import { scrollTop } from 'mastodon/scroll';
export interface ColumnRef {
scrollTop: () => void;
node: HTMLDivElement | null;
}
interface ColumnProps {
children?: React.ReactNode;
label?: string;
bindToDocument?: boolean;
}
export const Column = forwardRef<ColumnRef, ColumnProps>(
({ children, label, bindToDocument }, ref: Ref<ColumnRef>) => {
const nodeRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
node: nodeRef.current,
scrollTop() {
let scrollable = null;
if (bindToDocument) {
scrollable = document.scrollingElement;
} else {
scrollable = nodeRef.current?.querySelector('.scrollable');
}
if (!scrollable) {
return;
}
scrollTop(scrollable);
},
}));
return (
<div role='region' aria-label={label} className='column' ref={nodeRef}>
{children}
</div>
);
},
);
Column.displayName = 'Column';
// eslint-disable-next-line import/no-default-export
export default Column;

View file

@ -24,7 +24,7 @@ function useHandleClick(onClick?: OnClickCallback) {
}, [history, onClick]);
}
export const ColumnBackButton: React.FC<{ onClick: OnClickCallback }> = ({
export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
onClick,
}) => {
const handleClick = useHandleClick(onClick);

View file

@ -0,0 +1,67 @@
import { useCallback, useState, useEffect, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
export const ColumnSearchHeader: React.FC<{
onBack: () => void;
onSubmit: (value: string) => void;
onActivate: () => void;
placeholder: string;
active: boolean;
}> = ({ onBack, onActivate, onSubmit, placeholder, active }) => {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState('');
useEffect(() => {
if (!active) {
setValue('');
}
}, [active]);
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setValue(value);
onSubmit(value);
},
[setValue, onSubmit],
);
const handleKeyUp = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
e.preventDefault();
onBack();
inputRef.current?.blur();
}
},
[onBack],
);
const handleFocus = useCallback(() => {
onActivate();
}, [onActivate]);
const handleSubmit = useCallback(() => {
onSubmit(value);
}, [onSubmit, value]);
return (
<form className='column-search-header' onSubmit={handleSubmit}>
<input
ref={inputRef}
type='search'
value={value}
onChange={handleChange}
onKeyUp={handleKeyUp}
placeholder={placeholder}
onFocus={handleFocus}
/>
{active && (
<button type='button' className='link-button' onClick={onBack}>
<FormattedMessage id='column_search.cancel' defaultMessage='Cancel' />
</button>
)}
</form>
);
};

View file

@ -1,33 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { DisplayName } from 'mastodon/components/display_name';
import { Skeleton } from 'mastodon/components/skeleton';
interface Props {
size?: number;
minimal?: boolean;
}
export const EmptyAccount: React.FC<Props> = ({
size = 46,
minimal = false,
}) => {
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'>
<Skeleton width={size} height={size} />
</div>
<div>
<DisplayName />
<Skeleton width='7ch' />
</div>
</div>
</div>
</div>
);
};

View file

@ -99,7 +99,12 @@ export const FollowButton: React.FC<{
return (
<Button
onClick={handleClick}
disabled={relationship?.blocked_by || relationship?.blocking}
disabled={
relationship?.blocked_by ||
relationship?.blocking ||
(!(relationship?.following || relationship?.requested) &&
(account?.suspended || !!account?.moved))
}
secondary={following}
className={following ? 'button--destructive' : undefined}
>

View file

@ -0,0 +1,22 @@
import { useHovering } from '@/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
export const GIF: React.FC<{
src: string;
staticSrc: string;
className: string;
animate?: boolean;
}> = ({ src, staticSrc, className, animate = autoPlayGif }) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
return (
<img
className={className}
src={hovering || animate ? src : staticSrc}
alt=''
role='presentation'
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
);
};

View file

@ -8,10 +8,10 @@ import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { fetchServer } from 'mastodon/actions/server';
import { Account } from 'mastodon/components/account';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state';
const messages = defineMessages({

View file

@ -173,7 +173,7 @@ class Status extends ImmutablePureComponent {
handleMouseUp = e => {
// Only handle clicks on the empty space above the content
if (e.target !== e.currentTarget) {
if (e.target !== e.currentTarget && e.detail >= 1) {
return;
}

View file

@ -204,7 +204,7 @@ class StatusContent extends PureComponent {
element = element.parentNode;
}
if (deltaX + deltaY < 5 && (e.button === 0 || e.button === 1) && this.props.onClick) {
if (deltaX + deltaY < 5 && (e.button === 0 || e.button === 1) && e.detail >= 1 && this.props.onClick) {
this.props.onClick(e);
}

View file

@ -1,60 +0,0 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import {
followAccount,
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
} from '../actions/accounts';
import { initMuteModal } from '../actions/mutes';
import Account from '../components/account';
import { makeGetAccount } from '../selectors';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
} else {
dispatch(followAccount(account.get('id')));
}
},
onBlock (account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(blockAccount(account.get('id')));
}
},
onMute (account) {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},
onMuteNotifications (account, notifications) {
dispatch(muteAccount(account.get('id'), notifications));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));

View file

@ -13,11 +13,11 @@ import { connect } from 'react-redux';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
import { Account } from 'mastodon/components/account';
import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
const messages = defineMessages({

View file

@ -1,6 +1,7 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { FormattedMessage, FormattedNumber } from 'react-intl';
import { domain } from 'mastodon/initial_state';
import type { Percentiles } from 'mastodon/models/annual_report';
export const Percentile: React.FC<{
@ -12,7 +13,7 @@ export const Percentile: React.FC<{
<div className='annual-report__bento__box annual-report__summary__percentile'>
<FormattedMessage
id='annual_report.summary.percentile.text'
defaultMessage='<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>'
defaultMessage='<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>'
values={{
topLabel: (str) => (
<div className='annual-report__summary__percentile__label'>
@ -44,6 +45,8 @@ export const Percentile: React.FC<{
)}
</div>
),
domain,
}}
>
{(message) => <>{message}</>}

View file

@ -9,11 +9,11 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
import { Account } from 'mastodon/components/account';
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
const messages = defineMessages({
@ -70,7 +70,7 @@ class Blocks extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} defaultAction='block' />,
<Account key={id} id={id} defaultAction='block' />,
)}
</ScrollableList>
</Column>

View file

@ -6,7 +6,7 @@ import { useSelector, useDispatch } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { cancelReplyCompose } from 'mastodon/actions/compose';
import Account from 'mastodon/components/account';
import { Account } from 'mastodon/components/account';
import { IconButton } from 'mastodon/components/icon_button';
import { me } from 'mastodon/initial_state';
@ -20,7 +20,6 @@ const messages = defineMessages({
export const NavigationBar = () => {
const dispatch = useDispatch();
const intl = useIntl();
const account = useSelector(state => state.getIn(['accounts', me]));
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
const handleCancelClick = useCallback(() => {
@ -29,7 +28,7 @@ export const NavigationBar = () => {
return (
<div className='navigation-bar'>
<Account account={account} minimal />
<Account id={me} minimal />
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
</div>
);

View file

@ -6,6 +6,7 @@ import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { expandSearch } from 'mastodon/actions/search';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@ -13,7 +14,6 @@ import { SearchSection } from 'mastodon/features/explore/components/search_secti
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
const INITIAL_PAGE_LIMIT = 10;
@ -49,7 +49,7 @@ export const SearchResults = () => {
if (results.get('accounts') && results.get('accounts').size > 0) {
accounts = (
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
{withoutLastResult(results.get('accounts')).map(accountId => <Account key={accountId} id={accountId} />)}
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreAccounts} />}
</SearchSection>
);

View file

@ -15,7 +15,8 @@ import {
changeColumnParams,
} from 'mastodon/actions/columns';
import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
import Column from 'mastodon/components/column';
import { Column } from 'mastodon/components/column';
import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@ -49,7 +50,7 @@ export const Directory: React.FC<{
const intl = useIntl();
const dispatch = useAppDispatch();
const column = useRef<Column>(null);
const column = useRef<ColumnRef>(null);
const [orderParam, setOrderParam] = useSearchParam('order');
const [localParam, setLocalParam] = useSearchParam('local');

View file

@ -45,7 +45,7 @@ class Links extends PureComponent {
const banner = (
<DismissableBanner id='explore/links'>
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' />
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being shared the most on the fediverse today. Newer news stories posted by more different people are ranked higher.' />
</DismissableBanner>
);

View file

@ -13,10 +13,10 @@ import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { submitSearch, expandSearch } from 'mastodon/actions/search';
import { Account } from 'mastodon/components/account';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import Account from 'mastodon/containers/account_container';
import Status from 'mastodon/containers/status_container';
import { SearchSection } from './components/search_section';

View file

@ -58,7 +58,7 @@ class Statuses extends PureComponent {
return (
<StatusList
trackScroll
prepend={<DismissableBanner id='explore/statuses'><FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' /></DismissableBanner>}
prepend={<DismissableBanner id='explore/statuses'><FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from across the fediverse are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' /></DismissableBanner>}
alwaysPrepend
timelineId='explore'
statusIds={statusIds}

View file

@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
@ -15,15 +14,15 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Card } from './components/card';
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
suggestions: state.suggestions.items,
isLoading: state.suggestions.isLoading,
});
class Suggestions extends PureComponent {
static propTypes = {
isLoading: PropTypes.bool,
suggestions: ImmutablePropTypes.list,
suggestions: PropTypes.array,
dispatch: PropTypes.func.isRequired,
...WithRouterPropTypes,
};
@ -32,17 +31,17 @@ class Suggestions extends PureComponent {
const { dispatch, suggestions, history } = this.props;
// If we're navigating back to the screen, do not trigger a reload
if (history.action === 'POP' && suggestions.size > 0) {
if (history.action === 'POP' && suggestions.length > 0) {
return;
}
dispatch(fetchSuggestions(true));
dispatch(fetchSuggestions());
}
render () {
const { isLoading, suggestions } = this.props;
if (!isLoading && suggestions.isEmpty()) {
if (!isLoading && suggestions.length === 0) {
return (
<div className='explore__suggestions scrollable scrollable--flex'>
<div className='empty-column-indicator'>
@ -56,9 +55,9 @@ class Suggestions extends PureComponent {
<div className='explore__suggestions scrollable' data-nosnippet>
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
<Card
key={suggestion.get('account')}
id={suggestion.get('account')}
source={suggestion.getIn(['sources', 0])}
key={suggestion.account_id}
id={suggestion.account_id}
source={suggestion.sources[0]}
/>
))}
</div>

View file

@ -44,7 +44,7 @@ class Tags extends PureComponent {
const banner = (
<DismissableBanner id='explore/tags'>
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' />
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction on the fediverse today. Hashtags that are used by more different people are ranked higher.' />
</DismissableBanner>
);

View file

@ -12,11 +12,11 @@ import { debounce } from 'lodash';
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
import { Account } from 'mastodon/components/account';
import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column';
const messages = defineMessages({
@ -87,7 +87,7 @@ class Favourites extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>

View file

@ -133,7 +133,7 @@ const Firehose = ({ feedType, multiColumn }) => {
<DismissableBanner id='public_timeline'>
<FormattedMessage
id='dismissable_banner.public_timeline'
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
defaultMessage='These are the most recent public posts from people on the fediverse that people on {domain} follow.'
values={{ domain }}
/>
</DismissableBanner>

View file

@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
@ -23,7 +24,6 @@ import {
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
@ -175,7 +175,7 @@ class Followers extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{forceEmptyState ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>

View file

@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
@ -23,7 +24,6 @@ import {
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
@ -175,7 +175,7 @@ class Following extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{forceEmptyState ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>

View file

@ -4,12 +4,17 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { Button } from 'mastodon/components/button';
import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { withIdentity } from 'mastodon/identity_context';
import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
adminModeration: { id: 'hashtag.admin_moderation', defaultMessage: 'Open moderation interface for #{name}' },
});
const usesRenderer = (displayNumber, pluralReady) => (
@ -45,11 +50,18 @@ const usesTodayRenderer = (displayNumber, pluralReady) => (
/>
);
export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
export const HashtagHeader = withIdentity(injectIntl(({ tag, intl, disabled, onClick, identity }) => {
if (!tag) {
return null;
}
const { signedIn, permissions } = identity;
const menu = [];
if (signedIn && (permissions & PERMISSION_MANAGE_TAXONOMIES) === PERMISSION_MANAGE_TAXONOMIES ) {
menu.push({ text: intl.formatMessage(messages.adminModeration, { name: tag.get("name") }), href: `/admin/tags/${tag.get('id')}` });
}
const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
const dividingCircle = <span aria-hidden>{' · '}</span>;
@ -57,7 +69,10 @@ export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.get('name')}</h1>
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
<div className='hashtag-header__header__buttons'>
{ menu.length > 0 && <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> }
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
</div>
</div>
<div>
@ -69,7 +84,7 @@ export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
</div>
</div>
);
});
}));
HashtagHeader.propTypes = {
tag: ImmutablePropTypes.map,

View file

@ -1,217 +0,0 @@
import PropTypes from 'prop-types';
import { useEffect, useCallback, useRef, useState } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { useDispatch, useSelector } from 'react-redux';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import { changeSetting } from 'mastodon/actions/settings';
import { fetchSuggestions, dismissSuggestion } from 'mastodon/actions/suggestions';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { domain } from 'mastodon/initial_state';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" },
friendsOfFriendsHint: { id: 'follow_suggestions.hints.friends_of_friends', defaultMessage: 'This profile is popular among the people you follow.' },
similarToRecentlyFollowedHint: { id: 'follow_suggestions.hints.similar_to_recently_followed', defaultMessage: 'This profile is similar to the profiles you have most recently followed.' },
featuredHint: { id: 'follow_suggestions.hints.featured', defaultMessage: 'This profile has been hand-picked by the {domain} team.' },
mostFollowedHint: { id: 'follow_suggestions.hints.most_followed', defaultMessage: 'This profile is one of the most followed on {domain}.'},
mostInteractionsHint: { id: 'follow_suggestions.hints.most_interactions', defaultMessage: 'This profile has been recently getting a lot of attention on {domain}.' },
});
const Source = ({ id }) => {
const intl = useIntl();
let label, hint;
switch (id) {
case 'friends_of_friends':
hint = intl.formatMessage(messages.friendsOfFriendsHint);
label = <FormattedMessage id='follow_suggestions.personalized_suggestion' defaultMessage='Personalized suggestion' />;
break;
case 'similar_to_recently_followed':
hint = intl.formatMessage(messages.similarToRecentlyFollowedHint);
label = <FormattedMessage id='follow_suggestions.personalized_suggestion' defaultMessage='Personalized suggestion' />;
break;
case 'featured':
hint = intl.formatMessage(messages.featuredHint, { domain });
label = <FormattedMessage id='follow_suggestions.curated_suggestion' defaultMessage='Staff pick' />;
break;
case 'most_followed':
hint = intl.formatMessage(messages.mostFollowedHint, { domain });
label = <FormattedMessage id='follow_suggestions.popular_suggestion' defaultMessage='Popular suggestion' />;
break;
case 'most_interactions':
hint = intl.formatMessage(messages.mostInteractionsHint, { domain });
label = <FormattedMessage id='follow_suggestions.popular_suggestion' defaultMessage='Popular suggestion' />;
break;
}
return (
<div className='inline-follow-suggestions__body__scrollable__card__text-stack__source' title={hint}>
<Icon icon={InfoIcon} />
{label}
</div>
);
};
Source.propTypes = {
id: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']),
};
const Card = ({ id, sources }) => {
const intl = useIntl();
const account = useSelector(state => state.getIn(['accounts', id]));
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
const dispatch = useDispatch();
const handleDismiss = useCallback(() => {
dispatch(dismissSuggestion(id));
}, [id, dispatch]);
return (
<div className='inline-follow-suggestions__body__scrollable__card'>
<IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<div className='inline-follow-suggestions__body__scrollable__card__avatar'>
<Link to={`/@${account.get('acct')}`}><Avatar account={account} size={72} /></Link>
</div>
<div className='inline-follow-suggestions__body__scrollable__card__text-stack'>
<Link to={`/@${account.get('acct')}`}><DisplayName account={account} /></Link>
{firstVerifiedField ? <VerifiedBadge link={firstVerifiedField.get('value')} /> : <Source id={sources.get(0)} />}
</div>
<FollowButton accountId={id} />
</div>
);
};
Card.propTypes = {
id: PropTypes.string.isRequired,
sources: ImmutablePropTypes.list,
};
const DISMISSIBLE_ID = 'home/follow-suggestions';
export const InlineFollowSuggestions = ({ hidden }) => {
const intl = useIntl();
const dispatch = useDispatch();
const suggestions = useSelector(state => state.getIn(['suggestions', 'items']));
const isLoading = useSelector(state => state.getIn(['suggestions', 'isLoading']));
const dismissed = useSelector(state => state.getIn(['settings', 'dismissed_banners', DISMISSIBLE_ID]));
const bodyRef = useRef();
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
useEffect(() => {
dispatch(fetchSuggestions());
}, [dispatch]);
useEffect(() => {
if (!bodyRef.current) {
return;
}
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
const handleLeftNav = useCallback(() => {
bodyRef.current.scrollLeft -= 200;
}, [bodyRef]);
const handleRightNav = useCallback(() => {
bodyRef.current.scrollLeft += 200;
}, [bodyRef]);
const handleScroll = useCallback(() => {
if (!bodyRef.current) {
return;
}
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
const handleDismiss = useCallback(() => {
dispatch(changeSetting(['dismissed_banners', DISMISSIBLE_ID], true));
}, [dispatch]);
if (dismissed || (!isLoading && suggestions.isEmpty())) {
return null;
}
if (hidden) {
return (
<div className='inline-follow-suggestions' />
);
}
return (
<div className='inline-follow-suggestions'>
<div className='inline-follow-suggestions__header'>
<h3><FormattedMessage id='follow_suggestions.who_to_follow' defaultMessage='Who to follow' /></h3>
<div className='inline-follow-suggestions__header__actions'>
<button className='link-button' onClick={handleDismiss}><FormattedMessage id='follow_suggestions.dismiss' defaultMessage="Don't show again" /></button>
<Link to='/explore/suggestions' className='link-button'><FormattedMessage id='follow_suggestions.view_all' defaultMessage='View all' /></Link>
</div>
</div>
<div className='inline-follow-suggestions__body'>
<div className='inline-follow-suggestions__body__scrollable' ref={bodyRef} onScroll={handleScroll}>
{suggestions.map(suggestion => (
<Card
key={suggestion.get('account')}
id={suggestion.get('account')}
sources={suggestion.get('sources')}
/>
))}
</div>
{canScrollLeft && (
<button className='inline-follow-suggestions__body__scroll-button left' onClick={handleLeftNav} aria-label={intl.formatMessage(messages.previous)}>
<div className='inline-follow-suggestions__body__scroll-button__icon'><Icon icon={ChevronLeftIcon} /></div>
</button>
)}
{canScrollRight && (
<button className='inline-follow-suggestions__body__scroll-button right' onClick={handleRightNav} aria-label={intl.formatMessage(messages.next)}>
<div className='inline-follow-suggestions__body__scroll-button__icon'><Icon icon={ChevronRightIcon} /></div>
</button>
)}
</div>
</div>
);
};
InlineFollowSuggestions.propTypes = {
hidden: PropTypes.bool,
};

View file

@ -0,0 +1,326 @@
import { useEffect, useCallback, useRef, useState } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import { changeSetting } from 'mastodon/actions/settings';
import {
fetchSuggestions,
dismissSuggestion,
} from 'mastodon/actions/suggestions';
import type { ApiSuggestionSourceJSON } from 'mastodon/api_types/suggestions';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { domain } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
dismiss: {
id: 'follow_suggestions.dismiss',
defaultMessage: "Don't show again",
},
friendsOfFriendsHint: {
id: 'follow_suggestions.hints.friends_of_friends',
defaultMessage: 'This profile is popular among the people you follow.',
},
similarToRecentlyFollowedHint: {
id: 'follow_suggestions.hints.similar_to_recently_followed',
defaultMessage:
'This profile is similar to the profiles you have most recently followed.',
},
featuredHint: {
id: 'follow_suggestions.hints.featured',
defaultMessage: 'This profile has been hand-picked by the {domain} team.',
},
mostFollowedHint: {
id: 'follow_suggestions.hints.most_followed',
defaultMessage: 'This profile is one of the most followed on {domain}.',
},
mostInteractionsHint: {
id: 'follow_suggestions.hints.most_interactions',
defaultMessage:
'This profile has been recently getting a lot of attention on {domain}.',
},
});
const Source: React.FC<{
id: ApiSuggestionSourceJSON;
}> = ({ id }) => {
const intl = useIntl();
let label, hint;
switch (id) {
case 'friends_of_friends':
hint = intl.formatMessage(messages.friendsOfFriendsHint);
label = (
<FormattedMessage
id='follow_suggestions.personalized_suggestion'
defaultMessage='Personalized suggestion'
/>
);
break;
case 'similar_to_recently_followed':
hint = intl.formatMessage(messages.similarToRecentlyFollowedHint);
label = (
<FormattedMessage
id='follow_suggestions.personalized_suggestion'
defaultMessage='Personalized suggestion'
/>
);
break;
case 'featured':
hint = intl.formatMessage(messages.featuredHint, { domain });
label = (
<FormattedMessage
id='follow_suggestions.curated_suggestion'
defaultMessage='Staff pick'
/>
);
break;
case 'most_followed':
hint = intl.formatMessage(messages.mostFollowedHint, { domain });
label = (
<FormattedMessage
id='follow_suggestions.popular_suggestion'
defaultMessage='Popular suggestion'
/>
);
break;
case 'most_interactions':
hint = intl.formatMessage(messages.mostInteractionsHint, { domain });
label = (
<FormattedMessage
id='follow_suggestions.popular_suggestion'
defaultMessage='Popular suggestion'
/>
);
break;
}
return (
<div
className='inline-follow-suggestions__body__scrollable__card__text-stack__source'
title={hint}
>
<Icon id='' icon={InfoIcon} />
{label}
</div>
);
};
const Card: React.FC<{
id: string;
sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]];
}> = ({ id, sources }) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(id));
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
const dispatch = useAppDispatch();
const handleDismiss = useCallback(() => {
void dispatch(dismissSuggestion({ accountId: id }));
}, [id, dispatch]);
return (
<div className='inline-follow-suggestions__body__scrollable__card'>
<IconButton
icon=''
iconComponent={CloseIcon}
onClick={handleDismiss}
title={intl.formatMessage(messages.dismiss)}
/>
<div className='inline-follow-suggestions__body__scrollable__card__avatar'>
<Link to={`/@${account?.acct}`}>
<Avatar account={account} size={72} />
</Link>
</div>
<div className='inline-follow-suggestions__body__scrollable__card__text-stack'>
<Link to={`/@${account?.acct}`}>
<DisplayName account={account} />
</Link>
{firstVerifiedField ? (
<VerifiedBadge link={firstVerifiedField.value} />
) : (
<Source id={sources[0]} />
)}
</div>
<FollowButton accountId={id} />
</div>
);
};
const DISMISSIBLE_ID = 'home/follow-suggestions';
export const InlineFollowSuggestions: React.FC<{
hidden?: boolean;
}> = ({ hidden }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const suggestions = useAppSelector((state) => state.suggestions.items);
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const dismissed = useAppSelector(
(state) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
state.settings.getIn(['dismissed_banners', DISMISSIBLE_ID]) as boolean,
);
const bodyRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
useEffect(() => {
void dispatch(fetchSuggestions());
}, [dispatch]);
useEffect(() => {
if (!bodyRef.current) {
return;
}
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft(
bodyRef.current.clientWidth - bodyRef.current.scrollLeft <
bodyRef.current.scrollWidth,
);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight(
bodyRef.current.scrollLeft + bodyRef.current.clientWidth <
bodyRef.current.scrollWidth,
);
}
}, [setCanScrollRight, setCanScrollLeft, suggestions]);
const handleLeftNav = useCallback(() => {
if (!bodyRef.current) {
return;
}
bodyRef.current.scrollLeft -= 200;
}, []);
const handleRightNav = useCallback(() => {
if (!bodyRef.current) {
return;
}
bodyRef.current.scrollLeft += 200;
}, []);
const handleScroll = useCallback(() => {
if (!bodyRef.current) {
return;
}
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft(
bodyRef.current.clientWidth - bodyRef.current.scrollLeft <
bodyRef.current.scrollWidth,
);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight(
bodyRef.current.scrollLeft + bodyRef.current.clientWidth <
bodyRef.current.scrollWidth,
);
}
}, [setCanScrollRight, setCanScrollLeft]);
const handleDismiss = useCallback(() => {
dispatch(changeSetting(['dismissed_banners', DISMISSIBLE_ID], true));
}, [dispatch]);
if (dismissed || (!isLoading && suggestions.length === 0)) {
return null;
}
if (hidden) {
return <div className='inline-follow-suggestions' />;
}
return (
<div className='inline-follow-suggestions'>
<div className='inline-follow-suggestions__header'>
<h3>
<FormattedMessage
id='follow_suggestions.who_to_follow'
defaultMessage='Who to follow'
/>
</h3>
<div className='inline-follow-suggestions__header__actions'>
<button className='link-button' onClick={handleDismiss}>
<FormattedMessage
id='follow_suggestions.dismiss'
defaultMessage="Don't show again"
/>
</button>
<Link to='/explore/suggestions' className='link-button'>
<FormattedMessage
id='follow_suggestions.view_all'
defaultMessage='View all'
/>
</Link>
</div>
</div>
<div className='inline-follow-suggestions__body'>
<div
className='inline-follow-suggestions__body__scrollable'
ref={bodyRef}
onScroll={handleScroll}
>
{suggestions.map((suggestion) => (
<Card
key={suggestion.account_id}
id={suggestion.account_id}
sources={suggestion.sources}
/>
))}
</div>
{canScrollLeft && (
<button
className='inline-follow-suggestions__body__scroll-button left'
onClick={handleLeftNav}
aria-label={intl.formatMessage(messages.previous)}
>
<div className='inline-follow-suggestions__body__scroll-button__icon'>
<Icon id='' icon={ChevronLeftIcon} />
</div>
</button>
)}
{canScrollRight && (
<button
className='inline-follow-suggestions__body__scroll-button right'
onClick={handleRightNav}
aria-label={intl.formatMessage(messages.next)}
>
<div className='inline-follow-suggestions__body__scroll-button__icon'>
<Icon id='' icon={ChevronRightIcon} />
</div>
</button>
)}
</div>
</div>
);
};

View file

@ -5,7 +5,8 @@ import { useParams } from 'react-router-dom';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import { expandLinkTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column';
import { Column } from 'mastodon/components/column';
import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
import type { Card } from 'mastodon/models/status';
@ -17,7 +18,7 @@ export const LinkTimeline: React.FC<{
const { url } = useParams<{ url: string }>();
const decodedUrl = url ? decodeURIComponent(url) : undefined;
const dispatch = useAppDispatch();
const columnRef = useRef<Column>(null);
const columnRef = useRef<ColumnRef>(null);
const firstStatusId = useAppSelector((state) =>
decodedUrl
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access

View file

@ -11,7 +11,7 @@ import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { fetchLists } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
import Column from 'mastodon/components/column';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';

View file

@ -7,14 +7,15 @@ import { useParams, Link } from 'react-router-dom';
import { useDebouncedCallback } from 'use-debounce';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { fetchFollowing } from 'mastodon/actions/accounts';
import { fetchRelationships } from 'mastodon/actions/accounts';
import { showAlertForError } from 'mastodon/actions/alerts';
import { importFetchedAccounts } from 'mastodon/actions/importer';
import { fetchList } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
import { apiRequest } from 'mastodon/api';
import { apiFollowAccount } from 'mastodon/api/accounts';
import {
apiGetAccounts,
apiAddAccountToList,
@ -23,23 +24,22 @@ import {
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import Column from 'mastodon/components/column';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { ColumnSearchHeader } from 'mastodon/components/column_search_header';
import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
import { me } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
placeholder: {
id: 'lists.search_placeholder',
defaultMessage: 'Search people you follow',
id: 'lists.search',
defaultMessage: 'Search',
},
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
add: { id: 'lists.add_member', defaultMessage: 'Add' },
@ -49,54 +49,6 @@ const messages = defineMessages({
type Mode = 'remove' | 'add';
const ColumnSearchHeader: React.FC<{
onBack: () => void;
onSubmit: (value: string) => void;
}> = ({ onBack, onSubmit }) => {
const intl = useIntl();
const [value, setValue] = useState('');
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setValue(value);
onSubmit(value);
},
[setValue, onSubmit],
);
const handleSubmit = useCallback(() => {
onSubmit(value);
}, [onSubmit, value]);
return (
<ButtonInTabsBar>
<form className='column-search-header' onSubmit={handleSubmit}>
<button
type='button'
className='column-header__back-button compact'
onClick={onBack}
aria-label={intl.formatMessage(messages.back)}
>
<Icon
id='chevron-left'
icon={ArrowBackIcon}
className='column-back-button__icon'
/>
</button>
<input
type='search'
value={value}
onChange={handleChange}
placeholder={intl.formatMessage(messages.placeholder)}
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus
/>
</form>
</ButtonInTabsBar>
);
};
const AccountItem: React.FC<{
accountId: string;
listId: string;
@ -104,17 +56,51 @@ const AccountItem: React.FC<{
onToggle: (accountId: string) => void;
}> = ({ accountId, listId, partOfList, onToggle }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId));
const relationship = useAppSelector((state) =>
accountId ? state.relationships.get(accountId) : undefined,
);
const following =
accountId === me || relationship?.following || relationship?.requested;
useEffect(() => {
if (accountId) {
dispatch(fetchRelationships([accountId]));
}
}, [dispatch, accountId]);
const handleClick = useCallback(() => {
if (partOfList) {
void apiRemoveAccountFromList(listId, accountId);
onToggle(accountId);
} else {
void apiAddAccountToList(listId, accountId);
if (following) {
void apiAddAccountToList(listId, accountId);
onToggle(accountId);
} else {
dispatch(
openModal({
modalType: 'CONFIRM_FOLLOW_TO_LIST',
modalProps: {
accountId,
onConfirm: () => {
apiFollowAccount(accountId)
.then(() => apiAddAccountToList(listId, accountId))
.then(() => {
onToggle(accountId);
return '';
})
.catch((err: unknown) => {
dispatch(showAlertForError(err));
});
},
},
}),
);
}
}
onToggle(accountId);
}, [accountId, listId, partOfList, onToggle]);
}, [dispatch, accountId, following, listId, partOfList, onToggle]);
if (!account) {
return null;
@ -156,6 +142,7 @@ const AccountItem: React.FC<{
text={intl.formatMessage(
partOfList ? messages.remove : messages.add,
)}
secondary={partOfList}
onClick={handleClick}
/>
</div>
@ -171,9 +158,6 @@ const ListMembers: React.FC<{
const { id } = useParams<{ id: string }>();
const intl = useIntl();
const followingAccountIds = useAppSelector(
(state) => state.user_lists.getIn(['following', me, 'items']) as string[],
);
const [searching, setSearching] = useState(false);
const [accountIds, setAccountIds] = useState<string[]>([]);
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
@ -195,8 +179,6 @@ const ListMembers: React.FC<{
.catch(() => {
setLoading(false);
});
dispatch(fetchFollowing(me));
}
}, [dispatch, id]);
@ -243,8 +225,7 @@ const ListMembers: React.FC<{
signal: searchRequestRef.current.signal,
params: {
q: value,
resolve: false,
following: true,
resolve: true,
},
})
.then((data) => {
@ -265,8 +246,8 @@ const ListMembers: React.FC<{
let displayedAccountIds: string[];
if (mode === 'add') {
displayedAccountIds = searching ? searchAccountIds : followingAccountIds;
if (mode === 'add' && searching) {
displayedAccountIds = searchAccountIds;
} else {
displayedAccountIds = accountIds;
}
@ -276,31 +257,21 @@ const ListMembers: React.FC<{
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.heading)}
>
{mode === 'remove' ? (
<ColumnHeader
title={intl.formatMessage(messages.heading)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
extraButton={
<button
onClick={handleSearchClick}
type='button'
className='column-header__button'
title={intl.formatMessage(messages.enterSearch)}
aria-label={intl.formatMessage(messages.enterSearch)}
>
<Icon id='plus' icon={AddIcon} />
</button>
}
/>
) : (
<ColumnSearchHeader
onBack={handleDismissSearchClick}
onSubmit={handleSearch}
/>
)}
<ColumnHeader
title={intl.formatMessage(messages.heading)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
/>
<ColumnSearchHeader
placeholder={intl.formatMessage(messages.placeholder)}
onBack={handleDismissSearchClick}
onSubmit={handleSearch}
onActivate={handleSearchClick}
active={mode === 'add'}
/>
<ScrollableList
scrollKey='list_members'
@ -310,17 +281,15 @@ const ListMembers: React.FC<{
showLoading={loading && displayedAccountIds.length === 0}
hasMore={false}
footer={
mode === 'remove' && (
<>
<div className='spacer' />
<>
{displayedAccountIds.length > 0 && <div className='spacer' />}
<div className='column-footer'>
<Link to={`/lists/${id}`} className='button button--block'>
<FormattedMessage id='lists.done' defaultMessage='Done' />
</Link>
</div>
</>
)
<div className='column-footer'>
<Link to={`/lists/${id}`} className='button button--block'>
<FormattedMessage id='lists.done' defaultMessage='Done' />
</Link>
</div>
</>
}
emptyMessage={
mode === 'remove' ? (

View file

@ -14,7 +14,7 @@ import { fetchList } from 'mastodon/actions/lists';
import { createList, updateList } from 'mastodon/actions/lists_typed';
import { apiGetAccounts } from 'mastodon/api/lists';
import type { RepliesPolicyType } from 'mastodon/api_types/lists';
import Column from 'mastodon/components/column';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { useAppDispatch, useAppSelector } from 'mastodon/store';

View file

@ -11,11 +11,11 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import { Account } from 'mastodon/components/account';
import { fetchMutes, expandMutes } from '../../actions/mutes';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
const messages = defineMessages({
@ -72,7 +72,7 @@ class Mutes extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} defaultAction='mute' />,
<Account key={id} id={id} defaultAction='mute' />,
)}
</ScrollableList>

View file

@ -1,119 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { Icon } from 'mastodon/components/icon';
const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
});
class FilterBar extends PureComponent {
static propTypes = {
selectFilter: PropTypes.func.isRequired,
selectedFilter: PropTypes.string.isRequired,
advancedMode: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
};
onClick (notificationType) {
return () => this.props.selectFilter(notificationType);
}
render () {
const { selectedFilter, advancedMode, intl } = this.props;
const renderedElement = !advancedMode ? (
<div className='notification__filter-bar'>
<button
className={selectedFilter === 'all' ? 'active' : ''}
onClick={this.onClick('all')}
>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</button>
<button
className={selectedFilter === 'mention' ? 'active' : ''}
onClick={this.onClick('mention')}
>
<FormattedMessage
id='notifications.filter.mentions'
defaultMessage='Mentions'
/>
</button>
</div>
) : (
<div className='notification__filter-bar'>
<button
className={selectedFilter === 'all' ? 'active' : ''}
onClick={this.onClick('all')}
>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</button>
<button
className={selectedFilter === 'mention' ? 'active' : ''}
onClick={this.onClick('mention')}
title={intl.formatMessage(tooltips.mentions)}
>
<Icon id='reply-all' icon={ReplyAllIcon} />
</button>
<button
className={selectedFilter === 'favourite' ? 'active' : ''}
onClick={this.onClick('favourite')}
title={intl.formatMessage(tooltips.favourites)}
>
<Icon id='star' icon={StarIcon} />
</button>
<button
className={selectedFilter === 'reblog' ? 'active' : ''}
onClick={this.onClick('reblog')}
title={intl.formatMessage(tooltips.boosts)}
>
<Icon id='retweet' icon={RepeatIcon} />
</button>
<button
className={selectedFilter === 'poll' ? 'active' : ''}
onClick={this.onClick('poll')}
title={intl.formatMessage(tooltips.polls)}
>
<Icon id='tasks' icon={InsertChartIcon} />
</button>
<button
className={selectedFilter === 'status' ? 'active' : ''}
onClick={this.onClick('status')}
title={intl.formatMessage(tooltips.statuses)}
>
<Icon id='home' icon={HomeIcon} />
</button>
<button
className={selectedFilter === 'follow' ? 'active' : ''}
onClick={this.onClick('follow')}
title={intl.formatMessage(tooltips.follows)}
>
<Icon id='user-plus' icon={PersonAddIcon} />
</button>
</div>
);
return renderedElement;
}
}
export default injectIntl(FilterBar);

View file

@ -18,8 +18,8 @@ import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import AccountContainer from 'mastodon/containers/account_container';
import StatusContainer from 'mastodon/containers/status_container';
import { me } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -147,7 +147,7 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);
@ -167,7 +167,7 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
<FollowRequestContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);
@ -420,7 +420,7 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);

View file

@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { setFilter } from '../../../actions/notifications';
import FilterBar from '../components/filter_bar';
const makeMapStateToProps = state => ({
selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
advancedMode: state.getIn(['settings', 'notifications', 'quickFilter', 'advanced']),
});
const mapDispatchToProps = (dispatch) => ({
selectFilter (newActiveFilter) {
dispatch(setFilter(newActiveFilter));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(FilterBar);

View file

@ -1,308 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
import { compareId } from 'mastodon/compare_id';
import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { submitMarkers } from '../../actions/markers';
import {
expandNotifications,
scrollTopNotifications,
loadPending,
mountNotifications,
unmountNotifications,
markNotificationsAsRead,
} from '../../actions/notifications';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list';
import {
FilteredNotificationsBanner,
FilteredNotificationsIconButton,
} from './components/filtered_notifications_banner';
import NotificationsPermissionBanner from './components/notifications_permission_banner';
import ColumnSettingsContainer from './containers/column_settings_container';
import FilterBarContainer from './containers/filter_bar_container';
import NotificationContainer from './containers/notification_container';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
});
const getExcludedTypes = createSelector([
state => state.getIn(['settings', 'notifications', 'shows']),
], (shows) => {
return ImmutableList(shows.filter(item => !item).keys());
});
const getNotifications = createSelector([
state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
getExcludedTypes,
state => state.getIn(['notifications', 'items']),
], (showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') {
// used if user changed the notification settings after loading the notifications from the server
// otherwise a list of notifications will come pre-filtered from the backend
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
}
return notifications.filter(item => item === null || allowedType === item.get('type'));
});
const mapStateToProps = state => ({
notifications: getNotifications(state),
isLoading: state.getIn(['notifications', 'isLoading'], 0) > 0,
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0',
canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']),
});
class Notifications extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
columnId: PropTypes.string,
notifications: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
numPending: PropTypes.number,
lastReadId: PropTypes.string,
canMarkAsRead: PropTypes.bool,
needsNotificationPermission: PropTypes.bool,
};
static defaultProps = {
trackScroll: true,
};
UNSAFE_componentWillMount() {
this.props.dispatch(mountNotifications());
}
componentWillUnmount () {
this.handleLoadOlder.cancel();
this.handleScrollToTop.cancel();
this.handleScroll.cancel();
this.props.dispatch(scrollTopNotifications(false));
this.props.dispatch(unmountNotifications());
}
handleLoadGap = (maxId) => {
this.props.dispatch(expandNotifications({ maxId }));
};
handleLoadOlder = debounce(() => {
const last = this.props.notifications.last();
this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
}, 300, { leading: true });
handleLoadPending = () => {
this.props.dispatch(loadPending());
};
handleScrollToTop = debounce(() => {
this.props.dispatch(scrollTopNotifications(true));
}, 100);
handleScroll = debounce(() => {
this.props.dispatch(scrollTopNotifications(false));
}, 100);
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('NOTIFICATIONS', {}));
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
};
handleHeaderClick = () => {
this.column.scrollTop();
};
setColumnRef = c => {
this.column = c;
};
handleMoveUp = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
this._selectChild(elementIndex, true);
};
handleMoveDown = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
this._selectChild(elementIndex, false);
};
_selectChild (index, align_top) {
const container = this.column.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus();
}
}
handleMarkAsRead = () => {
this.props.dispatch(markNotificationsAsRead());
this.props.dispatch(submitMarkers({ immediate: true }));
};
render () {
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
const { signedIn } = this.props.identity;
let scrollableContent = null;
const filterBarContainer = signedIn
? (<FilterBarContainer />)
: null;
if (isLoading && this.scrollableContent) {
scrollableContent = this.scrollableContent;
} else if (notifications.size > 0 || hasMore) {
scrollableContent = notifications.map((item, index) => item === null ? (
<LoadGap
key={'gap:' + notifications.getIn([index + 1, 'id'])}
disabled={isLoading}
param={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
onClick={this.handleLoadGap}
/>
) : (
<NotificationContainer
key={item.get('id')}
notification={item}
accountId={item.get('account')}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
unread={lastReadId !== '0' && compareId(item.get('id'), lastReadId) > 0}
/>
));
} else {
scrollableContent = null;
}
this.scrollableContent = scrollableContent;
let scrollContainer;
const prepend = (
<>
{needsNotificationPermission && <NotificationsPermissionBanner />}
<FilteredNotificationsBanner />
</>
);
if (signedIn) {
scrollContainer = (
<ScrollableList
scrollKey={`notifications-${columnId}`}
trackScroll={!pinned}
isLoading={isLoading}
showLoading={isLoading && notifications.size === 0}
hasMore={hasMore}
numPending={numPending}
prepend={prepend}
alwaysPrepend
emptyMessage={emptyMessage}
onLoadMore={this.handleLoadOlder}
onLoadPending={this.handleLoadPending}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
bindToDocument={!multiColumn}
>
{scrollableContent}
</ScrollableList>
);
} else {
scrollContainer = <NotSignedInIndicator />;
}
const extraButton = (
<>
<FilteredNotificationsIconButton className='column-header__button' />
{canMarkAsRead && (
<button
aria-label={intl.formatMessage(messages.markAsRead)}
title={intl.formatMessage(messages.markAsRead)}
onClick={this.handleMarkAsRead}
className='column-header__button'
>
<Icon id='done-all' icon={DoneAllIcon} />
</button>
)}
</>
);
return (
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='bell'
iconComponent={NotificationsIcon}
active={isUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={extraButton}
>
<ColumnSettingsContainer />
</ColumnHeader>
{filterBarContainer}
{scrollContainer}
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(withIdentity(injectIntl(Notifications)));

View file

@ -36,7 +36,8 @@ import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { submitMarkers } from '../../actions/markers';
import Column from '../../components/column';
import { Column } from '../../components/column';
import type { ColumnRef } from '../../components/column';
import { ColumnHeader } from '../../components/column_header';
import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list';
@ -96,7 +97,7 @@ export const Notifications: React.FC<{
selectNeedsNotificationPermission,
);
const columnRef = useRef<Column>(null);
const columnRef = useRef<ColumnRef>(null);
const selectChild = useCallback((index: number, alignTop: boolean) => {
const container = columnRef.current?.node as HTMLElement | undefined;

View file

@ -1,9 +0,0 @@
import Notifications_v2 from 'mastodon/features/notifications_v2';
export const NotificationsWrapper = (props) => {
return (
<Notifications_v2 {...props} />
);
};
export default NotificationsWrapper;

View file

@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
import CheckIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from 'mastodon/components/icon';
export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
const content = (
<>
<div className='onboarding__steps__item__icon'>
<Icon id={icon} icon={iconComponent} />
</div>
<div className='onboarding__steps__item__description'>
<h6>{label}</h6>
<p>{description}</p>
</div>
<div className={completed ? 'onboarding__steps__item__progress' : 'onboarding__steps__item__go'}>
{completed ? <Icon icon={CheckIcon} /> : <Icon icon={ArrowRightAltIcon} />}
</div>
</>
);
if (href) {
return (
<a href={href} onClick={onClick} target='_blank' rel='noopener' className='onboarding__steps__item'>
{content}
</a>
);
} else if (to) {
return (
<Link to={to} className='onboarding__steps__item'>
{content}
</Link>
);
}
return (
<button onClick={onClick} className='onboarding__steps__item'>
{content}
</button>
);
};
Step.propTypes = {
label: PropTypes.node,
description: PropTypes.node,
icon: PropTypes.string,
iconComponent: PropTypes.func,
completed: PropTypes.bool,
href: PropTypes.string,
to: PropTypes.string,
onClick: PropTypes.func,
};

View file

@ -1,62 +0,0 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { markAsPartial } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { EmptyAccount } from 'mastodon/components/empty_account';
import Account from 'mastodon/containers/account_container';
import { useAppSelector } from 'mastodon/store';
export const Follows = () => {
const dispatch = useDispatch();
const isLoading = useAppSelector(state => state.getIn(['suggestions', 'isLoading']));
const suggestions = useAppSelector(state => state.getIn(['suggestions', 'items']));
useEffect(() => {
dispatch(fetchSuggestions(true));
return () => {
dispatch(markAsPartial('home'));
};
}, [dispatch]);
let loadedContent;
if (isLoading) {
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
} else if (suggestions.isEmpty()) {
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
} else {
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
}
return (
<>
<ColumnBackButton />
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
</div>
<div className='follow-recommendations'>
{loadedContent}
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
<div className='onboarding__footer'>
<Link className='link-button' to='/start'><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></Link>
</div>
</div>
</>
);
};

View file

@ -0,0 +1,186 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import { useDebouncedCallback } from 'use-debounce';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { fetchRelationships } from 'mastodon/actions/accounts';
import { importFetchedAccounts } from 'mastodon/actions/importer';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { markAsPartial } from 'mastodon/actions/timelines';
import { apiRequest } from 'mastodon/api';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { Account } from 'mastodon/components/account';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { ColumnSearchHeader } from 'mastodon/components/column_search_header';
import ScrollableList from 'mastodon/components/scrollable_list';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
title: {
id: 'onboarding.follows.title',
defaultMessage: 'Follow people to get started',
},
search: { id: 'onboarding.follows.search', defaultMessage: 'Search' },
back: { id: 'onboarding.follows.back', defaultMessage: 'Back' },
});
type Mode = 'remove' | 'add';
export const Follows: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const suggestions = useAppSelector((state) => state.suggestions.items);
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
const [mode, setMode] = useState<Mode>('remove');
const [isLoadingSearch, setIsLoadingSearch] = useState(false);
const [isSearching, setIsSearching] = useState(false);
useEffect(() => {
void dispatch(fetchSuggestions());
return () => {
dispatch(markAsPartial('home'));
};
}, [dispatch]);
const handleSearchClick = useCallback(() => {
setMode('add');
}, [setMode]);
const handleDismissSearchClick = useCallback(() => {
setMode('remove');
setIsSearching(false);
}, [setMode, setIsSearching]);
const searchRequestRef = useRef<AbortController | null>(null);
const handleSearch = useDebouncedCallback(
(value: string) => {
if (searchRequestRef.current) {
searchRequestRef.current.abort();
}
if (value.trim().length === 0) {
setIsSearching(false);
setSearchAccountIds([]);
return;
}
setIsSearching(true);
setIsLoadingSearch(true);
searchRequestRef.current = new AbortController();
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
signal: searchRequestRef.current.signal,
params: {
q: value,
},
})
.then((data) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchRelationships(data.map((a) => a.id)));
setSearchAccountIds(data.map((a) => a.id));
setIsLoadingSearch(false);
return '';
})
.catch(() => {
setIsLoadingSearch(false);
});
},
500,
{ leading: true, trailing: true },
);
let displayedAccountIds: string[];
if (mode === 'add' && isSearching) {
displayedAccountIds = searchAccountIds;
} else {
displayedAccountIds = suggestions.map(
(suggestion) => suggestion.account_id,
);
}
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.title)}
>
<ColumnHeader
title={intl.formatMessage(messages.title)}
icon='person'
iconComponent={PersonIcon}
multiColumn={multiColumn}
showBackButton
/>
<ColumnSearchHeader
placeholder={intl.formatMessage(messages.search)}
onBack={handleDismissSearchClick}
onActivate={handleSearchClick}
active={mode === 'add'}
onSubmit={handleSearch}
/>
<ScrollableList
scrollKey='follow_recommendations'
trackScroll={!multiColumn}
bindToDocument={!multiColumn}
showLoading={
(isLoading || isLoadingSearch) && displayedAccountIds.length === 0
}
hasMore={false}
isLoading={isLoading || isLoadingSearch}
footer={
<>
{displayedAccountIds.length > 0 && <div className='spacer' />}
<div className='column-footer'>
<Link className='button button--block' to='/home'>
<FormattedMessage
id='onboarding.follows.done'
defaultMessage='Done'
/>
</Link>
</div>
</>
}
emptyMessage={
mode === 'remove' ? (
<FormattedMessage
id='onboarding.follows.empty'
defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.'
/>
) : (
<FormattedMessage
id='lists.no_results_found'
defaultMessage='No results found.'
/>
)
}
>
{displayedAccountIds.map((accountId) => (
<Account id={accountId} key={accountId} withBio />
))}
</ScrollableList>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Follows;

View file

@ -1,91 +0,0 @@
import { useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Link, Switch, Route } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import illustration from '@/images/elephant_ui_conversation.svg';
import AccountCircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import EditNoteIcon from '@/material-icons/400-24px/edit_note.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import { focusCompose } from 'mastodon/actions/compose';
import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import Column from 'mastodon/features/ui/components/column';
import { me } from 'mastodon/initial_state';
import { useAppSelector } from 'mastodon/store';
import { assetHost } from 'mastodon/utils/config';
import { Step } from './components/step';
import { Follows } from './follows';
import { Profile } from './profile';
import { Share } from './share';
const messages = defineMessages({
template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' },
});
const Onboarding = () => {
const account = useAppSelector(state => state.getIn(['accounts', me]));
const dispatch = useDispatch();
const intl = useIntl();
const handleComposeClick = useCallback(() => {
dispatch(focusCompose(intl.formatMessage(messages.template)));
}, [dispatch, intl]);
return (
<Column>
{account ? (
<Switch>
<Route path='/start' exact>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<img src={illustration} alt='' className='onboarding__illustration' />
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
</div>
<div className='onboarding__steps'>
<Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
<Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
<Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
</div>
</Route>
<Route path='/start/profile' component={Profile} />
<Route path='/start/follows' component={Follows} />
<Route path='/start/share' component={Share} />
</Switch>
) : <NotSignedInIndicator />}
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
export default Onboarding;

View file

@ -1,162 +0,0 @@
import { useState, useMemo, useCallback, createRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import Toggle from 'react-toggle';
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import { updateAccount } from 'mastodon/actions/accounts';
import { Button } from 'mastodon/components/button';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { me } from 'mastodon/initial_state';
import { useAppSelector } from 'mastodon/store';
import { unescapeHTML } from 'mastodon/utils/html';
const messages = defineMessages({
uploadHeader: { id: 'onboarding.profile.upload_header', defaultMessage: 'Upload profile header' },
uploadAvatar: { id: 'onboarding.profile.upload_avatar', defaultMessage: 'Upload profile picture' },
});
const nullIfMissing = path => path.endsWith('missing.png') ? null : path;
export const Profile = () => {
const account = useAppSelector(state => state.getIn(['accounts', me]));
const [displayName, setDisplayName] = useState(account.get('display_name'));
const [note, setNote] = useState(unescapeHTML(account.get('note')));
const [avatar, setAvatar] = useState(null);
const [header, setHeader] = useState(null);
const [discoverable, setDiscoverable] = useState(account.get('discoverable'));
const [isSaving, setIsSaving] = useState(false);
const [errors, setErrors] = useState();
const avatarFileRef = createRef();
const headerFileRef = createRef();
const dispatch = useDispatch();
const intl = useIntl();
const history = useHistory();
const handleDisplayNameChange = useCallback(e => {
setDisplayName(e.target.value);
}, [setDisplayName]);
const handleNoteChange = useCallback(e => {
setNote(e.target.value);
}, [setNote]);
const handleDiscoverableChange = useCallback(e => {
setDiscoverable(e.target.checked);
}, [setDiscoverable]);
const handleAvatarChange = useCallback(e => {
setAvatar(e.target?.files?.[0]);
}, [setAvatar]);
const handleHeaderChange = useCallback(e => {
setHeader(e.target?.files?.[0]);
}, [setHeader]);
const avatarPreview = useMemo(() => avatar ? URL.createObjectURL(avatar) : nullIfMissing(account.get('avatar')), [avatar, account]);
const headerPreview = useMemo(() => header ? URL.createObjectURL(header) : nullIfMissing(account.get('header')), [header, account]);
const handleSubmit = useCallback(() => {
setIsSaving(true);
dispatch(updateAccount({
displayName,
note,
avatar,
header,
discoverable,
indexable: discoverable,
})).then(() => history.push('/start/follows')).catch(err => {
setIsSaving(false);
setErrors(err.response.data.details);
});
}, [dispatch, displayName, note, avatar, header, discoverable, history]);
return (
<>
<ColumnBackButton />
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.profile.title' defaultMessage='Profile setup' /></h3>
<p><FormattedMessage id='onboarding.profile.lead' defaultMessage='You can always complete this later in the settings, where even more customization options are available.' /></p>
</div>
<div className='simple_form'>
<div className='onboarding__profile'>
<label className={classNames('app-form__header-input', { selected: !!headerPreview, invalid: !!errors?.header })} title={intl.formatMessage(messages.uploadHeader)}>
<input
type='file'
hidden
ref={headerFileRef}
accept='image/*'
onChange={handleHeaderChange}
/>
{headerPreview && <img src={headerPreview} alt='' />}
<Icon icon={headerPreview ? EditIcon : AddPhotoAlternateIcon} />
</label>
<label className={classNames('app-form__avatar-input', { selected: !!avatarPreview, invalid: !!errors?.avatar })} title={intl.formatMessage(messages.uploadAvatar)}>
<input
type='file'
hidden
ref={avatarFileRef}
accept='image/*'
onChange={handleAvatarChange}
/>
{avatarPreview && <img src={avatarPreview} alt='' />}
<Icon icon={avatarPreview ? EditIcon : AddPhotoAlternateIcon} />
</label>
</div>
<div className={classNames('input with_block_label', { field_with_errors: !!errors?.display_name })}>
<label htmlFor='display_name'><FormattedMessage id='onboarding.profile.display_name' defaultMessage='Display name' /></label>
<span className='hint'><FormattedMessage id='onboarding.profile.display_name_hint' defaultMessage='Your full name or your fun name…' /></span>
<div className='label_input'>
<input id='display_name' type='text' value={displayName} onChange={handleDisplayNameChange} maxLength={30} />
</div>
</div>
<div className={classNames('input with_block_label', { field_with_errors: !!errors?.note })}>
<label htmlFor='note'><FormattedMessage id='onboarding.profile.note' defaultMessage='Bio' /></label>
<span className='hint'><FormattedMessage id='onboarding.profile.note_hint' defaultMessage='You can @mention other people or #hashtags…' /></span>
<div className='label_input'>
<textarea id='note' value={note} onChange={handleNoteChange} maxLength={500} />
</div>
</div>
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
<strong><FormattedMessage id='onboarding.profile.discoverable' defaultMessage='Make my profile discoverable' /></strong> <span className='recommended'><FormattedMessage id='recommended' defaultMessage='Recommended' /></span>
<span className='hint'><FormattedMessage id='onboarding.profile.discoverable_hint' defaultMessage='When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.' /></span>
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle checked={discoverable} onChange={handleDiscoverableChange} />
</div>
</div>
</label>
</div>
<div className='onboarding__footer'>
<Button block onClick={handleSubmit} disabled={isSaving}>{isSaving ? <LoadingIndicator /> : <FormattedMessage id='onboarding.profile.save_and_continue' defaultMessage='Save and continue' />}</Button>
</div>
</div>
</>
);
};

View file

@ -0,0 +1,329 @@
import { useState, useMemo, useCallback, createRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { useHistory } from 'react-router-dom';
import Toggle from 'react-toggle';
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { updateAccount } from 'mastodon/actions/accounts';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { me } from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { unescapeHTML } from 'mastodon/utils/html';
const messages = defineMessages({
title: {
id: 'onboarding.profile.title',
defaultMessage: 'Profile setup',
},
uploadHeader: {
id: 'onboarding.profile.upload_header',
defaultMessage: 'Upload profile header',
},
uploadAvatar: {
id: 'onboarding.profile.upload_avatar',
defaultMessage: 'Upload profile picture',
},
});
const nullIfMissing = (path: string) =>
path.endsWith('missing.png') ? null : path;
interface ApiAccountErrors {
display_name?: unknown;
note?: unknown;
avatar?: unknown;
header?: unknown;
}
export const Profile: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const account = useAppSelector((state) =>
me ? state.accounts.get(me) : undefined,
);
const [displayName, setDisplayName] = useState(account?.display_name ?? '');
const [note, setNote] = useState(
account ? (unescapeHTML(account.note) ?? '') : '',
);
const [avatar, setAvatar] = useState<File>();
const [header, setHeader] = useState<File>();
const [discoverable, setDiscoverable] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [errors, setErrors] = useState<ApiAccountErrors>();
const avatarFileRef = createRef<HTMLInputElement>();
const headerFileRef = createRef<HTMLInputElement>();
const dispatch = useAppDispatch();
const intl = useIntl();
const history = useHistory();
const handleDisplayNameChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.target.value);
},
[setDisplayName],
);
const handleNoteChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setNote(e.target.value);
},
[setNote],
);
const handleDiscoverableChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setDiscoverable(e.target.checked);
},
[setDiscoverable],
);
const handleAvatarChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setAvatar(e.target.files?.[0]);
},
[setAvatar],
);
const handleHeaderChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setHeader(e.target.files?.[0]);
},
[setHeader],
);
const avatarPreview = useMemo(
() =>
avatar
? URL.createObjectURL(avatar)
: nullIfMissing(account?.avatar ?? 'missing.png'),
[avatar, account],
);
const headerPreview = useMemo(
() =>
header
? URL.createObjectURL(header)
: nullIfMissing(account?.header ?? 'missing.png'),
[header, account],
);
const handleSubmit = useCallback(() => {
setIsSaving(true);
dispatch(
updateAccount({
displayName,
note,
avatar,
header,
discoverable,
indexable: discoverable,
}),
)
.then(() => {
history.push('/start/follows');
return '';
})
// eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable
.catch((err) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (err.response) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const { details }: { details: ApiAccountErrors } = err.response.data;
setErrors(details);
}
setIsSaving(false);
});
}, [dispatch, displayName, note, avatar, header, discoverable, history]);
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.title)}
>
<ColumnHeader
title={intl.formatMessage(messages.title)}
icon='person'
iconComponent={PersonIcon}
multiColumn={multiColumn}
/>
<div className='scrollable scrollable--flex'>
<div className='simple_form app-form'>
<div className='onboarding__profile'>
<label
className={classNames('app-form__header-input', {
selected: !!headerPreview,
invalid: !!errors?.header,
})}
title={intl.formatMessage(messages.uploadHeader)}
>
<input
type='file'
hidden
ref={headerFileRef}
accept='image/*'
onChange={handleHeaderChange}
/>
{headerPreview && <img src={headerPreview} alt='' />}
<Icon
id=''
icon={headerPreview ? EditIcon : AddPhotoAlternateIcon}
/>
</label>
<label
className={classNames('app-form__avatar-input', {
selected: !!avatarPreview,
invalid: !!errors?.avatar,
})}
title={intl.formatMessage(messages.uploadAvatar)}
>
<input
type='file'
hidden
ref={avatarFileRef}
accept='image/*'
onChange={handleAvatarChange}
/>
{avatarPreview && <img src={avatarPreview} alt='' />}
<Icon
id=''
icon={avatarPreview ? EditIcon : AddPhotoAlternateIcon}
/>
</label>
</div>
<div className='fields-group'>
<div
className={classNames('input with_block_label', {
field_with_errors: !!errors?.display_name,
})}
>
<label htmlFor='display_name'>
<FormattedMessage
id='onboarding.profile.display_name'
defaultMessage='Display name'
/>
</label>
<span className='hint'>
<FormattedMessage
id='onboarding.profile.display_name_hint'
defaultMessage='Your full name or your fun name…'
/>
</span>
<div className='label_input'>
<input
id='display_name'
type='text'
value={displayName}
onChange={handleDisplayNameChange}
maxLength={30}
/>
</div>
</div>
</div>
<div className='fields-group'>
<div
className={classNames('input with_block_label', {
field_with_errors: !!errors?.note,
})}
>
<label htmlFor='note'>
<FormattedMessage
id='onboarding.profile.note'
defaultMessage='Bio'
/>
</label>
<span className='hint'>
<FormattedMessage
id='onboarding.profile.note_hint'
defaultMessage='You can @mention other people or #hashtags…'
/>
</span>
<div className='label_input'>
<textarea
id='note'
value={note}
onChange={handleNoteChange}
maxLength={500}
/>
</div>
</div>
</div>
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
<strong>
<FormattedMessage
id='onboarding.profile.discoverable'
defaultMessage='Make my profile discoverable'
/>
</strong>{' '}
<span className='recommended'>
<FormattedMessage
id='recommended'
defaultMessage='Recommended'
/>
</span>
<span className='hint'>
<FormattedMessage
id='onboarding.profile.discoverable_hint'
defaultMessage='When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.'
/>
</span>
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle
checked={discoverable}
onChange={handleDiscoverableChange}
/>
</div>
</div>
</label>
</div>
<div className='spacer' />
<div className='column-footer'>
<Button block onClick={handleSubmit} disabled={isSaving}>
{isSaving ? (
<LoadingIndicator />
) : (
<FormattedMessage
id='onboarding.profile.save_and_continue'
defaultMessage='Save and continue'
/>
)}
</Button>
</div>
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Profile;

View file

@ -1,120 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import SwipeableViews from 'react-swipeable-views';
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { CopyPasteText } from 'mastodon/components/copy_paste_text';
import { Icon } from 'mastodon/components/icon';
import { me, domain } from 'mastodon/initial_state';
import { useAppSelector } from 'mastodon/store';
const messages = defineMessages({
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
});
class TipCarousel extends PureComponent {
static propTypes = {
children: PropTypes.node,
};
state = {
index: 0,
};
handleSwipe = index => {
this.setState({ index });
};
handleChangeIndex = e => {
this.setState({ index: Number(e.currentTarget.getAttribute('data-index')) });
};
handleKeyDown = e => {
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
this.setState(({ index }, { children }) => ({ index: Math.abs(index - 1) % children.length }));
break;
case 'ArrowRight':
e.preventDefault();
this.setState(({ index }, { children }) => ({ index: (index + 1) % children.length }));
break;
}
};
render () {
const { children } = this.props;
const { index } = this.state;
return (
<div className='tip-carousel' tabIndex='0' onKeyDown={this.handleKeyDown}>
<SwipeableViews onChangeIndex={this.handleSwipe} index={index} enableMouseEvents tabIndex='-1'>
{children}
</SwipeableViews>
<div className='media-modal__pagination'>
{children.map((_, i) => (
<button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
{i + 1}
</button>
))}
</div>
</div>
);
}
}
export const Share = () => {
const account = useAppSelector(state => state.getIn(['accounts', me]));
const intl = useIntl();
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
return (
<>
<ColumnBackButton />
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
<p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
</div>
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
<TipCarousel>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
</TipCarousel>
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
<div className='onboarding__links'>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
<div className='onboarding__footer'>
<Link className='link-button' to='/start'><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></Link>
</div>
</div>
</>
);
};

View file

@ -142,7 +142,7 @@ class PublicTimeline extends PureComponent {
</ColumnHeader>
<StatusListContainer
prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the fediverse that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
trackScroll={!pinned}

View file

@ -11,13 +11,13 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
import ColumnHeader from '../../components/column_header';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
const messages = defineMessages({
@ -88,7 +88,7 @@ class Reblogs extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>

View file

@ -9,58 +9,7 @@ import { Link } from 'react-router-dom';
import { Button } from 'mastodon/components/button';
import Column from 'mastodon/components/column';
import { autoPlayGif } from 'mastodon/initial_state';
class GIF extends PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
staticSrc: PropTypes.string.isRequired,
className: PropTypes.string,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
};
state = {
hovering: false,
};
handleMouseEnter = () => {
const { animate } = this.props;
if (!animate) {
this.setState({ hovering: true });
}
};
handleMouseLeave = () => {
const { animate } = this.props;
if (!animate) {
this.setState({ hovering: false });
}
};
render () {
const { src, staticSrc, className, animate } = this.props;
const { hovering } = this.state;
return (
<img
className={className}
src={(hovering || animate) ? src : staticSrc}
alt=''
role='presentation'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
/>
);
}
}
import { GIF } from 'mastodon/components/gif';
class CopyButton extends PureComponent {

View file

@ -1,56 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
});
class BundleModalError extends PureComponent {
static propTypes = {
onRetry: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleRetry = () => {
this.props.onRetry();
};
render () {
const { onClose, intl: { formatMessage } } = this.props;
// Keep the markup in sync with <ModalLoading />
// (make sure they have the same dimensions)
return (
<div className='modal-root__modal error-modal'>
<div className='error-modal__body'>
<IconButton title={formatMessage(messages.retry)} icon='refresh' iconComponent={RefreshIcon} onClick={this.handleRetry} size={64} />
{formatMessage(messages.error)}
</div>
<div className='error-modal__footer'>
<div>
<button
onClick={onClose}
className='error-modal__nav onboarding-modal__skip'
>
{formatMessage(messages.close)}
</button>
</div>
</div>
</div>
);
}
}
export default injectIntl(BundleModalError);

View file

@ -1,4 +1,4 @@
import Column from 'mastodon/components/column';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import type { Props as ColumnHeaderProps } from 'mastodon/components/column_header';

View file

@ -8,7 +8,7 @@ import { scrollRight } from '../../../scroll';
import BundleContainer from '../containers/bundle_container';
import {
Compose,
NotificationsWrapper,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
@ -30,7 +30,7 @@ import NavigationPanel from './navigation_panel';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
'NOTIFICATIONS': NotificationsWrapper,
'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline,

View file

@ -0,0 +1,43 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAppSelector } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
title: {
id: 'confirmations.follow_to_list.title',
defaultMessage: 'Follow user?',
},
confirm: {
id: 'confirmations.follow_to_list.confirm',
defaultMessage: 'Follow and add to list',
},
});
export const ConfirmFollowToListModal: React.FC<
{
accountId: string;
onConfirm: () => void;
} & BaseConfirmationModalProps
> = ({ accountId, onConfirm, onClose }) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(accountId));
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={
<FormattedMessage
id='confirmations.follow_to_list.message'
defaultMessage='You need to be following {name} to add them to a list.'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
}
confirm={intl.formatMessage(messages.confirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -6,3 +6,4 @@ export { ConfirmEditStatusModal } from './edit_status';
export { ConfirmUnfollowModal } from './unfollow';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';

View file

@ -1,18 +0,0 @@
import { LoadingIndicator } from '../../../components/loading_indicator';
// Keep the markup in sync with <BundleModalError />
// (make sure they have the same dimensions)
const ModalLoading = () => (
<div className='modal-root__modal error-modal'>
<div className='error-modal__body'>
<LoadingIndicator />
</div>
<div className='error-modal__footer'>
<div>
<button className='error-modal__nav onboarding-modal__skip' />
</div>
</div>
</div>
);
export default ModalLoading;

View file

@ -0,0 +1,61 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button } from 'mastodon/components/button';
import { GIF } from 'mastodon/components/gif';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
export const ModalPlaceholder: React.FC<{
loading: boolean;
onClose: (arg0: string | undefined, arg1: boolean) => void;
onRetry?: () => void;
}> = ({ loading, onClose, onRetry }) => {
const handleClose = useCallback(() => {
onClose(undefined, false);
}, [onClose]);
const handleRetry = useCallback(() => {
if (onRetry) onRetry();
}, [onRetry]);
return (
<div className='modal-root__modal modal-placeholder' aria-busy={loading}>
{loading ? (
<LoadingIndicator />
) : (
<div className='modal-placeholder__error'>
<GIF
src='/oops.gif'
staticSrc='/oops.png'
className='modal-placeholder__error__image'
/>
<div className='modal-placeholder__error__message'>
<p>
<FormattedMessage
id='bundle_modal_error.message'
defaultMessage='Something went wrong while loading this screen.'
/>
</p>
<div className='modal-placeholder__error__message__actions'>
<Button onClick={handleRetry}>
<FormattedMessage
id='bundle_modal_error.retry'
defaultMessage='Try again'
/>
</Button>
<Button onClick={handleClose} className='button button-tertiary'>
<FormattedMessage
id='bundle_modal_error.close'
defaultMessage='Close'
/>
</Button>
</div>
</div>
</div>
)}
</div>
);
};

View file

@ -26,7 +26,6 @@ import BundleContainer from '../containers/bundle_container';
import ActionsModal from './actions_modal';
import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal';
import BundleModalError from './bundle_modal_error';
import {
ConfirmationModal,
ConfirmDeleteStatusModal,
@ -36,11 +35,12 @@ import {
ConfirmUnfollowModal,
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
ConfirmFollowToListModal,
} from './confirmation_modals';
import FocalPointModal from './focal_point_modal';
import ImageModal from './image_modal';
import MediaModal from './media_modal';
import ModalLoading from './modal_loading';
import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal';
export const MODAL_COMPONENTS = {
@ -57,6 +57,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
@ -105,14 +106,16 @@ export default class ModalRoot extends PureComponent {
this.setState({ backgroundColor: color });
};
renderLoading = modalId => () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
renderLoading = () => {
const { onClose } = this.props;
return <ModalPlaceholder loading onClose={onClose} />;
};
renderError = (props) => {
const { onClose } = this.props;
return <BundleModalError {...props} onClose={onClose} />;
return <ModalPlaceholder {...props} onClose={onClose} />;
};
handleClose = (ignoreFocus = false) => {
@ -134,7 +137,7 @@ export default class ModalRoot extends PureComponent {
<Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
{visible && (
<>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => {
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;

View file

@ -49,7 +49,7 @@ import {
Favourites,
DirectTimeline,
HashtagTimeline,
NotificationsWrapper,
Notifications,
NotificationRequests,
NotificationRequest,
FollowRequests,
@ -66,8 +66,9 @@ import {
Mutes,
PinnedStatuses,
Directory,
OnboardingProfile,
OnboardingFollows,
Explore,
Onboarding,
About,
PrivacyPolicy,
} from './util/async-components';
@ -211,7 +212,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/lists/:id/edit' component={ListEdit} content={children} />
<WrappedRoute path='/lists/:id/members' component={ListMembers} content={children} />
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={NotificationsWrapper} content={children} exact />
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
<WrappedRoute path='/notifications/requests/:id' component={NotificationRequest} content={children} exact />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
@ -219,7 +220,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/start' component={Onboarding} content={children} />
<WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} />
<WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />

View file

@ -7,15 +7,7 @@ export function Compose () {
}
export function Notifications () {
return import(/* webpackChunkName: "features/notifications_v1" */'../../notifications');
}
export function Notifications_v2 () {
return import(/* webpackChunkName: "features/notifications_v2" */'../../notifications_v2');
}
export function NotificationsWrapper () {
return import(/* webpackChunkName: "features/notifications" */'../../notifications_wrapper');
return import(/* webpackChunkName: "features/notifications" */'../../notifications_v2');
}
export function HomeTimeline () {
@ -166,8 +158,12 @@ export function Directory () {
return import(/* webpackChunkName: "features/directory" */'../../directory');
}
export function Onboarding () {
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding');
export function OnboardingProfile () {
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/profile');
}
export function OnboardingFollows () {
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/follows');
}
export function CompareHistoryModal () {

View file

@ -71,7 +71,6 @@
"bundle_column_error.return": "Keer terug na die tuisblad",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Sluit",
"bundle_modal_error.message": "Die laai van die komponent het iewers skeefgeloop.",
"bundle_modal_error.retry": "Probeer weer",
"closed_registrations_modal.find_another_server": "Vind 'n ander bediener",
"closed_registrations_modal.preamble": "Omdat Mastodon gedesentraliseer is, kan jy op hierdie bediener enigiemand volg en met enigiemand gesels, al is jou rekening op n ander bediener. Jy kan selfs jou eie bediener by die netwerk voeg!",
@ -132,8 +131,6 @@
"directory.local": "Slegs van {domain}",
"disabled_account_banner.account_settings": "Rekeninginstellings",
"disabled_account_banner.text": "Jou rekening {disabledAccount} is tans gedeaktiveer.",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"embed.instructions": "Bed hierdie plasing op jou webblad in met die kode wat jy hier onder kan kopieer.",
"embed.preview": "Dit sal so lyk:",
"emoji_button.activity": "Aktiwiteit",
@ -248,19 +245,6 @@
"notifications.permission_denied_alert": "Lessenaarkennisgewings kan nie geaktiveer word nie omdat 'n webblaaier toegewing voorheen geweier was",
"notifications_permission_banner.enable": "Aktiveer lessenaarkennissgewings",
"notifications_permission_banner.how_to_control": "Om kennisgewings te ontvang wanner Mastodon nie oop is nie, aktiveer lessenaarkennisgewings. Jy kan beheer watter spesifieke tipe interaksies lessenaarkennisgewings genereer deur die {icon} knoppie hier bo sodra hulle geaktiveer is.",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"privacy.change": "Verander privaatheid van plasing",
"privacy.public.short": "Publiek",
"privacy_policy.last_updated": "Laaste bywerking op {date}",

View file

@ -81,7 +81,6 @@
"bundle_column_error.routing.body": "No se podió trobar la pachina solicitada. Yes seguro que la URL en a barra d'adrezas ye correcta?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Zarrar",
"bundle_modal_error.message": "Bella cosa salió malament en cargar este component.",
"bundle_modal_error.retry": "Intenta-lo de nuevo",
"closed_registrations.other_server_instructions": "Como Mastodon ye descentralizau, puetz creyar una cuenta en unatro servidor y seguir interactuando con este.",
"closed_registrations_modal.description": "La creyación d'una cuenta en {domain} no ye posible actualment, pero tiene en cuenta que no amenestes una cuenta especificament en {domain} pa usar Mastodon.",
@ -155,8 +154,6 @@
"disabled_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada.",
"dismissable_banner.community_timeline": "Estas son las publicacions publicas mas recients de personas que las suyas cuentas son alochadas en {domain}.",
"dismissable_banner.dismiss": "Descartar",
"dismissable_banner.explore_links": "Estas noticias son estando discutidas per personas en este y atros servidors d'o ret descentralizau en este momento.",
"dismissable_banner.explore_tags": "Estas tendencias son ganando popularidat entre la chent en este y atros servidors d'o ret descentralizau en este momento.",
"embed.instructions": "Anyade esta publicación a lo tuyo puesto web con o siguient codigo.",
"embed.preview": "Asinas ye como se veyerá:",
"emoji_button.activity": "Actividat",
@ -359,19 +356,6 @@
"notifications_permission_banner.enable": "Habilitar notificacions d'escritorio",
"notifications_permission_banner.how_to_control": "Pa recibir notificacions quan Mastodon no sía ubierto, habilite las notificacions d'escritorio. Puetz controlar con precisión qué tipos d'interaccions cheneran notificacions d'escritorio a traviés d'o botón {icon} d'alto una vegada que sían habilitadas.",
"notifications_permission_banner.title": "Nunca te pierdas cosa",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"picture_in_picture.restore": "Restaurar",
"poll.closed": "Zarrada",
"poll.refresh": "Actualizar",

View file

@ -110,7 +110,6 @@
"bundle_column_error.routing.body": "تعذر العثور على الصفحة المطلوبة. هل أنت متأكد من أنّ الرابط التشعبي URL في شريط العناوين صحيح؟",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "إغلاق",
"bundle_modal_error.message": "لقد حدث خطأ ما أثناء تحميل هذا العنصر.",
"bundle_modal_error.retry": "إعادة المُحاولة",
"closed_registrations.other_server_instructions": "بما أن ماستدون لامركزي، يمكنك إنشاء حساب على خادم آخر للاستمرار في التفاعل مع هذا الخادم.",
"closed_registrations_modal.description": "لا يمكن إنشاء حساب على {domain} حاليا، ولكن على فكرة لست بحاجة إلى حساب على {domain} بذاته لاستخدام ماستدون.",
@ -211,10 +210,6 @@
"disabled_account_banner.text": "حسابك {disabledAccount} معطل حاليا.",
"dismissable_banner.community_timeline": "هذه هي أحدث المنشورات العامة من أشخاص تُستضاف حساباتهم على {domain}.",
"dismissable_banner.dismiss": "رفض",
"dismissable_banner.explore_links": "هذه هي القصص الإخبارية الأكثر مشاركة على الشبكة الاجتماعية اليوم. القصص الإخبارية الأحدث التي تنشرها أشخاص مختلفة هي مصنفة في الأعلى.",
"dismissable_banner.explore_statuses": "هذه هي المنشورات الرائجة على الشبكات الاجتماعيّة اليوم. تظهر المنشورات المعاد نشرها والحائزة على مفضّلات أكثر في مرتبة عليا.",
"dismissable_banner.explore_tags": "هذه هي الوسوم تكتسب جذب الاهتمام حاليًا على الويب الاجتماعي. الوسوم التي يستخدمها مختلف الناس تحتل مرتبة عليا.",
"dismissable_banner.public_timeline": "هذه هي أحدث المنشورات العامة من الناس على الشبكة الاجتماعية التي يتبعها الناس على {domain}.",
"domain_block_modal.block": "حظر الخادم",
"domain_block_modal.block_account_instead": "أحجب @{name} بدلاً من ذلك",
"domain_block_modal.they_can_interact_with_old_posts": "يمكن للأشخاص من هذا الخادم التفاعل مع منشوراتك القديمة.",
@ -563,44 +558,17 @@
"notifications_permission_banner.enable": "تفعيل إشعارات سطح المكتب",
"notifications_permission_banner.how_to_control": "لتلقي الإشعارات عندما لا يكون ماستدون مفتوح، قم بتفعيل إشعارات سطح المكتب، يمكنك التحكم بدقة في أنواع التفاعلات التي تولد إشعارات سطح المكتب من خلال زر الـ{icon} أعلاه بمجرد تفعيلها.",
"notifications_permission_banner.title": "لا تفوت شيئاً أبداً",
"onboarding.action.back": "تراجع",
"onboarding.actions.back": "تراجع",
"onboarding.actions.go_to_explore": "خذني إلى المتداولة",
"onboarding.actions.go_to_home": "خذني إلى وصلات خيطي الرئيس",
"onboarding.compose.template": "مرحبا #ماستدون!",
"onboarding.follows.empty": "نأسف، لا يمكن عرض نتائج في الوقت الحالي. جرب البحث أو انتقل لصفحة الاستكشاف لإيجاد أشخاص للمتابعة، أو حاول مرة أخرى.",
"onboarding.follows.lead": "مقتطفات خيطك الرئيس هي الطريقة الأساسية لتجربة ماستدون. كلما زاد عدد الأشخاص الذين تتبعهم، كلما زاد خيط أخبارك نشاطا وإثارة للاهتمام. بداية، إليك بعض الاقتراحات:",
"onboarding.follows.title": "أضفِ طابعا شخصيا على موجزات خيطك الرئيس",
"onboarding.profile.discoverable": "اجعل ملفي الشخصي قابلاً للاكتشاف",
"onboarding.profile.discoverable_hint": "عندما تختار تفعيل إمكانية الاكتشاف على ماستدون، قد تظهر منشوراتك في نتائج البحث والمواضيع الرائجة، وقد يتم اقتراح ملفك الشخصي لأشخاص ذوي اهتمامات مماثلة معك.",
"onboarding.profile.display_name": "الاسم العلني",
"onboarding.profile.display_name_hint": "اسمك الكامل أو اسمك المرح…",
"onboarding.profile.lead": "يمكنك دائمًا إكمال ذلك لاحقًا في الإعدادات، حيث يتوفر المزيد من خيارات التخصيص.",
"onboarding.profile.note": "نبذة عنك",
"onboarding.profile.note_hint": "يمكنك @ذِكر أشخاص آخرين أو استعمال #الوسوم…",
"onboarding.profile.save_and_continue": "حفظ و إستمرار",
"onboarding.profile.title": "إعداد الملف الشخصي",
"onboarding.profile.upload_avatar": "تحميل صورة الملف الشخصي",
"onboarding.profile.upload_header": "تحميل رأسية الملف الشخصي",
"onboarding.share.lead": "اسمح للأشخاص بمعرفة إمكانية الوصول إليك على ماستدون!",
"onboarding.share.message": "أنا {username} في #Mastodon! تعال لمتابعتي على {url}",
"onboarding.share.next_steps": "الخطوات المحتملة التالية:",
"onboarding.share.title": "شارك ملفك التعريفي",
"onboarding.start.lead": "أنت الآن جزء من ماستدون، منصة إعلامية اجتماعية فريدة من نوعها ولا مركزية حيث أنت - وليست الخوارزميات - من يقوم بضبط تجربتك الخاصة. دعنا نبدأ على هذه الحدود الاجتماعية الجديدة:",
"onboarding.start.skip": "ألست بحاجة للمساعدة للبداية؟",
"onboarding.start.title": "لقد نجحت!",
"onboarding.steps.follow_people.body": "إن متابعة الأشخاص المثيرين للاهتمام هي غاية ماستدون.",
"onboarding.steps.follow_people.title": "أضفِ طابعا شخصيا على خيطك الرئيس",
"onboarding.steps.publish_status.body": "قل مرحبا للعالَم عبر نصّ أو صور أو فيديوهات أو استطلاعات رأي {emoji}",
"onboarding.steps.publish_status.title": "قم بإنشاء أول منشور لك",
"onboarding.steps.setup_profile.body": "قم بتعزيز تفاعلاتك عبر الحصول على مِلَفّ شخصي شامل.",
"onboarding.steps.setup_profile.title": "قم بتخصيص ملفك التعريفي",
"onboarding.steps.share_profile.body": "أخبر أصدقائك بكيفية العثور عليك على ماستدون",
"onboarding.steps.share_profile.title": "شارك مِلَفّ ماستدون التعريفي الخاص بك",
"onboarding.tips.2fa": "<strong>هل تعلم؟</strong> يمكنك تأمين حسابك عن طريق إعداد المصادقة ذات عاملين في إعدادات حسابك. تعمل مع أي تطبيق TOTP من اختيارك، لا حاجة لرقم هاتف!",
"onboarding.tips.accounts_from_other_servers": "<strong>هل تعلم؟</strong> لأن ماستدون لامركزية فإن بعض الحسابات التي تصادفها ستكون مستضافة على خوادم غير خادمك. ومع ذلك يمكنك التفاعل معها بسلاسة! خادمهم هو النصف الآخر من اسم المستخدم خاصتهم!",
"onboarding.tips.migration": "<strong>هل تعلم؟</strong> إذا شعرت بأن {domain} ليس خياراً ممتازاً لك في المستقبل، فيمكنك الانتقال إلى خادم ماستدون آخر دون خسارة متابعيك. يمكنك حتى استضافة خادمك الخاص!",
"onboarding.tips.verification": "<strong>هل تعلم؟</strong> يمكنك تأكيد حسابك عبر وضع رابط إلى ملف ماستدون الشخصي الخاص بك في موقعك الخاص وإضافة رابط موقعك على ملفك الشخصي. لا حاجة لأي رسوم أو مستندات!",
"password_confirmation.exceeds_maxlength": "تأكيد كلمة المرور يتجاوز الحد الأقصى لطول كلمة المرور",
"password_confirmation.mismatching": "تأكيد كلمة المرور غير مطابق",
"picture_in_picture.restore": "ضعها مرة أخرى",

View file

@ -66,7 +66,6 @@
"bundle_column_error.return": "Volver al aniciu",
"bundle_column_error.routing.body": "Nun se pudo atopar la páxina solicitada. ¿De xuru que la URL de la barra de direiciones ta bien escrita?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.message": "Asocedió daqué malo mentanto se cargaba esti componente.",
"closed_registrations.other_server_instructions": "Darréu que Mastodon ye una rede social descentralizada, pues crear una cuenta n'otru sirvidor y siguir interactuando con esti.",
"closed_registrations_modal.description": "Anguaño nun ye posible crear cuentes en {domain}, mas ten en cuenta que nun precises una cuenta nesti sirvidor pa usar Mastodon.",
"closed_registrations_modal.find_another_server": "Atopar otru sirvidor",
@ -129,8 +128,6 @@
"directory.recently_active": "Con actividá recién",
"dismissable_banner.community_timeline": "Esta seición contién los artículos públicos más actuales de los perfiles agospiaos nel dominiu {domain}.",
"dismissable_banner.dismiss": "Escartar",
"dismissable_banner.explore_tags": "Esta seición contién les etiquetes del fediversu que tán ganando popularidá güei. Les etiquetes más usaes polos perfiles apaecen no cimero.",
"dismissable_banner.public_timeline": "Esta seición contién los artículos más nuevos de les persones na web social que les persones de {domain} siguen.",
"embed.instructions": "Empotra esti artículu nel to sitiu web copiando'l códigu d'abaxo.",
"embed.preview": "Va apaecer asina:",
"emoji_button.activity": "Actividá",
@ -309,10 +306,6 @@
"notifications.mark_as_read": "Marcar tolos avisos como lleíos",
"notifications.permission_required": "Los avisos d'escritoriu nun tán disponibles porque nun se concedió'l permisu riquíu.",
"onboarding.profile.note_hint": "Pues @mentar a otros perfiles o poner #etiquetes…",
"onboarding.start.lead": "Yá yes parte de Mastodon, una plataforma social multimedia descentralizada onde tu y non un algoritmu, personalices la to esperiencia. Vamos presentate esti llugar social nuevu:",
"onboarding.start.skip": "¿Nun precises ayuda pa comenzar?",
"onboarding.steps.follow_people.body": "Mastodon trata namás de siguir a cuentes interesantes.",
"onboarding.steps.publish_status.body": "Saluda al mundu con semeyes, vídeos, testu o encuestes {emoji}",
"password_confirmation.exceeds_maxlength": "La contraseña de confirmación supera la llongura de caráuteres máxima",
"password_confirmation.mismatching": "La contraseña de confirmación nun concasa",
"poll.closed": "Finó",

View file

@ -110,7 +110,6 @@
"bundle_column_error.routing.body": "Запытаная старонка не знойдзена. Вы ўпэўнены, што URL у адрасным радку правільны?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Закрыць",
"bundle_modal_error.message": "Нешта пайшло не так падчас загрузкі гэтага кампанента.",
"bundle_modal_error.retry": "Паспрабуйце зноў",
"closed_registrations.other_server_instructions": "Паколькі Mastodon дэцэнтралізаваны, вы можаце стварыць уліковы запіс на іншым серверы і працягваць узаемадзейнічаць з ім.",
"closed_registrations_modal.description": "Стварэнне ўліковага запісу на {domain} цяпер немагчыма. Заўважце, што няма неабходнасці мець уліковы запіс менавіта на {domain}, каб выкарыстоўваць Mastodon.",
@ -213,10 +212,6 @@
"disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.",
"dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.",
"dismissable_banner.dismiss": "Адхіліць",
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца цяпер на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
"dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.",
"dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі",
"dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.",
"domain_block_modal.block": "Заблакіраваць сервер",
"domain_block_modal.block_account_instead": "Заблакіраваць @{name} замест гэтага",
"domain_block_modal.they_can_interact_with_old_posts": "Людзі з гэтага сервера змогуць узаемадзейнічаць з вашымі старымі допісамі.",
@ -568,44 +563,17 @@
"notifications_permission_banner.enable": "Уключыць апавяшчэнні на працоўным стале",
"notifications_permission_banner.how_to_control": "Каб атрымліваць апавяшчэнні, калі Mastodon не адкрыты, уключыце апавяшчэнні працоўнага стала. Вы зможаце дакладна кантраляваць, якія падзеі будуць ствараць апавяшчэнні з дапамогай {icon} кнопкі, як толькі яны будуць уключаны.",
"notifications_permission_banner.title": "Не прапусціце нічога",
"onboarding.action.back": "Прыняць мяне назад",
"onboarding.actions.back": "Прыняць мяне назад",
"onboarding.actions.go_to_explore": "Паглядзіце, што ў трэндзе",
"onboarding.actions.go_to_home": "Перайдзіце на свой хатні канал",
"onboarding.compose.template": "Прывітанне, #Mastodon!",
"onboarding.follows.empty": "На жаль, зараз немагчыма паказаць вынікі. Вы можаце паспрабаваць выкарыстоўваць пошук і праглядзець старонку агляду, каб знайсці людзей, на якіх можна падпісацца, або паўтарыце спробу пазней.",
"onboarding.follows.lead": "Вы самі ствараеце свой хатні канал. Чым больш людзей вы падпішаце, тым больш актыўна і цікавей гэта будзе. Гэтыя профілі могуць стаць добрай адпраўной кропкай — вы заўсёды можаце адмяніць падпіску на іх пазней!",
"onboarding.follows.title": "Папулярна на Mastodon",
"onboarding.profile.discoverable": "Зрабіць мой профіль бачным",
"onboarding.profile.discoverable_hint": "Калі вы звяртаецеся да адкрытасці на Mastodon, вашы паведамленні могуць з'яўляцца ў выніках пошуку і тэндэнцый, а ваш профіль можа быць прапанаваны людзям з такімі ж інтарэсамі.",
"onboarding.profile.display_name": "Бачнае імя",
"onboarding.profile.display_name_hint": "Ваша поўнае імя або ваш псеўданім…",
"onboarding.profile.lead": "Вы заўсёды можаце выканаць гэта пазней у Наладах, дзе даступна яшчэ больш параметраў.",
"onboarding.profile.note": "Біяграфія",
"onboarding.profile.note_hint": "Вы можаце @згадаць іншых людзей або выкарыстоўваць #хэштэгі…",
"onboarding.profile.save_and_continue": "Захаваць і працягнуць",
"onboarding.profile.title": "Налады профілю",
"onboarding.profile.upload_avatar": "Загрузіць фота профілю",
"onboarding.profile.upload_header": "Загрузіць шапку профілю",
"onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!",
"onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}",
"onboarding.share.next_steps": "Магчымыя наступныя крокі:",
"onboarding.share.title": "Абагульце свой профіль",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "Вы зрабілі гэта!",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.publish_status.title": "Зрабіце свой першы допіс",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Абагульць ваш профіль у Mastodon",
"onboarding.tips.2fa": "<strong>Ці вы ведаеце?</strong> Вы можаце абараніць свой уліковы запіс, усталяваўшы двухфактарную аўтэнтыфікацыю ў наладах уліковага запісу. Яна працуе з любой праграмай TOTP на ваш выбар, нумар тэлефона не патрэбны!",
"onboarding.tips.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!",
"onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!",
"onboarding.tips.verification": "<strong>Ці вы ведаеце?</strong> Вы можаце пацвердзіць свой уліковы запіс, размясціўшы спасылку на свой профіль Mastodon на сваім вэб-сайце і дадаўшы вэб-сайт у свой профіль. Ніякіх збораў і дакументаў не патрабуецца!",
"password_confirmation.exceeds_maxlength": "Пароль пацьверджання перавышае максімальна дапушчальную даўжыню",
"password_confirmation.mismatching": "Пароль пацьверджання не супадае",
"picture_in_picture.restore": "Вярніце назад",

View file

@ -22,7 +22,7 @@
"account.cancel_follow_request": "Оттегляне на заявката за последване",
"account.copy": "Копиране на връзка към профила",
"account.direct": "Частно споменаване на @{name}",
"account.disable_notifications": "Сприране на известия при публикуване от @{name}",
"account.disable_notifications": "Спиране на известяване при публикуване от @{name}",
"account.domain_blocked": "Блокиран домейн",
"account.edit_profile": "Редактиране на профила",
"account.enable_notifications": "Известяване при публикуване от @{name}",
@ -97,7 +97,7 @@
"annual_report.summary.here_it_is": "Ето преглед на вашата {year} година:",
"annual_report.summary.highlighted_post.by_favourites": "най-правено като любима публикация",
"annual_report.summary.highlighted_post.by_reblogs": "най-подсилваната публикация",
"annual_report.summary.highlighted_post.by_replies": "публикации с най-много отговори",
"annual_report.summary.highlighted_post.by_replies": "публикация с най-много отговори",
"annual_report.summary.highlighted_post.possessive": "на {name}",
"annual_report.summary.most_used_app.most_used_app": "най-употребявано приложение",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "най-употребяван хаштаг",
@ -128,7 +128,7 @@
"bundle_column_error.routing.body": "Заявената страница не може да се намери. Сигурни ли сте, че URL адресът в адресната лента е правилен?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Затваряне",
"bundle_modal_error.message": "Нещо се обърка, зареждайки компонента.",
"bundle_modal_error.message": "Нещо се обърка, зареждайки този екран.",
"bundle_modal_error.retry": "Нов опит",
"closed_registrations.other_server_instructions": "Oткак e децентрализиранa Mastodon, може да създадете акаунт на друг сървър и още може да взаимодействате с този.",
"closed_registrations_modal.description": "Създаването на акаунт в {domain} сега не е възможно, но обърнете внимание, че нямате нужда от акаунт конкретно на {domain}, за да ползвате Mastodon.",
@ -138,7 +138,7 @@
"column.about": "Относно",
"column.blocks": "Блокирани потребители",
"column.bookmarks": "Отметки",
"column.community": "Локален инфопоток",
"column.community": "Локална хронология",
"column.create_list": "Създаване на списък",
"column.direct": "Частни споменавания",
"column.directory": "Разглеждане на профили",
@ -153,7 +153,7 @@
"column.mutes": "Заглушени потребители",
"column.notifications": "Известия",
"column.pins": "Закачени публикации",
"column.public": "Федериран инфопоток",
"column.public": "Федеративна хронология",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Скриване на настройките",
"column_header.moveLeft_settings": "Преместване на колона вляво",
@ -161,6 +161,7 @@
"column_header.pin": "Закачане",
"column_header.show_settings": "Показване на настройките",
"column_header.unpin": "Разкачане",
"column_search.cancel": "Отказ",
"column_subheading.settings": "Настройки",
"community.column_settings.local_only": "Само локално",
"community.column_settings.media_only": "Само мултимедия",
@ -214,7 +215,7 @@
"confirmations.reply.message": "Отговарянето сега ще замени съобщението, което в момента съставяте. Сигурни ли сте, че искате да продължите?",
"confirmations.reply.title": "Презаписвате ли публикацията?",
"confirmations.unfollow.confirm": "Без следване",
"confirmations.unfollow.message": "Наистина ли искате да не следвате {name}?",
"confirmations.unfollow.message": "Наистина ли искате вече да не следвате {name}?",
"confirmations.unfollow.title": "Спирате ли да следвате потребителя?",
"content_warning.hide": "Скриване на публ.",
"content_warning.show": "Нека се покаже",
@ -234,16 +235,18 @@
"disabled_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен.",
"dismissable_banner.community_timeline": "Ето най-скорошните публични публикации от хора, чиито акаунти са разположени в {domain}.",
"dismissable_banner.dismiss": "Отхвърляне",
"dismissable_banner.explore_links": "Това са най-споделяните новини в социалната мрежа днес. По-нови истории, споделени от повече хора се показват по-напред.",
"dismissable_banner.explore_statuses": "Има публикации през социалната мрежа, които днес набират популярност. По-новите публикации с повече подсилвания и любими са класирани по-високо.",
"dismissable_banner.explore_tags": "Тези хаштагове сега набират популярност сред хората в този и други сървъри на децентрализирата мрежа.",
"dismissable_banner.public_timeline": "Ето най-новите обществени публикации от хора в социална мрежа, която хората в {domain} следват.",
"dismissable_banner.explore_links": "Тези новинарски истории са най-споделяните във федивселената днес. По-нови новинарски истории, публикувани от повече различни хора са класирани по-напред.",
"dismissable_banner.explore_statuses": "Има публикации из федивселената, които днес набират популярност. По-новите публикации с повече подсилвания и любими са класирани по-високо.",
"dismissable_banner.explore_tags": "Тези хаштагове днес набират популярност. Хаштагове, употребявани от повече различни хора са класирани по-напред.",
"dismissable_banner.public_timeline": "Ето най-новите обществени публикации от хора във федивселената, която хората в {domain} следват.",
"domain_block_modal.block": "Блокиране на сървър",
"domain_block_modal.block_account_instead": "Вместо това блокиране на @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Хората от този сървър могат да взаимодействат с ваши стари публикации.",
"domain_block_modal.they_cant_follow": "Никого от този сървър не може да ви последва.",
"domain_block_modal.they_wont_know": "Няма да узнаят, че са били блокирани.",
"domain_block_modal.title": "Блокирате ли домейн?",
"domain_block_modal.you_will_lose_num_followers": "Ще загубите {followersCount, plural, one {{followersCountDisplay} последовател} other {{followersCountDisplay} последователи}} и {followingCount, plural, one {{followingCountDisplay} лице, което следвате} other {{followingCountDisplay} души, които следвате}}.",
"domain_block_modal.you_will_lose_relationships": "Ще загубите всичките си последователи и хората, които следвате от този сървър.",
"domain_block_modal.you_wont_see_posts": "Няма да виждате публикации или известия от потребителите на този сървър.",
"domain_pill.activitypub_lets_connect": "Позволява ви да се свързвате и взаимодействате с хора не само в Mastodon, но и през различни социални приложения.",
"domain_pill.activitypub_like_language": "ActivityPub е като език на Mastodon, говорещ с други социални мрежи.",
@ -281,16 +284,16 @@
"empty_column.account_unavailable": "Профилът не е наличен",
"empty_column.blocks": "Още не сте блокирали никакви потребители.",
"empty_column.bookmarked_statuses": "Още не сте отметнали публикации. Отметвайки някоя, то тя ще се покаже тук.",
"empty_column.community": "Локалният инфопоток е празен. Публикувайте нещо, за да започнете!",
"empty_column.community": "Локалната хронология е празна. Напишете нещо публично, за да завъртите процеса!",
"empty_column.direct": "Още нямате никакви частни споменавания. Тук ще се показват, изпращайки или получавайки едно.",
"empty_column.domain_blocks": "Още няма блокирани домейни.",
"empty_column.explore_statuses": "Няма тенденции в момента. Проверете пак по-късно!",
"empty_column.explore_statuses": "Няма нищо популярно в момента. Проверете пак по-късно!",
"empty_column.favourited_statuses": "Още нямате никакви любими публикации. Правейки любима, то тя ще се покаже тук.",
"empty_column.favourites": "Още никого не е слагал публикацията в любими. Когато някой го направи, този човек ще се покаже тук.",
"empty_column.follow_requests": "Още нямате заявки за последване. Получавайки такава, то тя ще се покаже тук.",
"empty_column.followed_tags": "Още не сте последвали никакви хаштагове. Последваните хаштагове ще се покажат тук.",
"empty_column.hashtag": "Още няма нищо в този хаштаг.",
"empty_column.home": "Вашата начална часова ос е празна! Последвайте повече хора, за да я запълните. {suggestions}",
"empty_column.home": "Вашата начална хронология е празна! Последвайте повече хора, за да се запълни.",
"empty_column.list": "Все още списъкът е празен. Членуващите на списъка, публикуващи нови публикации, ще се появят тук.",
"empty_column.mutes": "Още не сте заглушавали потребители.",
"empty_column.notification_requests": "Всичко е чисто! Тук няма нищо. Получавайки нови известия, те ще се появят тук според настройките ви.",
@ -324,6 +327,7 @@
"filter_modal.select_filter.subtitle": "Изберете съществуваща категория или създайте нова",
"filter_modal.select_filter.title": "Филтриране на публ.",
"filter_modal.title.status": "Филтриране на публ.",
"filter_warning.matches_filter": "Съвпадащ филтър на “<span>{title}</span>”",
"filtered_notifications_banner.pending_requests": "От {count, plural, =0 {никого, когото може да познавате} one {едно лице, което може да познавате} other {# души, които може да познавате}}",
"filtered_notifications_banner.title": "Филтрирани известия",
"firehose.all": "Всичко",
@ -358,6 +362,7 @@
"footer.status": "Състояние",
"generic.saved": "Запазено",
"getting_started.heading": "Първи стъпки",
"hashtag.admin_moderation": "Отваряне на модериращия интерфейс за #{name}",
"hashtag.column_header.tag_mode.all": "и {additional}",
"hashtag.column_header.tag_mode.any": "или {additional}",
"hashtag.column_header.tag_mode.none": "без {additional}",
@ -391,11 +396,13 @@
"ignore_notifications_modal.disclaimer": "Mastodon не може да осведоми потребители, че сте пренебрегнали известията им. Пренебрегването на известията няма да спре самите съобщения да не бъдат изпращани.",
"ignore_notifications_modal.filter_to_act_users": "Вие все още ще може да приемате, отхвърляте или докладвате потребители",
"ignore_notifications_modal.filter_to_avoid_confusion": "Прецеждането помага за избягване на възможно объркване",
"ignore_notifications_modal.filter_to_review_separately": "Може да разгледате отделно филтрираните известия",
"ignore_notifications_modal.ignore": "Пренебрегване на известията",
"ignore_notifications_modal.limited_accounts_title": "Пренебрегвате ли известията от модерирани акаунти?",
"ignore_notifications_modal.new_accounts_title": "Пренебрегвате ли известията от нови акаунти?",
"ignore_notifications_modal.not_followers_title": "Пренебрегвате ли известията от хора, които не са ви последвали?",
"ignore_notifications_modal.not_following_title": "Пренебрегвате ли известията от хора, които не сте последвали?",
"ignore_notifications_modal.private_mentions_title": "Пренебрегвате ли известия от непоискани лични споменавания?",
"interaction_modal.description.favourite": "Имайки акаунт в Mastodon, може да сложите тази публикации в любими, за да позволите на автора да узнае, че я цените и да я запазите за по-късно.",
"interaction_modal.description.follow": "С акаунт в Mastodon може да последвате {name}, за да получавате публикациите от този акаунт в началния си инфоканал.",
"interaction_modal.description.reblog": "С акаунт в Mastodon може да подсилите тази публикация, за да я споделите с последователите си.",
@ -403,7 +410,7 @@
"interaction_modal.description.vote": "Имайки акаунт в Mastodon, можете да гласувате в тази анкета.",
"interaction_modal.login.action": "Към началото",
"interaction_modal.login.prompt": "Домейнът на сървъра ви, примерно, mastodon.social",
"interaction_modal.no_account_yet": "Още не е в Мастодон?",
"interaction_modal.no_account_yet": "Още ли не сте в Mastodon?",
"interaction_modal.on_another_server": "На различен сървър",
"interaction_modal.on_this_server": "На този сървър",
"interaction_modal.sign_in": "Не сте влезли в този сървър. Къде се хоства акаунтът ви?",
@ -427,12 +434,12 @@
"keyboard_shortcuts.enter": "Отваряне на публикация",
"keyboard_shortcuts.favourite": "Любима публикация",
"keyboard_shortcuts.favourites": "Отваряне на списъка с любими",
"keyboard_shortcuts.federated": "Отваряне на федерирания инфопоток",
"keyboard_shortcuts.federated": "Отваряне на федералната хронология",
"keyboard_shortcuts.heading": "Клавишни съчетания",
"keyboard_shortcuts.home": "Отваряне на личния инфопоток",
"keyboard_shortcuts.home": "Отваряне на началната хронология",
"keyboard_shortcuts.hotkey": "Бърз клавиш",
"keyboard_shortcuts.legend": "Показване на тази легенда",
"keyboard_shortcuts.local": "Отваряне на локалния инфопоток",
"keyboard_shortcuts.local": "Отваряне на локалната хронология",
"keyboard_shortcuts.mention": "Споменаване на автора",
"keyboard_shortcuts.muted": "Отваряне на списъка със заглушени потребители",
"keyboard_shortcuts.my_profile": "Отваряне на профила ви",
@ -460,13 +467,22 @@
"link_preview.author": "От {name}",
"link_preview.more_from_author": "Още от {name}",
"link_preview.shares": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
"lists.add_member": "Добавяне",
"lists.add_to_list": "Добавяне в списък",
"lists.add_to_lists": "Добавяне на {name} в списъци",
"lists.create": "Създаване",
"lists.create_a_list_to_organize": "Сътворете нов списък, за да организирате инфоканала си на Начало",
"lists.create_list": "Създаване на списък",
"lists.delete": "Изтриване на списъка",
"lists.done": "Готово",
"lists.edit": "Промяна на списъка",
"lists.exclusive": "Скриване на членуващи в Начало",
"lists.exclusive_hint": "Ако някой е в този списък, то скрийте го в инфоканала си на Начало, за да избегнете виждането на публикациите му два пъти.",
"lists.find_users_to_add": "Намерете потребители за добавяне",
"lists.list_members": "Списък членуващи",
"lists.list_members_count": "{count, plural, one {# членуващ} other {# членуващи}}",
"lists.list_name": "Име на списък",
"lists.new_list_name": "Ново име на списък",
"lists.no_lists_yet": "Още няма списъци.",
"lists.no_members_yet": "Още няма членуващи.",
"lists.no_results_found": "Няма намерени резултати.",
@ -476,6 +492,7 @@
"lists.replies_policy.none": "Никого",
"lists.save": "Запазване",
"lists.search_placeholder": "Търсене сред, които сте последвали",
"lists.show_replies_to": "Включва отговори от членуващи в списъка до",
"load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
"loading_indicator.label": "Зареждане…",
"media_gallery.hide": "Скриване",
@ -494,7 +511,7 @@
"navigation_bar.advanced_interface": "Отваряне в разширен уебинтерфейс",
"navigation_bar.blocks": "Блокирани потребители",
"navigation_bar.bookmarks": "Отметки",
"navigation_bar.community_timeline": "Локален инфопоток",
"navigation_bar.community_timeline": "Локална хронология",
"navigation_bar.compose": "Съставяне на нова публикация",
"navigation_bar.direct": "Частни споменавания",
"navigation_bar.discover": "Откриване",
@ -524,6 +541,8 @@
"notification.admin.report_statuses_other": "{name} докладва {target}",
"notification.admin.sign_up": "{name} се регистрира",
"notification.admin.sign_up.name_and_others": "{name} и {count, plural, one {# друг} other {# други}} се регистрираха",
"notification.annual_report.message": "#Wrapstodon за {year} година ви очаква! Свалете булото на изтъкнатото и паметните моменти за годината си в Mastodon!",
"notification.annual_report.view": "Преглед на #Wrapstodon",
"notification.favourite": "{name} направи любима публикацията ви",
"notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> направиха любима ваша публикация",
"notification.follow": "{name} ви последва",
@ -557,10 +576,15 @@
"notification.status": "{name} току-що публикува",
"notification.update": "{name} промени публикация",
"notification_requests.accept": "Приемам",
"notification_requests.accept_multiple": "{count, plural, one {Приемане на # заявка…} other {Приемане на # заявки…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Приемане на заявката} other {Приемане на заявките}}",
"notification_requests.confirm_accept_multiple.message": "На път сте да приемете {count, plural, one {едно известие за заявка} other {# известия за заявки}}. Наистина ли искате да продължите?",
"notification_requests.confirm_accept_multiple.title": "Приемате ли заявките за известие?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Отхвърляне на заявката} other {Отхвърляне на заявките}}",
"notification_requests.confirm_dismiss_multiple.message": "На път сте да отхвърлите {count, plural, one {една заявка за известие} other {# заявки за известие}}. Няма да имате лесен достъп до {count, plural, one {това лице} other {тях}} отново. Наистина ли искате да продължите?",
"notification_requests.confirm_dismiss_multiple.title": "Отхвърляте ли заявките за известие?",
"notification_requests.dismiss": "Отхвърлям",
"notification_requests.dismiss_multiple": "{count, plural, one {Отхвърляне на # заявка…} other {Отхвърляне на # заявки…}}",
"notification_requests.edit_selection": "Редактиране",
"notification_requests.exit_selection": "Готово",
"notification_requests.explainer_for_limited_account": "Известията от този акаунт са прецедени, защото акаунтът е ограничен от модератор.",
@ -581,6 +605,7 @@
"notifications.column_settings.filter_bar.category": "Лента за бърз филтър",
"notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.follow_request": "Нови заявки за последване:",
"notifications.column_settings.group": "Групиране",
"notifications.column_settings.mention": "Споменавания:",
"notifications.column_settings.poll": "Резултати от анкета:",
"notifications.column_settings.push": "Изскачащи известия",
@ -606,6 +631,7 @@
"notifications.permission_required": "Известията на работния плот ги няма, щото няма дадено нужното позволение.",
"notifications.policy.accept": "Приемам",
"notifications.policy.accept_hint": "Показване в известия",
"notifications.policy.drop": "Пренебрегване",
"notifications.policy.drop_hint": "Изпращане в празнотата, за да не се видим никога пак",
"notifications.policy.filter": "Филтър",
"notifications.policy.filter_limited_accounts_hint": "Ограничено от модераторите на сървъра",
@ -622,44 +648,21 @@
"notifications_permission_banner.enable": "Включване на известията на работния плот",
"notifications_permission_banner.how_to_control": "За да получавате известия, когато Mastodon не е отворен, включете известията на работния плот. Може да управлявате точно кои видове взаимодействия пораждат известия на работния плот чрез бутона {icon} по-горе, след като бъдат включени.",
"notifications_permission_banner.title": "Никога не пропускайте нищо",
"onboarding.action.back": "Върнете ме обратно",
"onboarding.actions.back": "Върнете ме обратно",
"onboarding.actions.go_to_explore": "Виж тенденции",
"onboarding.actions.go_to_home": "Към началния ми инфоканал",
"onboarding.compose.template": "Здравейте, #Mastodon!",
"onboarding.follows.empty": "За съжаление, в момента не могат да бъдат показани резултати. Може да опитате да търсите или да разгледате, за да намерите кого да последвате, или опитайте отново по-късно.",
"onboarding.follows.lead": "Може да бъдете куратор на началния си инфоканал. Последвайки повече хора, по-деен и по-интересен ще става. Тези профили може да са добра начална точка, от която винаги по-късно да спрете да следвате!",
"onboarding.follows.title": "Популярно в Mastodon",
"onboarding.follows.back": "Назад",
"onboarding.follows.done": "Готово",
"onboarding.follows.empty": "За съжаление, в момента не могат да се показват резултати. Може да опитате посредством търсене или сърфиране да разгледате страницата, за да намерите хора за последване, или опитайте пак по-късно.",
"onboarding.follows.search": "Търсене",
"onboarding.follows.title": "Последвайте хора, за да започнете",
"onboarding.profile.discoverable": "Правене на моя профил откриваем",
"onboarding.profile.discoverable_hint": "Включвайки откриваемостта в Mastodon, вашите публикации може да се появят при резултатите от търсене и изгряващи неща, и вашия профил може да бъде предложен на хора с подобни интереси като вашите.",
"onboarding.profile.display_name": "Името на показ",
"onboarding.profile.display_name_hint": "Вашето пълно име или псевдоним…",
"onboarding.profile.lead": "Винаги може да завършите това по-късно в настройките, където дори има повече възможности за настройване.",
"onboarding.profile.note": "Биогр.",
"onboarding.profile.note": "Биография",
"onboarding.profile.note_hint": "Може да @споменавате други хора или #хаштагове…",
"onboarding.profile.save_and_continue": "Запазване и продължаване",
"onboarding.profile.title": "Настройване на профила",
"onboarding.profile.upload_avatar": "Качване на снимка на профила",
"onboarding.profile.upload_header": "Качване на заглавка на профила",
"onboarding.share.lead": "Позволете на хората да знаят, че могат да ви намерят в Mastodon!",
"onboarding.share.message": "Аз съм {username} в #Mastodon! Елате да ме последвате при {url}",
"onboarding.share.next_steps": "Възможни следващи стъпки:",
"onboarding.share.title": "Споделяне на профила ви",
"onboarding.start.lead": "Вашият нов акаунт в Mastodon е готов за употреба. Ето как може да се възползвате по най-добрия начин от него:",
"onboarding.start.skip": "Желаете ли да прескочите?",
"onboarding.start.title": "Успяхте!",
"onboarding.steps.follow_people.body": "Може да бъдете куратор на инфоканала си. Хайде да го запълним с интересни хора.",
"onboarding.steps.follow_people.title": "Персонализиране на началния ви инфоканал",
"onboarding.steps.publish_status.body": "Поздравете целия свят.",
"onboarding.steps.publish_status.title": "Направете първата си публикация",
"onboarding.steps.setup_profile.body": "Подсилете взаимодействията си, имайки изчерпателен профил.",
"onboarding.steps.setup_profile.title": "Пригодете профила си",
"onboarding.steps.share_profile.body": "Позволете на приятелите си да знаят как да ви намират в Mastodon!",
"onboarding.steps.share_profile.title": "Споделяне на профила ви",
"onboarding.tips.2fa": "<strong>Знаете ли, че?</strong> Може да защитите акаунта си, настройвайки двуфакторното удостоверяване в настройките на акаунта си. То работи с всяко приложение TOTP по ваш избор, не е необходим номер телефона!",
"onboarding.tips.accounts_from_other_servers": "<strong>Знаете ли, че?</strong> Откак Mastodon е децентрализиран, някои профили, които срещате ще бъдат разположени на сървъри различен от вашия. И още може да взаимодействате с тях безпроблемно! Сървърът им е втората половина от потребителското им име!",
"onboarding.tips.migration": "<strong>Знаете ли, че?</strong> Ако се чувствате, че {domain} не е чудесен избор на сървър в бъдуще, може да се преместите на друг сървър на Mastodon без да загубите последователите си. Дори може да сте съдържатели на свой собствен сървър!",
"onboarding.tips.verification": "<strong>Знаете ли, че?</strong> Може да потвърдите акаунта си, слагайки връзка към профила си в Mastodon на уебсайта си и добавите уебсайта в профила си. Не са необходими документи или такси!",
"password_confirmation.exceeds_maxlength": "Потвърждаването на паролата превишава максимално допустимата дължина за парола",
"password_confirmation.mismatching": "Потвърждаването на паролата не съвпада",
"picture_in_picture.restore": "Връщане обратно",
@ -723,7 +726,7 @@
"report.placeholder": "Допълнителни коментари",
"report.reasons.dislike": "Не ми харесва",
"report.reasons.dislike_description": "Не е нещо, което искате да виждате",
"report.reasons.legal": "Законово е",
"report.reasons.legal": "Незаконно е",
"report.reasons.legal_description": "Смятате, че това нарушава закона на вашата страна или държавата на сървъра",
"report.reasons.other": "Нещо друго е",
"report.reasons.other_description": "Проблемът не попада в нито една от останалите категории",
@ -761,7 +764,7 @@
"search.quick_action.open_url": "Отваряне на URL адреса в Mastodon",
"search.quick_action.status_search": "Съвпадение на публикации {x}",
"search.search_or_paste": "Търсене/поставяне на URL",
"search_popout.full_text_search_disabled_message": "Не е достъпно на {domain}.",
"search_popout.full_text_search_disabled_message": "Не е налично на {domain}.",
"search_popout.full_text_search_logged_out_message": "Достъпно само при влизане в системата.",
"search_popout.language_code": "Код на езика по ISO",
"search_popout.options": "Възможности при търсене",
@ -843,9 +846,9 @@
"status.uncached_media_warning": "Онагледяването не е налично",
"status.unmute_conversation": "Без заглушаването на разговора",
"status.unpin": "Разкачане от профила",
"subscribed_languages.lead": "Публикации само на избрани езици ще се явяват в началото ви и в списъка с часови оси след промяната. Изберете \"нищо\", за да получавате публикации на всички езици.",
"subscribed_languages.lead": "Публикации само на избрани езици ще се явяват в началото ви и в хронологичните списъци след промяната. Изберете \"нищо\", за да получавате публикации на всички езици.",
"subscribed_languages.save": "Запазване на промените",
"subscribed_languages.target": "Смяна на езика за {target}",
"subscribed_languages.target": "Промяна на абонираните езици за {target}",
"tabs_bar.home": "Начало",
"tabs_bar.notifications": "Известия",
"time_remaining.days": "{number, plural, one {остава # ден} other {остават # дни}}",

View file

@ -91,7 +91,6 @@
"bundle_column_error.routing.body": "অনুরোধ করা পৃষ্ঠা খুঁজে পাওয়া যাবে না। আপনি কি নিশ্চিত যে ঠিকানা বারে ইউআরএলটি সঠিক?",
"bundle_column_error.routing.title": "",
"bundle_modal_error.close": "বন্ধ করুন",
"bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।.",
"bundle_modal_error.retry": "আবার চেষ্টা করুন",
"closed_registrations.other_server_instructions": "মাস্টোডন বিকেন্দ্রীভূত হওয়ায়, আপনি অন্য সার্ভারে একটি অ্যাকাউন্ট তৈরি করতে পারেন এবং এখনও এটির সাথে যোগাযোগ করতে পারেন।",
"closed_registrations_modal.description": "{domain} এ একটি অ্যাকাউন্ট তৈরি করা বর্তমানে সম্ভব নয়, তবে দয়া করে মনে রাখবেন যে ম্যাস্টোডন ব্যবহার করার জন্য আপনার বিশেষভাবে {domain} এ কোনো অ্যাকাউন্টের প্রয়োজন নেই৷",
@ -173,8 +172,6 @@
"disabled_account_banner.account_settings": "একাউন্ট সেটিংস",
"disabled_account_banner.text": "আপনার একাউন্ট {disabledAccount} বর্তমানে বন্ধ করা.",
"dismissable_banner.dismiss": "সরাও",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।",
"embed.preview": "সেটা দেখতে এরকম হবে:",
"emoji_button.activity": "কার্যকলাপ",
@ -327,21 +324,6 @@
"notifications.filter.mentions": "উল্লেখিত",
"notifications.filter.polls": "নির্বাচনের ফলাফল",
"notifications.group": "{count} প্রজ্ঞাপন",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"onboarding.tips.accounts_from_other_servers": "<strong>তুমি কি জানতে?</strong> যেহেতু মাস্টোডন বিকেন্দ্রীভূত, কিছু অ্যাকাউন্ট তোমার নিজের ছাড়া অন্য কোনো সার্ভারে থাকতে পারে। অথচ তুমি তাদের সাথে কোনো সমস্যা ছাড়াই কথা বলতে পারছো! তাদের সার্ভার তাদের ব্যবহারকারী নামের দ্বিতীয় অর্ধাংশ!",
"onboarding.tips.migration": "<strong>তুমি কি জানো?</strong> {domain} তোমার পছন্দ না হলে, ভবিষ্যতে তুমি অন্য কোনো সার্ভারে যেতে পারো তোমার অনুসরণকারীদেরকে না হারিয়েই। এমনকি তুমি নিজের সার্ভারও তৈরি করতে পারো!",
"picture_in_picture.restore": "ফিরত রাখো",
"poll.closed": "বন্ধ",
"poll.refresh": "বদলেছে কিনা দেখতে",

View file

@ -99,7 +99,6 @@
"bundle_column_error.routing.body": "N'haller ket kavout ar bajenn goulennet. Sur oc'h eo reizh an URL er varrenn chomlec'hioù?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Serriñ",
"bundle_modal_error.message": "Degouezhet ez eus bet ur fazi en ur gargañ an elfenn-mañ.",
"bundle_modal_error.retry": "Klask en-dro",
"closed_registrations.other_server_instructions": "Peogwir ez eo Mastodon digreizennet e c'heller krouiñ ur gont war ur servijer all ha kenderc'hel da zaremprediñ gant hemañ.",
"closed_registrations_modal.description": "N'eo ket posupl krouiñ ur gont war {domain} evit ar mare, met n'ho peus ket ezhomm ur gont war {domain} dre ret evit ober gant Mastodon.",
@ -129,6 +128,7 @@
"column_header.pin": "Spilhennañ",
"column_header.show_settings": "Diskouez an arventennoù",
"column_header.unpin": "Dispilhennañ",
"column_search.cancel": "Nullañ",
"column_subheading.settings": "Arventennoù",
"community.column_settings.local_only": "Nemet lec'hel",
"community.column_settings.media_only": "Nemet Mediaoù",
@ -189,8 +189,6 @@
"disabled_account_banner.text": "Ho kont {disabledAccount} zo divev evit bremañ.",
"dismissable_banner.community_timeline": "Setu toudoù foran nevesañ an dud a zo herberchiet o c'hontoù gant {domain}.",
"dismissable_banner.dismiss": "Diverkañ",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"domain_pill.server": "Dafariad",
"domain_pill.username": "Anv-implijer",
"embed.instructions": "Enframmit an toud-mañ en ho lec'hienn en ur eilañ ar c'hod amañ-dindan.",
@ -422,33 +420,15 @@
"notifications_permission_banner.enable": "Lezel kemennoù war ar burev",
"notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.",
"notifications_permission_banner.title": "Na vankit netra morse",
"onboarding.action.back": "Distreiñ",
"onboarding.actions.back": "Distreiñ",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Mont d'ho red degemer",
"onboarding.compose.template": "Salud #Mastodon!",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.follows.back": "Distreiñ",
"onboarding.follows.done": "Graet",
"onboarding.follows.search": "Klask",
"onboarding.profile.display_name": "Anv diskouezet",
"onboarding.profile.display_name_hint": "Hoc'h anv klok pe hoc'h anv fentus…",
"onboarding.profile.note": "Berr-ha-berr",
"onboarding.profile.note_hint": "Gallout a rit @menegiñ tud all pe #hashtagoù…",
"onboarding.profile.save_and_continue": "Enrollañ ha kenderc'hel",
"onboarding.profile.upload_avatar": "Enporzhiañ ur skeudenn profil",
"onboarding.share.lead": "Roit da c'houzout d'an dud e c'hallont ho kavout war vMastondon!",
"onboarding.share.message": "Me a zo {username} war #Mastodon! Heuilhit ac'hanon war {url}",
"onboarding.share.title": "Skignañ ho profil",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "Deuet oc'h a-benn!",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.publish_status.title": "Grit hoc'h embannadur kentañ",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Rannit ho kont Mastodon",
"password_confirmation.mismatching": "Disheñvel eo an daou c'her-termen-se",
"picture_in_picture.restore": "Adlakaat",
"poll.closed": "Serret",

View file

@ -11,8 +11,6 @@
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"embed.instructions": "Embed this status on your website by copying the code below.",
"empty_column.account_timeline": "No posts found",
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
@ -51,19 +49,6 @@
"navigation_bar.domain_blocks": "Hidden domains",
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
"notification.reblog": "{name} boosted your status",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"privacy.change": "Adjust status privacy",
"report.placeholder": "Type or paste additional comments",
"report.submit": "Submit report",

View file

@ -87,7 +87,11 @@
"alert.unexpected.title": "Vaja!",
"alt_text_badge.title": "Text alternatiu",
"announcement.announcement": "Anunci",
"annual_report.summary.archetype.booster": "Sempre a la moda",
"annual_report.summary.archetype.lurker": "Tot ho llegeix",
"annual_report.summary.archetype.oracle": "L'Oracle",
"annual_report.summary.archetype.pollster": "Tot són enquestes",
"annual_report.summary.archetype.replier": "Tot ho respon",
"annual_report.summary.followers.followers": "seguidors",
"annual_report.summary.followers.total": "{count} en total",
"annual_report.summary.here_it_is": "El repàs del vostre {year}:",
@ -99,6 +103,8 @@
"annual_report.summary.most_used_hashtag.most_used_hashtag": "l'etiqueta més utilitzada",
"annual_report.summary.most_used_hashtag.none": "Cap",
"annual_report.summary.new_posts.new_posts": "publicacions noves",
"annual_report.summary.percentile.text": "<topLabel>Que us posa en el</topLabel><percentage></percentage><bottomLabel>més alt dels usuaris de Mastodon.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "No li ho direm al Bernie.",
"annual_report.summary.thanks": "Gràcies per formar part de Mastodon!",
"attachments_list.unprocessed": "(sense processar)",
"audio.hide": "Amaga l'àudio",
@ -123,7 +129,7 @@
"bundle_column_error.routing.body": "No es pot trobar la pàgina sol·licitada. Segur que l'enllaç que has introduït és correcte?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Tanca",
"bundle_modal_error.message": "S'ha produït un error en carregar aquest component.",
"bundle_modal_error.message": "S'ha produït un error en carregar aquesta pantalla.",
"bundle_modal_error.retry": "Torna-ho a provar",
"closed_registrations.other_server_instructions": "Com que Mastodon és descentralitzat, pots crear un compte en un altre servidor i continuar interactuant amb aquest.",
"closed_registrations_modal.description": "No es pot crear un compte a {domain} ara mateix, però tingues en compte que no necessites específicament un compte a {domain} per a usar Mastodon.",
@ -134,13 +140,16 @@
"column.blocks": "Usuaris blocats",
"column.bookmarks": "Marcadors",
"column.community": "Línia de temps local",
"column.create_list": "Crea una llista",
"column.direct": "Mencions privades",
"column.directory": "Navega pels perfils",
"column.domain_blocks": "Dominis blocats",
"column.edit_list": "Edita la llista",
"column.favourites": "Favorits",
"column.firehose": "Tuts en directe",
"column.follow_requests": "Peticions de seguir-te",
"column.home": "Inici",
"column.list_members": "Gestiona els membres de la llista",
"column.lists": "Llistes",
"column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions",
@ -153,6 +162,7 @@
"column_header.pin": "Fixa",
"column_header.show_settings": "Mostra la configuració",
"column_header.unpin": "Desfixa",
"column_search.cancel": "Cancel·la",
"column_subheading.settings": "Configuració",
"community.column_settings.local_only": "Només local",
"community.column_settings.media_only": "Només contingut",
@ -226,10 +236,10 @@
"disabled_account_banner.text": "El teu compte {disabledAccount} està desactivat.",
"dismissable_banner.community_timeline": "Aquests són els tuts públics més recents d'usuaris amb els seus comptes a {domain}.",
"dismissable_banner.dismiss": "Ometre",
"dismissable_banner.explore_links": "Gent d'aquest i d'altres servidors de la xarxa descentralitzada estan comentant ara mateix aquestes notícies.",
"dismissable_banner.explore_statuses": "Aquests son els tuts de la xarxa descentralitzada que guanyen atenció ara mateix. Els tuts més nous amb més impulsos i favorits tenen millor rànquing.",
"dismissable_banner.explore_tags": "Aquestes etiquetes estan guanyant ara mateix l'atenció dels usuaris d'aquest i altres servidors de la xarxa descentralitzada.",
"dismissable_banner.public_timeline": "Aquests son els tuts públics més recents de les persones a la web social que les persones de {domain} segueixen.",
"dismissable_banner.explore_links": "Aquestes històries noves són les més compartides avui al Fedivers. Les històries noves publicades per més persones diferents es classifiquen amunt.",
"dismissable_banner.explore_statuses": "Aquestes publicacions d'arreu del Fedivers estan atraient l'atenció avui. Les publicacions noves amb més impulsos i favorits es classifiquen amunt.",
"dismissable_banner.explore_tags": "Aquestes etiquetes estan atraient l'atenció avui. Les etiquetes que fan servir més persones diferents es classifiquen amunt.",
"dismissable_banner.public_timeline": "Aquestes són les publicacions més recents al Fedivers que segueixen gent a {domain}.",
"domain_block_modal.block": "Bloca el servidor",
"domain_block_modal.block_account_instead": "En lloc d'això, bloca @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Els usuaris d'aquest servidor poden interactuar amb les vostres publicacions antigues.",
@ -353,6 +363,7 @@
"footer.status": "Estat",
"generic.saved": "Desat",
"getting_started.heading": "Primeres passes",
"hashtag.admin_moderation": "Obre la interfície de moderació per a #{name}",
"hashtag.column_header.tag_mode.all": "i {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sense {additional}",
@ -458,11 +469,32 @@
"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}}",
"lists.add_member": "Afegeix",
"lists.add_to_list": "Afegeix a la llista",
"lists.add_to_lists": "Afegeix {name} a les llistes",
"lists.create": "Crea",
"lists.create_a_list_to_organize": "Creeu una nova llista per a organitzar la pantalla d'inici",
"lists.create_list": "Crea una llista",
"lists.delete": "Elimina la llista",
"lists.done": "Fet",
"lists.edit": "Edita la llista",
"lists.exclusive": "Amaga membres a Inici",
"lists.exclusive_hint": "Si algú és a la llista, amagueu-los de la pantalla d'inici, per a no veure'n les publicacions duplicades.",
"lists.find_users_to_add": "Troba usuaris per a afegir",
"lists.list_members": "Membres de la llista",
"lists.list_members_count": "{count, plural, one {# membre} other {# membres}}",
"lists.list_name": "Nom de la llista",
"lists.new_list_name": "Nom de la nova llista",
"lists.no_lists_yet": "Encara no hi ha cap llista.",
"lists.no_members_yet": "Encara no hi ha membres.",
"lists.no_results_found": "No s'han trobat resultats.",
"lists.remove_member": "Elimina",
"lists.replies_policy.followed": "Qualsevol usuari que segueixis",
"lists.replies_policy.list": "Membres de la llista",
"lists.replies_policy.none": "Ningú",
"lists.save": "Desa",
"lists.search_placeholder": "Cerca persones que seguiu",
"lists.show_replies_to": "Inclou respostes de membres de la llista a",
"load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
"loading_indicator.label": "Es carrega…",
"media_gallery.hide": "Amaga",
@ -511,6 +543,8 @@
"notification.admin.report_statuses_other": "{name} ha reportat {target}",
"notification.admin.sign_up": "{name} s'ha registrat",
"notification.admin.sign_up.name_and_others": "{name} i {count, plural, one {# altre} other {# altres}} s'han registrat",
"notification.annual_report.message": "El vostre {year} #Wrapstodon t'espera. Desveleu els vostres moments més memorables a Mastodon!",
"notification.annual_report.view": "Visualitzeu #Wrapstodon",
"notification.favourite": "{name} ha afavorit el teu tut",
"notification.favourite.name_and_others_with_link": "{name} i <a>{count, plural, one {# altre} other {# altres}}</a> han afavorit la vostra publicació",
"notification.follow": "{name} et segueix",
@ -617,44 +651,21 @@
"notifications_permission_banner.enable": "Activa les notificacions descriptori",
"notifications_permission_banner.how_to_control": "Per a rebre notificacions quan Mastodon no és obert cal activar les notificacions descriptori. Pots controlar amb precisió quins tipus dinteraccions generen notificacions descriptori després dactivar el botó {icon} de dalt.",
"notifications_permission_banner.title": "No et perdis mai res",
"onboarding.action.back": "Porta'm enrere",
"onboarding.actions.back": "Porta'm enrere",
"onboarding.actions.go_to_explore": "Mira què és tendència",
"onboarding.actions.go_to_home": "Ves a la teva línia de temps",
"onboarding.compose.template": "Hola Mastodon!",
"onboarding.follows.back": "Enrere",
"onboarding.follows.done": "Fet",
"onboarding.follows.empty": "Malauradament, cap resultat pot ser mostrat ara mateix. Pots provar de fer servir la cerca o visitar la pàgina Explora per a trobar gent a qui seguir o provar-ho de nou més tard.",
"onboarding.follows.lead": "La teva línia de temps inici només està a les teves mans. Com més gent segueixis, més activa i interessant serà. Aquests perfils poden ser un bon punt d'inici—sempre pots acabar deixant de seguir-los!:",
"onboarding.follows.title": "Personalitza la pantalla d'inci",
"onboarding.follows.search": "Cerca",
"onboarding.follows.title": "Seguiu gent per a començar",
"onboarding.profile.discoverable": "Fes el meu perfil descobrible",
"onboarding.profile.discoverable_hint": "En acceptar d'ésser descobert a Mastodon els teus missatges poden aparèixer dins les tendències i els resultats de cerques, i el teu perfil es pot suggerir a qui tingui interessos semblants als teus.",
"onboarding.profile.display_name": "Nom que es mostrarà",
"onboarding.profile.display_name_hint": "El teu nom complet o el teu malnom…",
"onboarding.profile.lead": "Sempre ho pots completar més endavant a la configuració, on hi ha encara més opcions disponibles.",
"onboarding.profile.note": "Biografia",
"onboarding.profile.note_hint": "Pots @mencionar altra gent o #etiquetes…",
"onboarding.profile.save_and_continue": "Desa i continua",
"onboarding.profile.title": "Configuració del perfil",
"onboarding.profile.upload_avatar": "Importa una foto de perfil",
"onboarding.profile.upload_header": "Importa una capçalera de perfil",
"onboarding.share.lead": "Permet que la gent sàpiga com trobar-te a Mastodon!",
"onboarding.share.message": "Sóc {username} a #Mastodon! Vine i segueix-me a {url}",
"onboarding.share.next_steps": "Possibles passes següents:",
"onboarding.share.title": "Comparteix el teu perfil",
"onboarding.start.lead": "El teu nou compte ja està preparat a Mastodon, la xarxa social on tu—no un algorisme—té tot el control. Aquí tens com en pots treure tot el suc:",
"onboarding.start.skip": "Vols saltar-te tota la resta?",
"onboarding.start.title": "Llestos!",
"onboarding.steps.follow_people.body": "Mastodon va de seguir a gent interessant.",
"onboarding.steps.follow_people.title": "Personalitza la pantalla d'inci",
"onboarding.steps.publish_status.body": "Saluda al món amb text, fotos, vídeos o enquestes {emoji}",
"onboarding.steps.publish_status.title": "Fes el teu primer tut",
"onboarding.steps.setup_profile.body": "És més fàcil que altres interactuïn amb tu si tens un perfil complet.",
"onboarding.steps.setup_profile.title": "Personalitza el perfil",
"onboarding.steps.share_profile.body": "Fer saber als teus amics com trobar-te a Mastodon",
"onboarding.steps.share_profile.title": "Comparteix el teu perfil",
"onboarding.tips.2fa": "<strong>Ho sabies?</strong> Pots securitzar el teu compte activant l'autenticació de doble factor en la configuració del teu perfil. Funciona amb qualsevol aplicació TOTP de la teva elecció, no cal número de telèfon!",
"onboarding.tips.accounts_from_other_servers": "<strong>Ho sabies?</strong> Com Mastodon és descentralitzat, et pots trobar amb perfils que són a servidors diferents del teu. I, tanmateix, també hi pots interactuar sense cap problema! El servidor és la segona part del seu nom d'usuari!",
"onboarding.tips.migration": "<strong>Ho sabies?</strong> Si et sembla que {domain} no és una bona elecció de servidor per a tu en el futur, pots moure't a un altre servidor Mastodon sense perdre els teus seguidors. Fins i tot pots tenir el teu propi servidor!",
"onboarding.tips.verification": "<strong>Ho sabies?</strong> Pots verificar el teu compte posant un enllaç al teu perfil a Mastodon en la teva pàgina web i afegint la adreça d'aquesta web en el teu perfil. Sense cap mena de tarifa o document!",
"password_confirmation.exceeds_maxlength": "La confirmació de la contrasenya excedeix la longitud màxima",
"password_confirmation.mismatching": "La confirmació de contrasenya no és coincident",
"picture_in_picture.restore": "Retornal",
@ -683,7 +694,7 @@
"recommended": "Recomanat",
"refresh": "Actualitza",
"regeneration_indicator.label": "Es carrega…",
"regeneration_indicator.sublabel": "Es prepara la teva línia de temps d'Inici!",
"regeneration_indicator.sublabel": "Es prepara la vostra pantalla d'Inici!",
"relative_time.days": "{number}d",
"relative_time.full.days": "fa {number, plural, one {# dia} other {# dies}}",
"relative_time.full.hours": "fa {number, plural, one {# hora} other {# hores}}",

View file

@ -95,7 +95,6 @@
"bundle_column_error.routing.body": "پەیجی داواکراو ناتوانرێت بدۆزرێتەوە. ئایا دڵنیای کە URL ی ناو ناونیشانەکان ڕاستە?",
"bundle_column_error.routing.title": "٤٠٤",
"bundle_modal_error.close": "داخستن",
"bundle_modal_error.message": "هەڵەیەک ڕوویدا لەکاتی بارکردنی ئەم پێکهاتەیە.",
"bundle_modal_error.retry": "دووبارە تاقی بکەوە",
"closed_registrations.other_server_instructions": "بەو پێیەی ماستۆدۆن لامەرکەزییە، دەتوانیت ئەکاونتێک لەسەر سێرڤەرێکی تر دروست بکەیت و هێشتا کارلێک لەگەڵ ئەم سێرڤەرەدا بکەیت.",
"closed_registrations_modal.description": "دروستکردنی ئەکاونت لەسەر {domain} لە ئێستادا ناتوانرێت، بەڵام تکایە ئەوەت لەبەرچاو بێت کە پێویستت بە ئەکاونتێک نییە بە تایبەتی لەسەر {domain} بۆ بەکارهێنانی ماستۆدۆن.",
@ -187,9 +186,6 @@
"disabled_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە.",
"dismissable_banner.community_timeline": "ئەمانە دوایین پۆستی گشتی ئەو کەسانەن کە ئەکاونتەکانیان لەلایەن {domain}ەوە هۆست کراوە.",
"dismissable_banner.dismiss": "بەلاوە نان",
"dismissable_banner.explore_links": "ئەم هەواڵانە لە ئێستادا لەلایەن کەسانێکەوە لەسەر ئەم سێرڤەرە و سێرڤەرەکانی تری تۆڕی لامەرکەزی باس دەکرێن.",
"dismissable_banner.explore_statuses": "ئەمانە پۆستەکانن لە سەرانسەری وێبی کۆمەڵایەتی کە ئەمڕۆ کێشکردنیان بەدەستهێناوە. پۆستە نوێیەکان کە بووست و فەڤریتی زیاتریان هەیە ڕیزبەندی بەرزتریان هەیە.",
"dismissable_banner.explore_tags": "ئەم هاشتاگانە لە ئێستادا لە نێو خەڵکی سەر ئەم سێرڤەرە و سێرڤەرەکانی تری تۆڕی لامەرکەزیدا جێگەی خۆیان دەگرن.",
"embed.instructions": "ئەم توتە بنچین بکە لەسەر وێب سایتەکەت بە کۆپیکردنی کۆدەکەی خوارەوە.",
"embed.preview": "ئەمە ئەو شتەیە کە لە شێوەی خۆی دەچێت:",
"emoji_button.activity": "چالاکی",
@ -407,19 +403,6 @@
"notifications_permission_banner.enable": "چالاککردنی ئاگانامەکانی دێسکتۆپ",
"notifications_permission_banner.how_to_control": "بۆ وەرگرتنی ئاگانامەکان کاتێک ماستۆدۆن نەکراوەیە، ئاگانامەکانی دێسکتۆپ چالاک بکە. دەتوانیت بە وردی کۆنترۆڵی جۆری کارلێکەکان بکەیت کە ئاگانامەکانی دێسکتۆپ دروست دەکەن لە ڕێگەی دوگمەی {icon} لەسەرەوە کاتێک چالاک دەکرێن.",
"notifications_permission_banner.title": "هەرگیز شتێک لە دەست مەدە",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"picture_in_picture.restore": "بیگەڕێنەوە",
"poll.closed": "دابخە",
"poll.refresh": "نوێکردنەوە",

View file

@ -43,7 +43,6 @@
"boost_modal.combo": "Pudete appughjà nant'à {combo} per saltà quessa a prussima volta",
"bundle_column_error.retry": "Pruvà torna",
"bundle_modal_error.close": "Chjudà",
"bundle_modal_error.message": "C'hè statu un prublemu caricandu st'elementu.",
"bundle_modal_error.retry": "Pruvà torna",
"column.blocks": "Utilizatori bluccati",
"column.bookmarks": "Segnalibri",
@ -103,8 +102,6 @@
"directory.local": "Solu da {domain}",
"directory.new_arrivals": "Ultimi arrivi",
"directory.recently_active": "Attività ricente",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"embed.instructions": "Integrà stu statutu à u vostru situ cù u codice quì sottu.",
"embed.preview": "Hà da parè à quessa:",
"emoji_button.activity": "Attività",
@ -253,19 +250,6 @@
"notifications_permission_banner.enable": "Attivà e nutificazione nant'à l'urdinatore",
"notifications_permission_banner.how_to_control": "Per riceve nutificazione quandu Mastodon ùn hè micca aperta, attivate e nutificazione nant'à l'urdinatore. Pudete decide quali tippi d'interazione anu da mandà ste nutificazione cù u buttone {icon} quì sopra quandu saranu attivate.",
"notifications_permission_banner.title": "Ùn mancate mai nunda",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"picture_in_picture.restore": "Rimette in piazza",
"poll.closed": "Chjosu",
"poll.refresh": "Attualizà",

View file

@ -109,7 +109,6 @@
"bundle_column_error.routing.body": "Požadovaná stránka nebyla nalezena. Opravdu je URL v adresním řádku správně?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Zavřít",
"bundle_modal_error.message": "Při načítání tohoto komponentu se něco pokazilo.",
"bundle_modal_error.retry": "Zkusit znovu",
"closed_registrations.other_server_instructions": "Protože Mastodon je decentralizovaný, můžete si vytvořit účet na jiném serveru a přesto komunikovat s tímto serverem.",
"closed_registrations_modal.description": "V současné době není možné vytvořit účet na {domain}, ale mějte prosím na paměti, že k používání Mastodonu nepotřebujete účet konkrétně na {domain}.",
@ -209,10 +208,6 @@
"disabled_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován.",
"dismissable_banner.community_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí, jejichž účty hostuje {domain}.",
"dismissable_banner.dismiss": "Zavřít",
"dismissable_banner.explore_links": "O těchto zprávách hovoří lidé na tomto a dalších serverech decentralizované sítě právě teď.",
"dismissable_banner.explore_statuses": "Toto jsou příspěvky ze sociálních sítí, které dnes získávají na popularitě. Novější příspěvky s větším počtem boostů a oblíbení jsou hodnoceny výše.",
"dismissable_banner.explore_tags": "Tyto hashtagy právě teď získávají na popularitě mezi lidmi na tomto a dalších serverech decentralizované sítě.",
"dismissable_banner.public_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí na sociální síti, které sledují lidé na {domain}.",
"domain_block_modal.block": "Blokovat server",
"domain_block_modal.block_account_instead": "Raději blokovat @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Lidé z tohoto serveru mohou interagovat s vašimi starými příspěvky.",
@ -534,44 +529,17 @@
"notifications_permission_banner.enable": "Povolit oznámení na ploše",
"notifications_permission_banner.how_to_control": "Chcete-li dostávat oznámení, i když nemáte Mastodon otevřený, povolte oznámení na ploše. Můžete si zvolit, o kterých druzích interakcí chcete být oznámením na ploše informování pod tlačítkem {icon} výše.",
"notifications_permission_banner.title": "Nenechte si nic uniknout",
"onboarding.action.back": "Vrátit se zpět",
"onboarding.actions.back": "Vrátit se zpět",
"onboarding.actions.go_to_explore": "Podívejte se, co je populární",
"onboarding.actions.go_to_home": "Přejít na svůj domovský feed",
"onboarding.compose.template": "Ahoj #Mastodon!",
"onboarding.follows.empty": "Bohužel, žádné výsledky nelze momentálně zobrazit. Můžete zkusit vyhledat nebo procházet stránku s průzkumem a najít lidi, kteří budou sledovat, nebo to zkuste znovu později.",
"onboarding.follows.lead": "Domovský kanál je hlavní metodou zažívání Mastodonu. Čím více lidí sledujete, tím aktivnější a zajímavější bude. Pro začnutí, zde máte několik návrhů:",
"onboarding.follows.title": "Přispůsobit vlastní domovský kanál",
"onboarding.profile.discoverable": "Udělat svůj profil vyhledatelným",
"onboarding.profile.discoverable_hint": "Když se rozhodnete být vyhledatelný na Mastodonu, vaše příspěvky se mohou objevit ve výsledcích vyhledávání a v populárních, a váš profil může být navrhován lidem s podobnými zájmy.",
"onboarding.profile.display_name": "Zobrazované jméno",
"onboarding.profile.display_name_hint": "Vaše celé jméno nebo přezdívka…",
"onboarding.profile.lead": "Toto můžete vždy dokončit později v nastavení, kde je k dispozici ještě více možností přizpůsobení.",
"onboarding.profile.note": "O vás",
"onboarding.profile.note_hint": "Můžete @zmínit jiné osoby nebo #hashtagy…",
"onboarding.profile.save_and_continue": "Uložit a pokračovat",
"onboarding.profile.title": "Nastavení profilu",
"onboarding.profile.upload_avatar": "Nahrát profilový obrázek",
"onboarding.profile.upload_header": "Nahrát hlavičku profilu",
"onboarding.share.lead": "Dejte lidem vědět, jak vás mohou najít na Mastodonu!",
"onboarding.share.message": "Jsem {username} na #Mastodonu! Pojď mě sledovat na {url}",
"onboarding.share.next_steps": "Možné další kroky:",
"onboarding.share.title": "Sdílejte svůj profil",
"onboarding.start.lead": "Nyní jste součástí Mastodonu, unikátní sociální sítě, kde vy - ne algoritmus - vytváří vaše vlastní prožitky. Začněte na této nové sociální platformě:",
"onboarding.start.skip": "Nepotřebujete pomoci začít?",
"onboarding.start.title": "Dokázali jste to!",
"onboarding.steps.follow_people.body": "Mastodon je o sledování zajimavých lidí.",
"onboarding.steps.follow_people.title": "Přispůsobit vlastní domovský kanál",
"onboarding.steps.publish_status.body": "Řekněte světu ahoj s pomocí textem, fotografiemi, videami nebo anketami {emoji}",
"onboarding.steps.publish_status.title": "Vytvořte svůj první příspěvek",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Přizpůsobit svůj profil",
"onboarding.steps.share_profile.body": "Dejte blízkým lidem vědět, jak vás mohou najít na Mastodonu",
"onboarding.steps.share_profile.title": "Sdílejte svůj profil",
"onboarding.tips.2fa": "<strong>Víte, že?</strong> Svůj účet můžete zabezpečit nastavením dvoufaktorového ověřování v nastavení účtu. Funguje s jakoukoli TOTP aplikací podle vašeho výběru, telefonní číslo není nutné!",
"onboarding.tips.accounts_from_other_servers": "<strong>Víte, že?</strong> Protože je Mastodon decentralizovaný, některé profily, na které narazíte, budou hostovány na jiných serverech, než je ten váš. A přesto s nimi můžete bezproblémově komunikovat! Jejich server se nachází v druhé polovině uživatelského jména!",
"onboarding.tips.migration": "<strong>Víte, že?</strong> Pokud máte pocit, že {domain} pro vás v budoucnu není vhodnou volbou, můžete se přesunout na jiný Mastodon server, aniž byste přišli o své sledující. Můžete dokonce hostovat svůj vlastní server!",
"onboarding.tips.verification": "<strong>Víte, že?</strong> Svůj účet můžete ověřit tak, že na své webové stránky umístíte odkaz na váš Mastodon profil a odkaz na stránku přidáte do svého profilu. Nejsou k tomu potřeba žádné poplatky ani dokumenty!",
"password_confirmation.exceeds_maxlength": "Potvrzení hesla překračuje maximální povolenou délku hesla",
"password_confirmation.mismatching": "Zadaná hesla se neshodují",
"picture_in_picture.restore": "Vrátit zpět",

View file

@ -129,7 +129,6 @@
"bundle_column_error.routing.body": "Nid oedd modd canfod y dudalen honno. Ydych chi'n siŵr fod yr URL yn y bar cyfeiriad yn gywir?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Cau",
"bundle_modal_error.message": "Aeth rhywbeth o'i le tra'n llwytho'r elfen hon.",
"bundle_modal_error.retry": "Ceisiwch eto",
"closed_registrations.other_server_instructions": "Gan fod Mastodon yn ddatganoledig, gallwch greu cyfrif ar weinydd arall a dal i ryngweithio gyda hwn.",
"closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.",
@ -140,13 +139,16 @@
"column.blocks": "Defnyddwyr a flociwyd",
"column.bookmarks": "Llyfrnodau",
"column.community": "Ffrwd lleol",
"column.create_list": "Creu rhestr",
"column.direct": "Crybwylliadau preifat",
"column.directory": "Pori proffiliau",
"column.domain_blocks": "Parthau wedi'u blocio",
"column.edit_list": "Golygu rhestr",
"column.favourites": "Ffefrynnau",
"column.firehose": "Ffrydiau byw",
"column.follow_requests": "Ceisiadau dilyn",
"column.home": "Cartref",
"column.list_members": "Rheoli aelodau rhestr",
"column.lists": "Rhestrau",
"column.mutes": "Defnyddwyr wedi'u tewi",
"column.notifications": "Hysbysiadau",
@ -159,6 +161,7 @@
"column_header.pin": "Pinio",
"column_header.show_settings": "Dangos gosodiadau",
"column_header.unpin": "Dadbinio",
"column_search.cancel": "Diddymu",
"column_subheading.settings": "Gosodiadau",
"community.column_settings.local_only": "Lleol yn unig",
"community.column_settings.media_only": "Cyfryngau yn unig",
@ -232,10 +235,6 @@
"disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.",
"dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.",
"dismissable_banner.dismiss": "Cau",
"dismissable_banner.explore_links": "Dyma straeon newyddion syn cael eu rhannu fwyaf ar y we gymdeithasol heddiw. Mae'r straeon newyddion diweddaraf sy'n cael eu postio gan fwy o unigolion gwahanol yn cael eu graddio'n uwch.",
"dismissable_banner.explore_statuses": "Dyma postiadau o bob gwr o'r we gymdeithasol sy'n derbyn sylw heddiw. Mae postiadau mwy diweddar sydd â mwy o hybiau a ffefrynnau'n cael eu graddio'n uwch.",
"dismissable_banner.explore_tags": "Mae'r rhain yn hashnodau sydd ar gynnydd ar y we gymdeithasol heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o unigolion gwahanol yn cael eu graddio'n uwch.",
"dismissable_banner.public_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl ar y we gymdeithasol y mae pobl ar {domain} yn eu dilyn.",
"domain_block_modal.block": "Blocio gweinydd",
"domain_block_modal.block_account_instead": "Blocio @{name} yn ei le",
"domain_block_modal.they_can_interact_with_old_posts": "Gall pobl o'r gweinydd hwn ryngweithio â'ch hen bostiadau.",
@ -464,11 +463,32 @@
"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}}",
"lists.add_member": "Ychwanegu",
"lists.add_to_list": "Ychwanegu at restr",
"lists.add_to_lists": "Ychwanegu {name} at restrau",
"lists.create": "Creu",
"lists.create_a_list_to_organize": "Creu rhestr newydd i drefnu eich llif Cartref",
"lists.create_list": "Creu rhestr",
"lists.delete": "Dileu rhestr",
"lists.done": "Wedi gorffen",
"lists.edit": "Golygu rhestr",
"lists.exclusive": "Cuddio aelodau yn y Cartref",
"lists.exclusive_hint": "Os oes rhywun ar y rhestr hon, cuddiwch nhw yn eich llif Cartref i osgoi gweld eu postiadau ddwywaith.",
"lists.find_users_to_add": "Canfod defnyddwyr i'w hychwanegu",
"lists.list_members": "Aelodau rhestr",
"lists.list_members_count": "{count, plural, one {# aelod} other {# aelod}}",
"lists.list_name": "Enw rhestr",
"lists.new_list_name": "Enw rhestr newydd",
"lists.no_lists_yet": "Dim rhestrau eto.",
"lists.no_members_yet": "Dim aelodau eto.",
"lists.no_results_found": "Heb ganfod canlyniadau.",
"lists.remove_member": "Tynnu",
"lists.replies_policy.followed": "Unrhyw ddefnyddiwr sy'n cael ei ddilyn",
"lists.replies_policy.list": "Aelodau'r rhestr",
"lists.replies_policy.none": "Neb",
"lists.save": "Cadw",
"lists.search_placeholder": "Chwiliwch am bobl rydych chi'n eu dilyn",
"lists.show_replies_to": "Cynhwyswch atebion gan aelodau'r rhestr i",
"load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
"loading_indicator.label": "Yn llwytho…",
"media_gallery.hide": "Cuddio",
@ -625,44 +645,21 @@
"notifications_permission_banner.enable": "Galluogi hysbysiadau bwrdd gwaith",
"notifications_permission_banner.how_to_control": "I dderbyn hysbysiadau pan nad yw Mastodon ar agor, galluogwch hysbysiadau bwrdd gwaith. Gallwch reoli'n union pa fathau o ryngweithiadau sy'n cynhyrchu hysbysiadau bwrdd gwaith trwy'r botwm {icon} uchod unwaith y byddan nhw wedi'u galluogi.",
"notifications_permission_banner.title": "Peidiwch â cholli dim",
"onboarding.action.back": "Ewch â fi nôl",
"onboarding.actions.back": "Ewch â fi nôl",
"onboarding.actions.go_to_explore": "Gweld y pynciau llosg",
"onboarding.actions.go_to_home": "Ewch i'm ffrwd gartref",
"onboarding.compose.template": "Helo, #Mastodon!",
"onboarding.follows.back": "Nôl",
"onboarding.follows.done": "Wedi gorffen",
"onboarding.follows.empty": "Yn anffodus, nid oes modd dangos unrhyw ganlyniadau ar hyn o bryd. Gallwch geisio defnyddio chwilio neu bori'r dudalen archwilio i ddod o hyd i bobl i'w dilyn, neu ceisio eto yn nes ymlaen.",
"onboarding.follows.lead": "Rydych chi'n curadu eich ffrwd gartref eich hun. Po fwyaf o bobl y byddwch chi'n eu dilyn, y mwyaf egnïol a diddorol fydd hi. Gall y proffiliau hyn fod yn fan cychwyn da - gallwch chi bob amser eu dad-ddilyn yn nes ymlaen:",
"onboarding.follows.title": "Personolwch eich ffrwd gartref",
"onboarding.follows.search": "Chwilio",
"onboarding.follows.title": "Dilynwch bobl i gychwyn arni",
"onboarding.profile.discoverable": "Gwnewch fy mhroffil yn un y gellir ei ddarganfod",
"onboarding.profile.discoverable_hint": "Pan fyddwch yn optio i mewn i ddarganfodadwyedd ar Mastodon, gall eich postiadau ymddangos mewn canlyniadau chwilio a threndiau, ac efallai y bydd eich proffil yn cael ei awgrymu i bobl sydd â diddordebau tebyg i chi.",
"onboarding.profile.display_name": "Enw dangos",
"onboarding.profile.display_name_hint": "Eich enw llawn neu'ch enw hwyl…",
"onboarding.profile.lead": "Gallwch chi bob amser gwblhau hyn yn ddiweddarach yn y gosodiadau, lle mae hyd yn oed mwy o ddewisiadau cyfaddasu ar gael.",
"onboarding.profile.note": "Bywgraffiad",
"onboarding.profile.note_hint": "Gallwch @grybwyll pobl eraill neu #hashnodau…",
"onboarding.profile.save_and_continue": "Cadw a pharhau",
"onboarding.profile.title": "Gosodiad proffil",
"onboarding.profile.upload_avatar": "Llwytho llun proffil",
"onboarding.profile.upload_header": "Llwytho pennyn proffil",
"onboarding.share.lead": "Cofiwch ddweud wrth bobl sut y gallan nhw ddod o hyd i chi ar Mastodon!",
"onboarding.share.message": "Fi yw {username} ar #Mastodon! Dewch i'm dilyn i yn {url}",
"onboarding.share.next_steps": "Camau nesaf posib:",
"onboarding.share.title": "Rhannwch eich proffil",
"onboarding.start.lead": "Mae eich cyfrif Mastodon newydd yn barod! Dyma sut y gallwch chi wneud y gorau ohono:",
"onboarding.start.skip": "Eisiau mynd syth yn eich blaen?",
"onboarding.start.title": "Rydych chi wedi cyrraedd!",
"onboarding.steps.follow_people.body": "Rydych chi'n curadu eich ffrwd eich hun. Gadewch i ni ei lenwi â phobl ddiddorol.",
"onboarding.steps.follow_people.title": "Personolwch eich ffrwd gartref",
"onboarding.steps.publish_status.body": "Dywedwch helo wrth y byd gyda thestun, lluniau, fideos neu arolygon barn {emoji}",
"onboarding.steps.publish_status.title": "Gwnewch eich postiad cyntaf",
"onboarding.steps.setup_profile.body": "Mae eraill yn fwy tebygol o ryngweithio â chi gyda phroffil wedi'i lenwi.",
"onboarding.steps.setup_profile.title": "Cyfaddaswch eich proffil",
"onboarding.steps.share_profile.body": "Gadewch i'ch ffrindiau wybod sut i ddod o hyd i chi ar Mastodon",
"onboarding.steps.share_profile.title": "Rhannwch eich proffil",
"onboarding.tips.2fa": "<strong>Oeddech chi'n gwybod?</strong> Gallwch ddiogelu'ch cyfrif trwy osod dilysiad dau ffactor yng ngosodiadau eich cyfrif. Mae'n gweithio gydag unrhyw app TOTP o'ch dewis, nid oes angen rhif ffôn!",
"onboarding.tips.accounts_from_other_servers": "<strong>Oeddech chi'n gwybod?</strong> Gan fod Mastodon wedi'i ddatganoli, bydd rhai proffiliau y dewch ar eu traws yn cael eu cynnal ar weinyddion heblaw eich un chi. Ac eto gallwch chi ryngweithio â nhw yn hawdd! Mae eu gweinydd yn ail hanner eu henw defnyddiwr!",
"onboarding.tips.migration": "<strong>Oeddech chi'n gwybod?</strong> Os ydych chi'n teimlo nad yw {domain} yn ddewis gweinydd gwych i chi i'r dyfodol, gallwch chi symud i weinydd Mastodon arall heb golli'ch dilynwyr. Gallwch chi hyd yn oed gynnal eich gweinydd eich hun!",
"onboarding.tips.verification": "<strong>Oeddech chi'n gwybod?</strong> Gallwch wirio'ch cyfrif trwy roi dolen i'ch proffil Mastodon ar eich gwefan eich hun ac ychwanegu'r wefan at eich proffil. Nid oes angen ffioedd na dogfennau!",
"password_confirmation.exceeds_maxlength": "Mae'r cadarnhad cyfrinair yn fwy nag uchafswm hyd y cyfrinair",
"password_confirmation.mismatching": "Nid yw'r cadarnhad cyfrinair yn cyfateb",
"picture_in_picture.restore": "Rhowch ef yn ôl",

View file

@ -129,7 +129,7 @@
"bundle_column_error.routing.body": "Den anmodede side kunne ikke findes. Er du sikker på, at URL'en er korrekt?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Luk",
"bundle_modal_error.message": "Noget gik galt under indlæsningen af denne komponent.",
"bundle_modal_error.message": "Noget gik galt under indlæsningen af denne skærm.",
"bundle_modal_error.retry": "Forsøg igen",
"closed_registrations.other_server_instructions": "Da Mastodon er decentraliseret, kan du oprette en konto på en anden server og stadig interagere med denne.",
"closed_registrations_modal.description": "Oprettelse af en konto på {domain} er i øjeblikket ikke muligt, men husk på, at du ikke behøver en konto specifikt på {domain} for at bruge Mastodon.",
@ -162,6 +162,7 @@
"column_header.pin": "Fastgør",
"column_header.show_settings": "Vis indstillinger",
"column_header.unpin": "Løsgør",
"column_search.cancel": "Afbryd",
"column_subheading.settings": "Indstillinger",
"community.column_settings.local_only": "Kun lokalt",
"community.column_settings.media_only": "Kun medier",
@ -235,10 +236,10 @@
"disabled_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret.",
"dismissable_banner.community_timeline": "Disse er de seneste offentlige indlæg fra personer med konti hostet af {domain}.",
"dismissable_banner.dismiss": "Afvis",
"dismissable_banner.explore_links": "Der tales lige nu om disse nyhedshistorier af folk på denne og andre servere i det decentraliserede netværk.",
"dismissable_banner.explore_statuses": "Disse indlæg fra diverse sociale netværk vinder fodfæste i dag. Nyere indlæg med flere boosts og favoritter rangeres højere.",
"dismissable_banner.explore_tags": "Disse hashtages vinder lige nu fodfæste blandt folk på denne og andre servere i det decentraliserede netværk.",
"dismissable_banner.public_timeline": "Dette er de seneste offentlige indlæg fra folk på det sociale netværk, som folk på {domain} følger.",
"dismissable_banner.explore_links": "Disse nyhedshistorier deles mest på fediverset i dag. Nyere nyhedshistorier lagt op af flere forskellige personer rangeres højere.",
"dismissable_banner.explore_statuses": "Disse indlæg på tværs af fediverset opnår momentum i dag. Nyere indlæg med flere boosts og favoritter rangeres højere.",
"dismissable_banner.explore_tags": "Disse hashtags opnår momentum på fediverset i dag. Hashtags brugt af flere forskellige personer rangeres højere.",
"dismissable_banner.public_timeline": "Dette er de seneste offentlige indlæg fra personer på fediverset, som folk på {domain} følger.",
"domain_block_modal.block": "Blokér server",
"domain_block_modal.block_account_instead": "Blokér i stedet @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Folk fra denne server kan interagere med de gamle indlæg.",
@ -362,6 +363,7 @@
"footer.status": "Status",
"generic.saved": "Gemt",
"getting_started.heading": "Startmenu",
"hashtag.admin_moderation": "Åbn modereringsbrugerflade for #{name}",
"hashtag.column_header.tag_mode.all": "og {additional}",
"hashtag.column_header.tag_mode.any": "eller {additional}",
"hashtag.column_header.tag_mode.none": "uden {additional}",
@ -648,44 +650,21 @@
"notifications_permission_banner.enable": "Aktivér computernotifikationer",
"notifications_permission_banner.how_to_control": "Aktivér computernotifikationer for at få besked, når Mastodon ikke er åben. Når de er aktiveret, kan man via knappen {icon} ovenfor præcist styre, hvilke typer af interaktioner, som genererer computernotifikationer.",
"notifications_permission_banner.title": "Gå aldrig glip af noget",
"onboarding.action.back": "Gå tilbage",
"onboarding.actions.back": "Gå tilbage",
"onboarding.actions.go_to_explore": "Se, hvad som trender",
"onboarding.actions.go_to_home": "Gå til hjemme-feed'et",
"onboarding.compose.template": "Hej #Mastodon!",
"onboarding.follows.back": "Retur",
"onboarding.follows.done": "Færdig",
"onboarding.follows.empty": "Ingen resultater tilgængelige pt. Prøv at bruge søgning eller gennemse siden for at finde personer at følge, eller forsøg igen senere.",
"onboarding.follows.lead": "Man kurerer sin eget hjemme-feed. Jo flere personer man følger, des mere aktiv og interessant vil det være. Disse profiler kan være et godt udgangspunkt de kan altid fjernes senere!",
"onboarding.follows.title": "Populært på Mastodon",
"onboarding.follows.search": "Søg",
"onboarding.follows.title": "Følg folk for at komme i gang",
"onboarding.profile.discoverable": "Gør min profil synlig",
"onboarding.profile.discoverable_hint": "Når man vælger at være synlig på Mastodon, kan ens indlæg fremgå i søgeresultater og tendenser, og profilen kan blive foreslået til andre med tilsvarende interesse.",
"onboarding.profile.display_name": "Visningsnavn",
"onboarding.profile.display_name_hint": "Fulde navn eller dit sjove navn…",
"onboarding.profile.lead": "Dette kan altid færdiggøres senere i indstillingerne, hvor endnu flere tilpasningsmuligheder forefindes.",
"onboarding.profile.note": "Bio",
"onboarding.profile.note_hint": "Man kan @omtale andre personer eller #hashtags…",
"onboarding.profile.save_and_continue": "Gem og fortsæt",
"onboarding.profile.title": "Profilopsætning",
"onboarding.profile.upload_avatar": "Upload profilbillede",
"onboarding.profile.upload_header": "Upload profiloverskrift",
"onboarding.share.lead": "Lad folk vide, hvordan de kan finde dig på Mastodon!",
"onboarding.share.message": "Jeg er {username} på #Mastodon! Følg mig på {url}",
"onboarding.share.next_steps": "Mulige næste trin:",
"onboarding.share.title": "Del profilen",
"onboarding.start.lead": "Den nye Mastodon konto er klar til brug. Sådan kan man få mest muligt ud af den:",
"onboarding.start.skip": "Vil springe længere frem?",
"onboarding.start.title": "Du klarede det!",
"onboarding.steps.follow_people.body": "Man kurerer sit eget feed. Lad os fylde det med interessante personer.",
"onboarding.steps.follow_people.title": "Følg {count, plural, one {en person} other {# personer}}",
"onboarding.steps.publish_status.body": "Sig hej til verden med tekst, billeder, videoer eller afstemninger {emoji}",
"onboarding.steps.publish_status.title": "Skriv dit første indlæg",
"onboarding.steps.setup_profile.body": "Andre er mere tilbøjelige til at interagere, hvis man har udfyldt sin profil.",
"onboarding.steps.setup_profile.title": "Tilpas profilen",
"onboarding.steps.share_profile.body": "Lad vennerne vide, hvordan de finder dig på Mastodon!",
"onboarding.steps.share_profile.title": "Del profilen",
"onboarding.tips.2fa": "<strong>Vidste du?</strong> Man kan sikre sin konto ved at opsætte tofaktorgodkendelse i kontoindstillingerne. Det virker med enhver valgt TOTP-app, intet telefonnummer nødvendigt!",
"onboarding.tips.accounts_from_other_servers": "<strong>Vidste du?</strong> Da Mastodon er decentraliseret, vil nogle af de profiler, man støder på, være hostet på andre servere end ens egen. Alligevel kan man interagere med dem problemfrit! Deres servernavn udgør anden halvdel af deres brugernavn!",
"onboarding.tips.migration": "<strong>Vidste du?</strong> Synes man ikke, at {domain} er et godt servervalg fremadrettet, kan man flytte til en anden Mastodon-server uden at miste sine følgere. Man kan endda hoste sin egen server!",
"onboarding.tips.verification": "<strong>Vidste du det?</strong> Man kan bekræfte sin konto ved at placere sit Mastodon-profillink på sin egen websted og føje webstedet til sin profil. Ingen gebyrer eller dokumenter påkrævet!",
"password_confirmation.exceeds_maxlength": "Adgangskodebekræftelse overstiger maks. adgangskodelængde",
"password_confirmation.mismatching": "Adgangskodebekræftelse matcher ikke",
"picture_in_picture.restore": "Indsæt det igen",

View file

@ -19,7 +19,7 @@
"account.block_domain": "{domain} sperren",
"account.block_short": "Blockieren",
"account.blocked": "Blockiert",
"account.cancel_follow_request": "Folgeanfrage zurückziehen",
"account.cancel_follow_request": "Follower-Anfrage zurückziehen",
"account.copy": "Link zum Profil kopieren",
"account.direct": "@{name} privat erwähnen",
"account.disable_notifications": "Höre auf mich zu benachrichtigen wenn @{name} etwas postet",
@ -89,9 +89,9 @@
"announcement.announcement": "Ankündigung",
"annual_report.summary.archetype.booster": "Trendjäger*in",
"annual_report.summary.archetype.lurker": "Beobachter*in",
"annual_report.summary.archetype.oracle": "Orakel",
"annual_report.summary.archetype.oracle": "Universaltalent",
"annual_report.summary.archetype.pollster": "Meinungsforscher*in",
"annual_report.summary.archetype.replier": "Geselliger Schmetterling",
"annual_report.summary.archetype.replier": "Sozialer Schmetterling",
"annual_report.summary.followers.followers": "Follower",
"annual_report.summary.followers.total": "{count} insgesamt",
"annual_report.summary.here_it_is": "Dein Jahresrückblick für {year}:",
@ -113,7 +113,7 @@
"block_modal.show_more": "Mehr anzeigen",
"block_modal.they_cant_mention": "Das Profil wird dich nicht erwähnen oder dir folgen können.",
"block_modal.they_cant_see_posts": "Deine Beiträge können nicht mehr angesehen werden und du wirst deren Beiträge nicht mehr sehen.",
"block_modal.they_will_know": "Es wird erkennbar sein, dass dieses Profil blockiert wurde.",
"block_modal.they_will_know": "Das Profil wird erkennen können, dass du es blockiert hast.",
"block_modal.title": "Profil blockieren?",
"block_modal.you_wont_see_mentions": "Du wirst keine Beiträge sehen, die dieses Profil erwähnen.",
"boost_modal.combo": "Mit {combo} erscheint dieses Fenster beim nächsten Mal nicht mehr",
@ -129,7 +129,7 @@
"bundle_column_error.routing.body": "Die angeforderte Seite konnte nicht gefunden werden. Bist du dir sicher, dass die URL in der Adressleiste korrekt ist?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Schließen",
"bundle_modal_error.message": "Beim Laden dieser Komponente ist etwas schiefgelaufen.",
"bundle_modal_error.message": "Beim Laden des Inhalts ist etwas schiefgelaufen.",
"bundle_modal_error.retry": "Erneut versuchen",
"closed_registrations.other_server_instructions": "Da Mastodon dezentralisiert ist, kannst du ein Konto auf einem anderen Server erstellen und trotzdem mit diesem Server interagieren.",
"closed_registrations_modal.description": "Das Anlegen eines Kontos auf {domain} ist derzeit nicht möglich, aber bedenke, dass du kein extra Konto auf {domain} benötigst, um Mastodon nutzen zu können.",
@ -162,6 +162,7 @@
"column_header.pin": "Anheften",
"column_header.show_settings": "Einstellungen anzeigen",
"column_header.unpin": "Lösen",
"column_search.cancel": "Abbrechen",
"column_subheading.settings": "Einstellungen",
"community.column_settings.local_only": "Nur lokal",
"community.column_settings.media_only": "Nur Beiträge mit Medien",
@ -179,7 +180,7 @@
"compose_form.placeholder": "Was gibts Neues?",
"compose_form.poll.duration": "Umfragedauer",
"compose_form.poll.multiple": "Mehrfachauswahl",
"compose_form.poll.option_placeholder": "Option {number}",
"compose_form.poll.option_placeholder": "{number}. Auswahl",
"compose_form.poll.single": "Einfachauswahl",
"compose_form.poll.switch_to_multiple": "Mehrfachauswahl erlauben",
"compose_form.poll.switch_to_single": "Nur Einfachauswahl erlauben",
@ -235,10 +236,10 @@
"disabled_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert.",
"dismissable_banner.community_timeline": "Das sind die neuesten öffentlichen Beiträge von Profilen, deren Konten von {domain} verwaltet werden.",
"dismissable_banner.dismiss": "Ablehnen",
"dismissable_banner.explore_links": "Diese Nachrichten werden heute am häufigsten im Social Web geteilt. Neuere Nachrichten, die von vielen verschiedenen Profilen geteilt wurden, erscheinen weiter oben.",
"dismissable_banner.explore_statuses": "Diese Beiträge sind heute im Social Web sehr beliebt. Neuere Beiträge, die häufiger geteilt und favorisiert wurden, erscheinen weiter oben.",
"dismissable_banner.explore_tags": "Diese Hashtags sind heute im Social Web sehr beliebt. Hashtags, die von vielen verschiedenen Profilen verwendet werden, erscheinen weiter oben.",
"dismissable_banner.public_timeline": "Das sind die neuesten öffentlichen Beiträge von Profilen im Social Web, denen Leute auf {domain} folgen.",
"dismissable_banner.explore_links": "Diese Nachrichten werden heute am häufigsten im Fediverse geteilt. Neuere Nachrichten, die von vielen verschiedenen Profilen geteilt wurden, erscheinen weiter oben.",
"dismissable_banner.explore_statuses": "Diese Beiträge sind heute im Fediverse sehr beliebt. Neuere Beiträge, die häufiger geteilt und favorisiert wurden, erscheinen weiter oben.",
"dismissable_banner.explore_tags": "Diese Hashtags sind heute im Fediverse sehr beliebt. Hashtags, die von vielen verschiedenen Profilen verwendet werden, erscheinen weiter oben.",
"dismissable_banner.public_timeline": "Das sind die neuesten öffentlichen Beiträge von Profilen im Fediverse, denen Leute auf {domain} folgen.",
"domain_block_modal.block": "Server blockieren",
"domain_block_modal.block_account_instead": "Stattdessen @{name} blockieren",
"domain_block_modal.they_can_interact_with_old_posts": "Profile von diesem Server werden mit deinen älteren Beiträgen interagieren können.",
@ -255,7 +256,7 @@
"domain_pill.their_server": "Deren digitale Heimat. Hier „leben“ alle Beiträge von diesem Profil.",
"domain_pill.their_username": "Deren eindeutigen Identität auf dem betreffenden Server. Es ist möglich, Profile mit dem gleichen Profilnamen auf verschiedenen Servern zu finden.",
"domain_pill.username": "Profilname",
"domain_pill.whats_in_a_handle": "Was ist Teil der Adresse?",
"domain_pill.whats_in_a_handle": "Woraus besteht eine Adresse?",
"domain_pill.who_they_are": "Adressen teilen mit, wer jemand ist und wo sich jemand aufhält. Daher kannst du mit Leuten im gesamten Social Web interagieren, wenn es eine durch <button>ActivityPub angetriebene Plattform</button> ist.",
"domain_pill.who_you_are": "Deine Adresse teilt mit, wer du bist und wo du dich aufhältst. Daher können andere Leute im gesamten Social Web mit dir interagieren, wenn es eine durch <button>ActivityPub angetriebene Plattform</button> ist.",
"domain_pill.your_handle": "Deine Adresse:",
@ -330,9 +331,9 @@
"filter_warning.matches_filter": "Übereinstimmend mit dem Filter „<span>{title}</span>“",
"filtered_notifications_banner.pending_requests": "Von {count, plural, =0 {keinem, den} one {einer Person, die} other {# Personen, die}} du möglicherweise kennst",
"filtered_notifications_banner.title": "Gefilterte Benachrichtigungen",
"firehose.all": "Alles",
"firehose.all": "Alle Server",
"firehose.local": "Dieser Server",
"firehose.remote": "Andere Server",
"firehose.remote": "Externe Server",
"follow_request.authorize": "Genehmigen",
"follow_request.reject": "Ablehnen",
"follow_requests.unlocked_explanation": "Auch wenn dein Konto öffentlich bzw. nicht geschützt ist, haben die Moderator*innen von {domain} gedacht, dass du diesen Follower lieber manuell bestätigen solltest.",
@ -362,6 +363,7 @@
"footer.status": "Status",
"generic.saved": "Gespeichert",
"getting_started.heading": "Auf gehts!",
"hashtag.admin_moderation": "#{name} moderieren",
"hashtag.column_header.tag_mode.all": "und {additional}",
"hashtag.column_header.tag_mode.any": "oder {additional}",
"hashtag.column_header.tag_mode.none": "ohne {additional}",
@ -492,7 +494,7 @@
"lists.replies_policy.none": "Niemanden",
"lists.save": "Speichern",
"lists.search_placeholder": "Nach Profilen suchen, denen du folgst",
"lists.show_replies_to": "Antworten von Listenmitgliedern anzeigen für …",
"lists.show_replies_to": "Antworten von Listenmitgliedern einbeziehen an …",
"load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
"loading_indicator.label": "Wird geladen …",
"media_gallery.hide": "Ausblenden",
@ -541,7 +543,7 @@
"notification.admin.report_statuses_other": "{name} meldete {target}",
"notification.admin.sign_up": "{name} registrierte sich",
"notification.admin.sign_up.name_and_others": "{name} und {count, plural, one {# weiteres Profil} other {# weitere Profile}} registrierten sich",
"notification.annual_report.message": "Dein {year} #Wrapstodon erwartet dich! Lass deine Highlights und unvergesslichen Momente auf Mastodon erneut aufleben!",
"notification.annual_report.message": "Dein #Wrapstodon für {year} erwartet dich! Lass deine Highlights und unvergesslichen Momente auf Mastodon erneut aufleben!",
"notification.annual_report.view": "#Wrapstodon ansehen",
"notification.favourite": "{name} favorisierte deinen Beitrag",
"notification.favourite.name_and_others_with_link": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> favorisierten deinen Beitrag",
@ -649,44 +651,21 @@
"notifications_permission_banner.enable": "Aktiviere Desktop-Benachrichtigungen",
"notifications_permission_banner.how_to_control": "Um Benachrichtigungen zu erhalten, wenn Mastodon nicht geöffnet ist, aktiviere die Desktop-Benachrichtigungen. Du kannst genau bestimmen, welche Arten von Interaktionen Desktop-Benachrichtigungen über die {icon} -Taste erzeugen, sobald diese aktiviert sind.",
"notifications_permission_banner.title": "Nichts verpassen",
"onboarding.action.back": "Bring mich zurück",
"onboarding.actions.back": "Bring mich zurück",
"onboarding.actions.go_to_explore": "Zeig mir die Trends",
"onboarding.actions.go_to_home": "Bring mich zu meiner Startseite",
"onboarding.compose.template": "Hallo #Mastodon!",
"onboarding.follows.back": "Zurück",
"onboarding.follows.done": "Fertig",
"onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden oder du versuchst es später erneut.",
"onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Vorschläge:",
"onboarding.follows.title": "Personalisiere deine Startseite",
"onboarding.follows.search": "Suchen",
"onboarding.follows.title": "Folge Profilen, um loszulegen",
"onboarding.profile.discoverable": "Mein Profil darf entdeckt werden",
"onboarding.profile.discoverable_hint": "Wenn du entdeckt werden möchtest, dann können deine Beiträge in Suchergebnissen und Trends erscheinen. Dein Profil kann ebenfalls anderen mit ähnlichen Interessen vorgeschlagen werden.",
"onboarding.profile.display_name": "Anzeigename",
"onboarding.profile.display_name_hint": "Dein richtiger Name oder dein Fantasiename …",
"onboarding.profile.lead": "Du kannst dein Profil später in den Einstellungen vervollständigen. Dort stehen weitere Anpassungsmöglichkeiten zur Verfügung.",
"onboarding.profile.note": "Über mich",
"onboarding.profile.note_hint": "Du kannst andere @Profile erwähnen oder #Hashtags verwenden …",
"onboarding.profile.save_and_continue": "Speichern und fortfahren",
"onboarding.profile.title": "Profil einrichten",
"onboarding.profile.upload_avatar": "Profilbild hochladen",
"onboarding.profile.upload_header": "Titelbild hochladen",
"onboarding.share.lead": "Lass die Leute wissen, wie sie dich auf Mastodon finden können!",
"onboarding.share.message": "Ich bin {username} auf #Mastodon! Folge mir auf {url}",
"onboarding.share.next_steps": "Mögliche nächste Schritte:",
"onboarding.share.title": "Teile dein Profil",
"onboarding.start.lead": "Du bist nun ein Teil von Mastodon eine einzigartige, dezentralisierte Social-Media-Plattform, bei der du und kein Algorithmus deine eigene Erfahrung gestaltest. Fangen wir an, diese neue soziale Dimension zu erkunden:",
"onboarding.start.skip": "Du benötigst keine Hilfe für den Einstieg?",
"onboarding.start.title": "Du hast es geschafft!",
"onboarding.steps.follow_people.body": "Interessanten Profilen zu folgen ist das, was Mastodon ausmacht.",
"onboarding.steps.follow_people.title": "Personalisiere deine Startseite",
"onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen. {emoji}",
"onboarding.steps.publish_status.title": "Erstelle deinen ersten Beitrag",
"onboarding.steps.setup_profile.body": "Mit einem vollständigen Profil interagieren andere eher mit dir.",
"onboarding.steps.setup_profile.title": "Personalisiere dein Profil",
"onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können.",
"onboarding.steps.share_profile.title": "Teile dein Mastodon-Profil",
"onboarding.tips.2fa": "<strong>Wusstest du schon?</strong> Du kannst die Sicherheit deines Kontos erhöhen, indem du die Zwei-Faktor-Authentisierung in deinen Kontoeinstellungen aktivierst. Dafür ist keine Telefonnummer notwendig und es funktioniert jede beliebige TOTP-App!",
"onboarding.tips.accounts_from_other_servers": "<strong>Wusstest du schon?</strong> Da Mastodon dezentralisiert ist, werden einige Profile, denen du begegnest, auf anderen Servern als deinem bereitgestellt. Und trotzdem kannst du uneingeschränkt mit ihnen interagieren! Der Servername befindet sich in der zweiten Hälfte ihres Profilnamens!",
"onboarding.tips.migration": "<strong>Wusstest du schon?</strong> Wenn du das Gefühl hast, dass {domain} in Zukunft nicht die richtige Serverwahl für dich ist, kannst du auf einen anderen Mastodon-Server umziehen, ohne deine Follower zu verlieren. Du kannst sogar deinen eigenen Server betreiben!",
"onboarding.tips.verification": "<strong>Wusstest du schon?</strong> Du kannst dein Konto verifizieren, indem du auf deiner Website auf dein Mastodon-Profil verlinkst und den Link deiner Website zu deinem Profil hinzufügst. Völlig kostenlos und ohne Dokumente einsenden zu müssen!",
"password_confirmation.exceeds_maxlength": "Passwortbestätigung überschreitet die maximal erlaubte Zeichenanzahl",
"password_confirmation.mismatching": "Passwortbestätigung stimmt nicht überein",
"picture_in_picture.restore": "Zurücksetzen",

View file

@ -101,6 +101,7 @@
"annual_report.summary.highlighted_post.possessive": "του χρήστη {name}",
"annual_report.summary.most_used_app.most_used_app": "πιο χρησιμοποιημένη εφαρμογή",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "πιο χρησιμοποιημένη ετικέτα",
"annual_report.summary.most_used_hashtag.none": "Κανένα",
"annual_report.summary.new_posts.new_posts": "νέες αναρτήσεις",
"annual_report.summary.percentile.text": "<topLabel>Αυτό σε βάζει στην κορυφή του </topLabel><percentage></percentage><bottomLabel>των χρηστών του Mastodon.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Δεν θα το πούμε στον Bernie.",
@ -128,7 +129,7 @@
"bundle_column_error.routing.body": "Η επιθυμητή σελίδα δεν βρέθηκε. Είναι σωστό το URL στο πεδίο διευθύνσεων;",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Κλείσιμο",
"bundle_modal_error.message": "Κάτι πήγε στραβά κατά τη φόρτωση του στοιχείου.",
"bundle_modal_error.message": "Κάτι πήγε στραβά κατά τη φόρτωση αυτής της οθόνης.",
"bundle_modal_error.retry": "Δοκίμασε ξανά",
"closed_registrations.other_server_instructions": "Καθώς το Mastodon είναι αποκεντρωμένο, μπορείς να δημιουργήσεις λογαριασμό σε άλλον διακομιστή αλλά να συνεχίσεις να αλληλεπιδράς με αυτόν.",
"closed_registrations_modal.description": "Η δημιουργία λογαριασμού στον {domain} προς το παρόν δεν είναι δυνατή, αλλά λάβε υπόψη ότι δεν χρειάζεσαι λογαριασμό ειδικά στον {domain} για να χρησιμοποιήσεις το Mastodon.",
@ -139,13 +140,16 @@
"column.blocks": "Αποκλεισμένοι χρήστες",
"column.bookmarks": "Σελιδοδείκτες",
"column.community": "Τοπική ροή",
"column.create_list": "Δημιουργία λίστας",
"column.direct": "Ιδιωτικές αναφορές",
"column.directory": "Περιήγηση στα προφίλ",
"column.domain_blocks": "Αποκλεισμένοι τομείς",
"column.edit_list": "Επεξεργασία λίστας",
"column.favourites": "Αγαπημένα",
"column.firehose": "Ζωντανές ροές",
"column.follow_requests": "Αιτήματα ακολούθησης",
"column.home": "Αρχική",
"column.list_members": "Διαχείριση μελών λίστας",
"column.lists": "Λίστες",
"column.mutes": "Αποσιωπημένοι χρήστες",
"column.notifications": "Ειδοποιήσεις",
@ -158,6 +162,7 @@
"column_header.pin": "Καρφίτσωμα",
"column_header.show_settings": "Εμφάνιση ρυθμίσεων",
"column_header.unpin": "Ξεκαρφίτσωμα",
"column_search.cancel": "Ακύρωση",
"column_subheading.settings": "Ρυθμίσεις",
"community.column_settings.local_only": "Τοπικά μόνο",
"community.column_settings.media_only": "Μόνο πολυμέσα",
@ -231,10 +236,8 @@
"disabled_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προς το παρόν απενεργοποιημένος.",
"dismissable_banner.community_timeline": "Αυτές είναι οι πιο πρόσφατες δημόσιες αναρτήσεις ατόμων των οποίων οι λογαριασμοί φιλοξενούνται στο {domain}.",
"dismissable_banner.dismiss": "Παράβλεψη",
"dismissable_banner.explore_links": "Αυτές οι ειδήσεις συζητούνται σε αυτόν και άλλους διακομιστές του αποκεντρωμένου δικτύου αυτή τη στιγμή.",
"dismissable_banner.explore_statuses": "Αυτές είναι οι αναρτήσεις που έχουν απήχηση στο κοινωνικό δίκτυο σήμερα. Οι νεώτερες αναρτήσεις με περισσότερες προωθήσεις και προτιμήσεις κατατάσσονται ψηλότερα.",
"dismissable_banner.explore_tags": "Αυτές οι ετικέτες αποκτούν απήχηση σε αυτόν και άλλους διακομιστές του αποκεντρωμένου δικτύου αυτή τη στιγμή.",
"dismissable_banner.public_timeline": "Αυτές είναι οι πιο πρόσφατες δημόσιες αναρτήσεις από άτομα στον κοινωνικό ιστό που ακολουθούν άτομα από το {domain}.",
"dismissable_banner.explore_links": "Αυτές οι ιστορίες ειδήσεων μοιράζονται περισσότερο στο fediverse σήμερα. Νεότερες ιστορίες ειδήσεων που δημοσιεύτηκαν από πιο διαφορετικά άτομα κατατάσσονται υψηλότερα.",
"dismissable_banner.explore_statuses": "Αυτές οι αναρτήσεις από όλο το fediverse κερδίζουν την προσοχή σήμερα. Νεότερες αναρτήσεις με περισσότερες ενισχύσεις και αγαπημένα κατατάσσονται υψηλότερα.",
"domain_block_modal.block": "Αποκλεισμός διακομιστή",
"domain_block_modal.block_account_instead": "Αποκλεισμός @{name} αντ' αυτού",
"domain_block_modal.they_can_interact_with_old_posts": "Άτομα από αυτόν τον διακομιστή μπορούν να αλληλεπιδράσουν με τις παλιές αναρτήσεις σου.",
@ -624,44 +627,17 @@
"notifications_permission_banner.enable": "Ενεργοποίηση ειδοποιήσεων επιφάνειας εργασίας",
"notifications_permission_banner.how_to_control": "Για να λαμβάνεις ειδοποιήσεις όταν το Mastodon δεν είναι ανοιχτό, ενεργοποίησε τις ειδοποιήσεις επιφάνειας εργασίας. Μπορείς να ελέγξεις με ακρίβεια ποιοι τύποι αλληλεπιδράσεων δημιουργούν ειδοποιήσεις επιφάνειας εργασίας μέσω του κουμπιού {icon} μόλις ενεργοποιηθούν.",
"notifications_permission_banner.title": "Μη χάσεις στιγμή",
"onboarding.action.back": "Επιστροφή",
"onboarding.actions.back": "Επιστροφή",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Πηγαίνετε στην αρχική σας ροή",
"onboarding.compose.template": "Γειά σου #Mastodon!",
"onboarding.follows.empty": "Δυστυχώς, δεν μπορούν να εμφανιστούν αποτελέσματα αυτή τη στιγμή. Μπορείς να προσπαθήσεις να χρησιμοποιήσεις την αναζήτηση ή να περιηγηθείς στη σελίδα εξερεύνησης για να βρεις άτομα να ακολουθήσεις ή να δοκιμάσεις ξανά αργότερα.",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Δημοφιλή στο Mastodon",
"onboarding.profile.discoverable": "Κάνε το προφίλ μου ανακαλύψιμο",
"onboarding.profile.discoverable_hint": "Όταν επιλέγεις την δυνατότητα ανακάλυψης στο Mastodon, οι αναρτήσεις σου μπορεί να εμφανιστούν στα αποτελέσματα αναζήτησης και τις τάσεις, και το προφίλ σου μπορεί να προτείνεται σε άτομα με παρόμοια ενδιαφέροντα με εσένα.",
"onboarding.profile.display_name": "Εμφανιζόμενο όνομα",
"onboarding.profile.display_name_hint": "Το πλήρες ή το διασκεδαστικό σου όνομα…",
"onboarding.profile.lead": "Μπορείς πάντα να το ολοκληρώσεις αργότερα στις ρυθμίσεις, όπου είναι διαθέσιμες ακόμα περισσότερες επιλογές προσαρμογής.",
"onboarding.profile.note": "Βιογραφικό",
"onboarding.profile.note_hint": "Μπορείτε να @αναφέρετε άλλα άτομα ή #hashtags…",
"onboarding.profile.save_and_continue": "Αποθήκευση και συνέχεια",
"onboarding.profile.title": "Ρύθμιση προφίλ",
"onboarding.profile.upload_avatar": "Μεταφόρτωση εικόνας προφίλ",
"onboarding.profile.upload_header": "Μεταφόρτωση κεφαλίδας προφίλ",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!\nΕνημερώστε άλλα άτομα πώς μπορούν να σας βρουν στο Mastodon!",
"onboarding.share.message": "Με λένε {username} στο #Mastodon! Έλα να με ακολουθήσεις στο {url}",
"onboarding.share.next_steps": "Πιθανά επόμενα βήματα:",
"onboarding.share.title": "Κοινοποίηση του προφίλ σου",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "You've made it!\nΤα καταφέρατε!",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.publish_status.title": "Κάντε την πρώτη σας δημοσίευση",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"onboarding.tips.2fa": "<strong>Το ήξερες;</strong> Μπορείς να ασφαλίσεις το λογαριασμό σου ρυθμίζοντας ταυτότητα δύο παραγόντων στις ρυθμίσεις του λογαριασμού σου. Λειτουργεί με οποιαδήποτε εφαρμογή TOTP της επιλογής σας, δεν απαιτείται αριθμός τηλεφώνου!",
"onboarding.tips.accounts_from_other_servers": "<strong>Το ήξερες;</strong> Από τη στιγμή που το Mastodon είναι αποκεντρωμένο, κάποια προφίλ που συναντάς θα φιλοξενούνται σε διακομιστές διαφορετικούς από τον δικό σου. Και παρόλα αυτά μπορείς να αλληλεπιδράσεις μαζί τους απρόσκοπτα! Ο διακομιστής τους είναι στο δεύτερο μισό του ονόματος χρήστη!",
"onboarding.tips.migration": "<strong>Το ήξερες;</strong> Αν αισθάνεσαι ότι το {domain} δεν είναι η κατάλληλη επιλογή διακομιστή για σένα στο μέλλον, μπορείς να μετακινηθείς σε άλλο διακομιστή Mastodon χωρίς να χάσεις τους ακόλουθούς σου. Μπορείς να κάνεις ακόμα και τον δικό σου διακομιστή!",
"onboarding.tips.verification": "<strong>Το ήξερες;</strong> Μπορείς να επαληθεύσεις τον λογαριασμό σου βάζοντας έναν σύνδεσμο του προφίλ σου στο Mastodon στην ιστοσελίδα σου και να προσθέσεις την ιστοσελίδα στο προφίλ σου. Χωρίς έξοδα ή έγγραφα!",
"password_confirmation.exceeds_maxlength": "Η επιβεβαίωση κωδικού πρόσβασης υπερβαίνει το μέγιστο μήκος κωδικού πρόσβασης",
"password_confirmation.mismatching": "Η επιβεβαίωση του κωδικού πρόσβασης δε συμπίπτει",
"picture_in_picture.restore": "Βάλε το πίσω",

View file

@ -129,7 +129,6 @@
"bundle_column_error.routing.body": "The requested page could not be found. Are you sure the URL in the address bar is correct?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"closed_registrations.other_server_instructions": "Since Mastodon is decentralised, you can create an account on another server and still interact with this one.",
"closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
@ -140,13 +139,16 @@
"column.blocks": "Blocked users",
"column.bookmarks": "Bookmarks",
"column.community": "Local timeline",
"column.create_list": "Create list",
"column.direct": "Private mentions",
"column.directory": "Browse profiles",
"column.domain_blocks": "Blocked domains",
"column.edit_list": "Edit list",
"column.favourites": "Favourites",
"column.firehose": "Live feeds",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.list_members": "Manage list members",
"column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
@ -159,6 +161,7 @@
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_search.cancel": "Cancel",
"column_subheading.settings": "Settings",
"community.column_settings.local_only": "Local only",
"community.column_settings.media_only": "Media Only",
@ -232,10 +235,6 @@
"disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.",
"dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.",
"dismissable_banner.dismiss": "Dismiss",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralised network right now.",
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralised network right now.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
"domain_block_modal.block": "Block server",
"domain_block_modal.block_account_instead": "Block @{name} instead",
"domain_block_modal.they_can_interact_with_old_posts": "People from this server can interact with your old posts.",
@ -464,11 +463,32 @@
"link_preview.author": "By {name}",
"link_preview.more_from_author": "More from {name}",
"link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}",
"lists.add_member": "Add",
"lists.add_to_list": "Add to list",
"lists.add_to_lists": "Add {name} to lists",
"lists.create": "Create",
"lists.create_a_list_to_organize": "Create a new list to organise your Home feed",
"lists.create_list": "Create list",
"lists.delete": "Delete list",
"lists.done": "Done",
"lists.edit": "Edit list",
"lists.exclusive": "Hide members in Home",
"lists.exclusive_hint": "If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.",
"lists.find_users_to_add": "Find users to add",
"lists.list_members": "List members",
"lists.list_members_count": "{count, plural, one {# member} other {# members}}",
"lists.list_name": "List name",
"lists.new_list_name": "New list name",
"lists.no_lists_yet": "No lists yet.",
"lists.no_members_yet": "No members yet.",
"lists.no_results_found": "No results found.",
"lists.remove_member": "Remove",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.save": "Save",
"lists.search_placeholder": "Search people you follow",
"lists.show_replies_to": "Include replies from list members to",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading…",
"media_gallery.hide": "Hide",
@ -518,6 +538,7 @@
"notification.admin.sign_up": "{name} signed up",
"notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up",
"notification.annual_report.message": "Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!",
"notification.annual_report.view": "View #Wrapstodon",
"notification.favourite": "{name} favourited your post",
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favourited your post",
"notification.follow": "{name} followed you",
@ -624,44 +645,21 @@
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"onboarding.action.back": "Take me back",
"onboarding.actions.back": "Take me back",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Take me to my home feed",
"onboarding.compose.template": "Hello #Mastodon!",
"onboarding.follows.back": "Back",
"onboarding.follows.done": "Done",
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Personalize your home feed",
"onboarding.follows.search": "Search",
"onboarding.follows.title": "Follow people to get started",
"onboarding.profile.discoverable": "Make my profile discoverable",
"onboarding.profile.discoverable_hint": "When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.",
"onboarding.profile.display_name": "Display name",
"onboarding.profile.display_name_hint": "Your full name or your fun name…",
"onboarding.profile.lead": "You can always complete this later in the settings, where even more customisation options are available.",
"onboarding.profile.note": "Bio",
"onboarding.profile.note_hint": "You can @mention other people or #hashtags…",
"onboarding.profile.save_and_continue": "Save and continue",
"onboarding.profile.title": "Profile setup",
"onboarding.profile.upload_avatar": "Upload profile picture",
"onboarding.profile.upload_header": "Upload profile header",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
"onboarding.share.next_steps": "Possible next steps:",
"onboarding.share.title": "Share your profile",
"onboarding.start.lead": "You're now part of Mastodon, a unique, decentralized social media platform where you—not an algorithm—curate your own experience. Let's get you started on this new social frontier:",
"onboarding.start.skip": "Don't need help getting started?",
"onboarding.start.title": "You've made it!",
"onboarding.steps.follow_people.body": "Following interesting people is what Mastodon is all about.",
"onboarding.steps.follow_people.title": "Personalize your home feed",
"onboarding.steps.publish_status.body": "Say hello to the world with text, photos, videos, or polls {emoji}",
"onboarding.steps.publish_status.title": "Make your first post",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customise your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your Mastodon profile",
"onboarding.tips.2fa": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
"onboarding.tips.accounts_from_other_servers": "<strong>Did you know?</strong> Since Mastodon is decentralised, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!",
"onboarding.tips.migration": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
"onboarding.tips.verification": "<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!",
"password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length",
"password_confirmation.mismatching": "Password confirmation does not match",
"picture_in_picture.restore": "Put it back",

View file

@ -103,7 +103,7 @@
"annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag",
"annual_report.summary.most_used_hashtag.none": "None",
"annual_report.summary.new_posts.new_posts": "new posts",
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>",
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",
"annual_report.summary.thanks": "Thanks for being part of Mastodon!",
"attachments_list.unprocessed": "(unprocessed)",
@ -129,7 +129,7 @@
"bundle_column_error.routing.body": "The requested page could not be found. Are you sure the URL in the address bar is correct?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.message": "Something went wrong while loading this screen.",
"bundle_modal_error.retry": "Try again",
"closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.",
"closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
@ -162,6 +162,7 @@
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_search.cancel": "Cancel",
"column_subheading.settings": "Settings",
"community.column_settings.local_only": "Local only",
"community.column_settings.media_only": "Media Only",
@ -204,6 +205,9 @@
"confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.edit.title": "Overwrite post?",
"confirmations.follow_to_list.confirm": "Follow and add to list",
"confirmations.follow_to_list.message": "You need to be following {name} to add them to a list.",
"confirmations.follow_to_list.title": "Follow user?",
"confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.logout.title": "Log out?",
@ -235,10 +239,10 @@
"disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.",
"dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.",
"dismissable_banner.dismiss": "Dismiss",
"dismissable_banner.explore_links": "These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.",
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.",
"dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
"dismissable_banner.explore_links": "These news stories are being shared the most on the fediverse today. Newer news stories posted by more different people are ranked higher.",
"dismissable_banner.explore_statuses": "These posts from across the fediverse are gaining traction today. Newer posts with more boosts and favorites are ranked higher.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction on the fediverse today. Hashtags that are used by more different people are ranked higher.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the fediverse that people on {domain} follow.",
"domain_block_modal.block": "Block server",
"domain_block_modal.block_account_instead": "Block @{name} instead",
"domain_block_modal.they_can_interact_with_old_posts": "People from this server can interact with your old posts.",
@ -362,6 +366,7 @@
"footer.status": "Status",
"generic.saved": "Saved",
"getting_started.heading": "Getting started",
"hashtag.admin_moderation": "Open moderation interface for #{name}",
"hashtag.column_header.tag_mode.all": "and {additional}",
"hashtag.column_header.tag_mode.any": "or {additional}",
"hashtag.column_header.tag_mode.none": "without {additional}",
@ -491,7 +496,7 @@
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.save": "Save",
"lists.search_placeholder": "Search people you follow",
"lists.search": "Search",
"lists.show_replies_to": "Include replies from list members to",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading…",
@ -649,44 +654,21 @@
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"onboarding.action.back": "Take me back",
"onboarding.actions.back": "Take me back",
"onboarding.actions.go_to_explore": "Take me to trending",
"onboarding.actions.go_to_home": "Take me to my home feed",
"onboarding.compose.template": "Hello #Mastodon!",
"onboarding.follows.back": "Back",
"onboarding.follows.done": "Done",
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:",
"onboarding.follows.title": "Personalize your home feed",
"onboarding.follows.search": "Search",
"onboarding.follows.title": "Follow people to get started",
"onboarding.profile.discoverable": "Make my profile discoverable",
"onboarding.profile.discoverable_hint": "When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.",
"onboarding.profile.display_name": "Display name",
"onboarding.profile.display_name_hint": "Your full name or your fun name…",
"onboarding.profile.lead": "You can always complete this later in the settings, where even more customization options are available.",
"onboarding.profile.note": "Bio",
"onboarding.profile.note_hint": "You can @mention other people or #hashtags…",
"onboarding.profile.save_and_continue": "Save and continue",
"onboarding.profile.title": "Profile setup",
"onboarding.profile.upload_avatar": "Upload profile picture",
"onboarding.profile.upload_header": "Upload profile header",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
"onboarding.share.next_steps": "Possible next steps:",
"onboarding.share.title": "Share your profile",
"onboarding.start.lead": "You're now part of Mastodon, a unique, decentralized social media platform where you—not an algorithm—curate your own experience. Let's get you started on this new social frontier:",
"onboarding.start.skip": "Don't need help getting started?",
"onboarding.start.title": "You've made it!",
"onboarding.steps.follow_people.body": "Following interesting people is what Mastodon is all about.",
"onboarding.steps.follow_people.title": "Personalize your home feed",
"onboarding.steps.publish_status.body": "Say hello to the world with text, photos, videos, or polls {emoji}",
"onboarding.steps.publish_status.title": "Make your first post",
"onboarding.steps.setup_profile.body": "Boost your interactions by having a comprehensive profile.",
"onboarding.steps.setup_profile.title": "Personalize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon",
"onboarding.steps.share_profile.title": "Share your Mastodon profile",
"onboarding.tips.2fa": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
"onboarding.tips.accounts_from_other_servers": "<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!",
"onboarding.tips.migration": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
"onboarding.tips.verification": "<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!",
"password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length",
"password_confirmation.mismatching": "Password confirmation does not match",
"picture_in_picture.restore": "Put it back",

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