Merge branch 'main' into translate-toots

This commit is contained in:
Thomas Steiner 2025-08-21 18:25:18 +02:00 committed by GitHub
commit 7217edfd7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 1434 additions and 384 deletions

View File

@ -1,17 +1,21 @@
---
Metrics/AbcSize:
Exclude:
- lib/mastodon/cli/*.rb
Enabled: false
Metrics/BlockLength:
Enabled: false
Metrics/BlockNesting:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CollectionLiteralLength:
Enabled: false
Metrics/CyclomaticComplexity:
Exclude:
- lib/mastodon/cli/*.rb
Enabled: false
Metrics/MethodLength:
Enabled: false
@ -20,4 +24,7 @@ Metrics/ModuleLength:
Enabled: false
Metrics/ParameterLists:
CountKeywordArgs: false
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false

View File

@ -6,23 +6,6 @@
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 82
# Configuration parameters: CountBlocks, CountModifierForms, Max.
Metrics/BlockNesting:
Exclude:
- 'lib/tasks/mastodon.rake'
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 25
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 27
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedVars, DefaultToNil.
Style/FetchEnvVar:

View File

@ -806,10 +806,10 @@ GEM
ruby-saml (1.18.1)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.4)
ruby-vips (2.2.5)
ffi (~> 1.12)
logger
rubyzip (3.0.1)
rubyzip (3.0.2)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.4.0)

View File

@ -102,6 +102,16 @@ module ApplicationHelper
policy(record).public_send(:"#{action}?")
end
def conditional_link_to(condition, name, options = {}, html_options = {}, &block)
if condition && !current_page?(block_given? ? name : options)
link_to(name, options, html_options, &block)
elsif block_given?
content_tag(:span, options, html_options, &block)
else
content_tag(:span, name, html_options)
end
end
def material_symbol(icon, attributes = {})
safe_join(
[

View File

@ -84,6 +84,7 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
uploadQuote: { id: 'upload_error.quote', defaultMessage: 'File upload not allowed with quotes.' },
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
@ -146,7 +147,7 @@ export function resetCompose() {
};
}
export const focusCompose = (defaultText) => (dispatch, getState) => {
export const focusCompose = (defaultText = '') => (dispatch, getState) => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
@ -303,6 +304,11 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
return function (dispatch, getState) {
// Exit if there's a quote.
if (getState().compose.get('quoted_status_id')) {
dispatch(showAlert({ message: messages.uploadQuote }));
return;
}
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);

View File

@ -1,3 +1,5 @@
import { defineMessages } from 'react-intl';
import { createAction } from '@reduxjs/toolkit';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
@ -12,7 +14,27 @@ import {
import type { ApiQuotePolicy } from '../api_types/quotes';
import type { Status } from '../models/status';
import { ensureComposeIsVisible } from './compose';
import { showAlert } from './alerts';
import { focusCompose } from './compose';
const messages = defineMessages({
quoteErrorUpload: {
id: 'quote_error.upload',
defaultMessage: 'Quoting is not allowed with media attachments.',
},
quoteErrorPoll: {
id: 'quote_error.poll',
defaultMessage: 'Quoting is not allowed with polls.',
},
quoteErrorQuote: {
id: 'quote_error.quote',
defaultMessage: 'Only one quote at a time is allowed.',
},
quoteErrorUnauthorized: {
id: 'quote_error.unauthorized',
defaultMessage: 'You are not authorized to quote this post.',
},
});
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
unattached?: boolean;
@ -78,14 +100,43 @@ export const changeUploadCompose = createDataLoadingThunk(
},
);
export const quoteComposeByStatus = createAppThunk(
export const quoteCompose = createAppThunk(
'compose/quoteComposeStatus',
(status: Status, { getState }) => {
ensureComposeIsVisible(getState);
(status: Status, { dispatch }) => {
dispatch(focusCompose());
return status;
},
);
export const quoteComposeByStatus = createAppThunk(
(status: Status, { dispatch, getState }) => {
const composeState = getState().compose;
const mediaAttachments = composeState.get('media_attachments');
if (composeState.get('poll')) {
dispatch(showAlert({ message: messages.quoteErrorPoll }));
} else if (
composeState.get('is_uploading') ||
(mediaAttachments &&
typeof mediaAttachments !== 'string' &&
typeof mediaAttachments !== 'number' &&
typeof mediaAttachments !== 'boolean' &&
mediaAttachments.size !== 0)
) {
dispatch(showAlert({ message: messages.quoteErrorUpload }));
} else if (composeState.get('quoted_status_id')) {
dispatch(showAlert({ message: messages.quoteErrorQuote }));
} else if (
status.getIn(['quote_approval', 'current_user']) !== 'automatic' &&
status.getIn(['quote_approval', 'current_user']) !== 'manual'
) {
dispatch(showAlert({ message: messages.quoteErrorUnauthorized }));
} else {
dispatch(quoteCompose(status));
}
},
);
export const quoteComposeById = createAppThunk(
(statusId: string, { dispatch, getState }) => {
const status = getState().statuses.get(statusId);
@ -97,6 +148,6 @@ export const quoteComposeById = createAppThunk(
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
export const setQuotePolicy = createAction<ApiQuotePolicy>(
export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(
'compose/setQuotePolicy',
);

View File

@ -2,6 +2,7 @@ import type { ApiStatusJSON } from './statuses';
export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized';
export type ApiQuotePolicy = 'public' | 'followers' | 'nobody' | 'unknown';
export type ApiUserQuotePolicy = 'automatic' | 'manual' | 'denied' | 'unknown';
interface ApiQuoteEmptyJSON {
state: Exclude<ApiQuoteState, 'accepted'>;
@ -25,7 +26,7 @@ export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON;
export interface ApiQuotePolicyJSON {
automatic: ApiQuotePolicy[];
manual: ApiQuotePolicy[];
current_user: ApiQuotePolicy;
current_user: ApiUserQuotePolicy;
}
export function isQuotePolicy(policy: string): policy is ApiQuotePolicy {

View File

@ -133,3 +133,9 @@ export interface ApiStatusSourceJSON {
text: string;
spoiler_text: string;
}
export function isStatusVisibility(
visibility: string,
): visibility is StatusVisibility {
return ['public', 'unlisted', 'private', 'direct'].includes(visibility);
}

View File

@ -41,13 +41,16 @@ import { IconButton } from './icon_button';
let id = 0;
type RenderItemFn<Item = MenuItem> = (
export interface RenderItemFnHandlers {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
}
export type RenderItemFn<Item = MenuItem> = (
item: Item,
index: number,
handlers: {
onClick: (e: React.MouseEvent) => void;
onKeyUp: (e: React.KeyboardEvent) => void;
},
handlers: RenderItemFnHandlers,
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void,
) => React.ReactNode;
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
@ -173,7 +176,7 @@ export const DropdownMenu = <Item = MenuItem,>({
onItemClick(item, i);
} else if (isActionItem(item)) {
e.preventDefault();
item.action();
item.action(e);
}
},
[onClose, onItemClick, items],
@ -277,10 +280,15 @@ export const DropdownMenu = <Item = MenuItem,>({
})}
>
{items.map((option, i) =>
renderItemMethod(option, i, {
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
}),
renderItemMethod(
option,
i,
{
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
},
i === 0 ? handleFocusedItemRef : undefined,
),
)}
</ul>
)}
@ -307,7 +315,9 @@ interface DropdownProps<Item = MenuItem> {
forceDropdown?: boolean;
renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>;
onOpen?: () => void;
onOpen?: // Must use a union type for the full function as a union with void is not allowed.
| ((event: React.MouseEvent | React.KeyboardEvent) => void)
| ((event: React.MouseEvent | React.KeyboardEvent) => boolean);
onItemClick?: ItemClickFn<Item>;
}
@ -376,7 +386,7 @@ export const Dropdown = <Item = MenuItem,>({
onItemClick(item, i);
} else if (isActionItem(item)) {
e.preventDefault();
item.action();
item.action(e);
}
},
[handleClose, onItemClick, items],
@ -389,7 +399,10 @@ export const Dropdown = <Item = MenuItem,>({
if (open) {
handleClose();
} else {
onOpen?.();
const allow = onOpen?.(e);
if (allow === false) {
return;
}
if (prefetchAccountId) {
dispatch(fetchRelationships([prefetchAccountId]));

View File

@ -110,6 +110,7 @@ class Status extends ImmutablePureComponent {
onToggleCollapsed: PropTypes.func,
onTranslate: PropTypes.func,
onInteractionModal: PropTypes.func,
onQuoteCancel: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
@ -583,7 +584,7 @@ class Status extends ImmutablePureComponent {
<DisplayName account={status.get('account')} />
</Link>
{this.props.contextType === 'compose' && isQuotedPost && (
{isQuotedPost && !!this.props.onQuoteCancel && (
<IconButton
onClick={this.handleQuoteCancel}
className='status__quote-cancel'

View File

@ -0,0 +1,89 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
import { statusFactoryState } from '@/testing/factories';
import { LegacyReblogButton, StatusReblogButton } from './reblog_button';
interface StoryProps {
visibility: StatusVisibility;
quoteAllowed: boolean;
alreadyBoosted: boolean;
reblogCount: number;
}
const meta = {
title: 'Components/Status/ReblogButton',
args: {
visibility: 'public',
quoteAllowed: true,
alreadyBoosted: false,
reblogCount: 0,
},
argTypes: {
visibility: {
name: 'Visibility',
control: { type: 'select' },
options: ['public', 'unlisted', 'private', 'direct'],
},
reblogCount: {
name: 'Boost Count',
description: 'More than 0 will show the counter',
},
quoteAllowed: {
name: 'Quotes allowed',
},
alreadyBoosted: {
name: 'Already boosted',
},
},
render: (args) => (
<StatusReblogButton
status={argsToStatus(args)}
counters={args.reblogCount > 0}
/>
),
} satisfies Meta<StoryProps>;
export default meta;
function argsToStatus({
reblogCount,
visibility,
quoteAllowed,
alreadyBoosted,
}: StoryProps) {
return statusFactoryState({
reblogs_count: reblogCount,
visibility,
reblogged: alreadyBoosted,
quote_approval: {
automatic: [],
manual: [],
current_user: quoteAllowed ? 'automatic' : 'denied',
},
});
}
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Mine: Story = {
parameters: {
state: {
meta: {
me: '1',
},
},
},
};
export const Legacy: Story = {
render: (args) => (
<LegacyReblogButton
status={argsToStatus(args)}
counters={args.reblogCount > 0}
/>
),
};

View File

@ -0,0 +1,373 @@
import { useCallback, useMemo } from 'react';
import type {
FC,
KeyboardEvent,
MouseEvent,
MouseEventHandler,
SVGProps,
} from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { quoteComposeById } from '@/mastodon/actions/compose_typed';
import { toggleReblog } from '@/mastodon/actions/interactions';
import { openModal } from '@/mastodon/actions/modal';
import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu';
import type { Status, StatusVisibility } from '@/mastodon/models/status';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from '@/mastodon/store';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
import FormatQuote from '@/material-icons/400-24px/format_quote.svg?react';
import FormatQuoteOff from '@/material-icons/400-24px/format_quote_off.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
import { Dropdown } from '../dropdown_menu';
import { Icon } from '../icon';
import { IconButton } from '../icon_button';
const messages = defineMessages({
all_disabled: {
id: 'status.all_disabled',
defaultMessage: 'Boosts and quotes are disabled',
},
quote: { id: 'status.quote', defaultMessage: 'Quote' },
quote_cannot: {
id: 'status.cannot_quote',
defaultMessage: 'Author has disabled quoting on this post',
},
quote_private: {
id: 'status.quote_private',
defaultMessage: 'Private posts cannot be quoted',
},
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_cancel: {
id: 'status.cancel_reblog_private',
defaultMessage: 'Unboost',
},
reblog_private: {
id: 'status.reblog_private',
defaultMessage: 'Boost with original visibility',
},
reblog_cannot: {
id: 'status.cannot_reblog',
defaultMessage: 'This post cannot be boosted',
},
});
interface ReblogButtonProps {
status: Status;
counters?: boolean;
}
export const StatusReblogButton: FC<ReblogButtonProps> = ({
status,
counters,
}) => {
const intl = useIntl();
const statusState = useAppSelector((state) =>
selectStatusState(state, status),
);
const { isLoggedIn, isReblogged, isReblogAllowed, isQuoteAllowed } =
statusState;
const { iconComponent } = useMemo(
() => reblogIconText(statusState),
[statusState],
);
const disabled = !isQuoteAllowed && !isReblogAllowed;
const dispatch = useAppDispatch();
const statusId = status.get('id') as string;
const items: ActionMenuItem[] = useMemo(
() => [
{
text: 'reblog',
action: (event) => {
if (isLoggedIn) {
dispatch(toggleReblog(statusId, event.shiftKey));
}
},
},
{
text: 'quote',
action: () => {
if (isLoggedIn) {
dispatch(quoteComposeById(statusId));
}
},
},
],
[dispatch, isLoggedIn, statusId],
);
const handleDropdownOpen = useCallback(
(event: MouseEvent | KeyboardEvent) => {
if (!isLoggedIn) {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
} else if (event.shiftKey) {
dispatch(toggleReblog(status.get('id'), true));
return false;
}
return true;
},
[dispatch, isLoggedIn, status],
);
const renderMenuItem: RenderItemFn<ActionMenuItem> = useCallback(
(item, index, handlers, focusRefCallback) => (
<ReblogMenuItem
status={status}
index={index}
item={item}
handlers={handlers}
key={`${item.text}-${index}`}
focusRefCallback={focusRefCallback}
/>
),
[status],
);
return (
<Dropdown
items={items}
renderItem={renderMenuItem}
onOpen={handleDropdownOpen}
disabled={disabled}
>
<IconButton
title={intl.formatMessage(
!disabled ? messages.reblog : messages.all_disabled,
)}
icon='retweet'
iconComponent={iconComponent}
counter={counters ? (status.get('reblogs_count') as number) : undefined}
active={isReblogged}
/>
</Dropdown>
);
};
interface ReblogMenuItemProps {
status: Status;
item: ActionMenuItem;
index: number;
handlers: RenderItemFnHandlers;
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void;
}
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
status,
index,
item: { text },
handlers,
focusRefCallback,
}) => {
const intl = useIntl();
const statusState = useAppSelector((state) =>
selectStatusState(state, status),
);
const { title, meta, iconComponent, disabled } = useMemo(
() =>
text === 'quote'
? quoteIconText(statusState)
: reblogIconText(statusState),
[statusState, text],
);
const active = useMemo(
() => text === 'reblog' && !!status.get('reblogged'),
[status, text],
);
return (
<li
className={classNames('dropdown-menu__item reblog-button__item', {
disabled,
active,
})}
key={`${text}-${index}`}
>
<button
{...handlers}
title={intl.formatMessage(title)}
ref={focusRefCallback}
disabled={disabled}
data-index={index}
>
<Icon
id={text === 'quote' ? 'quote' : 'retweet'}
icon={iconComponent}
/>
<div>
{intl.formatMessage(title)}
{meta && (
<span className='reblog-button__meta'>
{intl.formatMessage(meta)}
</span>
)}
</div>
</button>
</li>
);
};
// Legacy helpers
// Switch between the legacy and new reblog button based on feature flag.
export const ReblogButton: FC<ReblogButtonProps> = (props) => {
if (isFeatureEnabled('outgoing_quotes')) {
return <StatusReblogButton {...props} />;
}
return <LegacyReblogButton {...props} />;
};
export const LegacyReblogButton: FC<ReblogButtonProps> = ({
status,
counters,
}) => {
const intl = useIntl();
const statusState = useAppSelector((state) =>
selectStatusState(state, status),
);
const { title, meta, iconComponent, disabled } = useMemo(
() => reblogIconText(statusState),
[statusState],
);
const dispatch = useAppDispatch();
const handleClick: MouseEventHandler = useCallback(
(event) => {
if (statusState.isLoggedIn) {
dispatch(toggleReblog(status.get('id') as string, event.shiftKey));
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
},
[dispatch, status, statusState.isLoggedIn],
);
return (
<IconButton
disabled={disabled}
active={!!status.get('reblogged')}
title={intl.formatMessage(meta ?? title)}
icon='retweet'
iconComponent={iconComponent}
onClick={!disabled ? handleClick : undefined}
counter={counters ? (status.get('reblogs_count') as number) : undefined}
/>
);
};
// Helpers for copy and state for status.
const selectStatusState = createAppSelector(
[
(state) => state.meta.get('me') as string | undefined,
(_, status: Status) => status,
],
(userId, status) => {
const isPublic = ['public', 'unlisted'].includes(
status.get('visibility') as StatusVisibility,
);
const isMineAndPrivate =
userId === status.getIn(['account', 'id']) &&
status.get('visibility') === 'private';
return {
isLoggedIn: !!userId,
isPublic,
isMine: userId === status.getIn(['account', 'id']),
isPrivateReblog:
userId === status.getIn(['account', 'id']) &&
status.get('visibility') === 'private',
isReblogged: !!status.get('reblogged'),
isReblogAllowed: isPublic || isMineAndPrivate,
isQuoteAllowed:
status.getIn(['quote_approval', 'current_user']) === 'automatic' &&
(isPublic || isMineAndPrivate),
};
},
);
type StatusState = ReturnType<typeof selectStatusState>;
interface IconText {
title: MessageDescriptor;
meta?: MessageDescriptor;
iconComponent: FC<SVGProps<SVGSVGElement>>;
disabled?: boolean;
}
function reblogIconText({
isPublic,
isPrivateReblog,
isReblogged,
}: StatusState): IconText {
if (isReblogged) {
return {
title: messages.reblog_cancel,
iconComponent: isPublic ? RepeatActiveIcon : RepeatPrivateActiveIcon,
};
}
const iconText: IconText = {
title: messages.reblog,
iconComponent: RepeatIcon,
};
if (isPrivateReblog) {
iconText.meta = messages.reblog_private;
iconText.iconComponent = RepeatPrivateIcon;
} else if (!isPublic) {
iconText.meta = messages.reblog_cannot;
iconText.iconComponent = RepeatDisabledIcon;
iconText.disabled = true;
}
return iconText;
}
function quoteIconText({
isMine,
isQuoteAllowed,
isPublic,
}: StatusState): IconText {
const iconText: IconText = {
title: messages.quote,
iconComponent: FormatQuote,
};
if (!isQuoteAllowed || (!isPublic && !isMine)) {
iconText.meta = !isQuoteAllowed
? messages.quote_cannot
: messages.quote_private;
iconText.iconComponent = FormatQuoteOff;
iconText.disabled = true;
}
return iconText;
}

View File

@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
@ -12,15 +11,10 @@ import { connect } from 'react-redux';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -30,6 +24,7 @@ import { me } from '../initial_state';
import { IconButton } from './icon_button';
import { isFeatureEnabled } from '../utils/environment';
import { ReblogButton } from './status/reblog_button';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -43,10 +38,6 @@ const messages = defineMessages({
share: { id: 'status.share', defaultMessage: 'Share' },
more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
removeFavourite: { id: 'status.remove_favourite', defaultMessage: 'Remove from favorites' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
@ -85,10 +76,9 @@ class StatusActionBar extends ImmutablePureComponent {
identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.record,
quotedAccountId: ImmutablePropTypes.string,
quotedAccountId: PropTypes.string,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onRevokeQuote: PropTypes.func,
onQuotePolicyChange: PropTypes.func,
@ -152,16 +142,6 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
handleReblogClick = e => {
const { signedIn } = this.props.identity;
if (signedIn) {
this.props.onReblog(this.props.status, e);
} else {
this.props.onInteractionModal('reblog', this.props.status);
}
};
handleBookmarkClick = () => {
this.props.onBookmark(this.props.status);
};
@ -377,25 +357,6 @@ class StatusActionBar extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll);
}
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark);
const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite);
const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
@ -406,7 +367,7 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<ReblogButton status={status} counters={withCounters} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={favouriteTitle} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />

View File

@ -63,12 +63,21 @@ type GetStatusSelector = (
props: { id?: string | null; contextType?: string },
) => Status | null;
export const QuotedStatus: React.FC<{
interface QuotedStatusProps {
quote: QuoteMap;
contextType?: string;
variant?: 'full' | 'link';
nestingLevel?: number;
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
onQuoteCancel?: () => void; // Used for composer.
}
export const QuotedStatus: React.FC<QuotedStatusProps> = ({
quote,
contextType,
nestingLevel = 1,
variant = 'full',
onQuoteCancel,
}) => {
const dispatch = useAppDispatch();
const quotedStatusId = quote.get('quoted_status');
const quoteState = quote.get('state');
@ -160,6 +169,7 @@ export const QuotedStatus: React.FC<{
id={quotedStatusId}
contextType={contextType}
avatarSize={32}
onQuoteCancel={onQuoteCancel}
>
{canRenderChildQuote && (
<QuotedStatus

View File

@ -41,10 +41,10 @@ import {
translateStatus,
undoStatusTranslation,
} from '../actions/statuses';
import { setStatusQuotePolicy } from '../actions/statuses_typed';
import Status from '../components/status';
import { deleteModal } from '../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
import { quoteComposeCancel } from '../actions/compose_typed';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
@ -112,18 +112,18 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
}
},
onQuoteCancel() {
if (contextType === 'compose') {
dispatch(quoteComposeCancel());
}
},
onRevokeQuote (status) {
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
},
onQuotePolicyChange(status) {
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
const statusId = status.get('id');
const handleChange = (_, quotePolicy) => {
dispatch(
setStatusQuotePolicy({ policy: quotePolicy, statusId }),
);
}
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
},
onEdit (status) {

View File

@ -15,10 +15,8 @@ import { missingAltTextModal } from 'mastodon/initial_state';
import AutosuggestInput from 'mastodon/components/autosuggest_input';
import AutosuggestTextarea from 'mastodon/components/autosuggest_textarea';
import { Button } from 'mastodon/components/button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollButtonContainer from '../containers/poll_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import UploadButtonContainer from '../containers/upload_button_container';
import { countableText } from '../util/counter';
@ -32,6 +30,7 @@ import { ReplyIndicator } from './reply_indicator';
import { UploadForm } from './upload_form';
import { Warning } from './warning';
import { ComposeQuotedStatus } from './quoted_post';
import { VisibilityButton } from './visibility_button';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@ -260,7 +259,7 @@ class ComposeForm extends ImmutablePureComponent {
<EditIndicator />
<div className='compose-form__dropdowns'>
<PrivacyDropdownContainer disabled={this.props.isEditing} />
<VisibilityButton disabled={this.props.isEditing} />
<LanguageDropdown />
</div>

View File

@ -1,15 +1,17 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import type { FC } from 'react';
import { Map } from 'immutable';
import { quoteComposeCancel } from '@/mastodon/actions/compose_typed';
import { QuotedStatus } from '@/mastodon/components/status_quoted';
import { useAppSelector } from '@/mastodon/store';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
export const ComposeQuotedStatus: FC = () => {
const quotedStatusId = useAppSelector(
(state) => state.compose.get('quoted_status_id') as string | null,
);
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
const quote = useMemo(
() =>
quotedStatusId
@ -20,8 +22,17 @@ export const ComposeQuotedStatus: FC = () => {
: null,
[quotedStatusId],
);
const dispatch = useAppDispatch();
const handleQuoteCancel = useCallback(() => {
dispatch(quoteComposeCancel());
}, [dispatch]);
if (!quote) {
return null;
}
return <QuotedStatus quote={quote} contextType='compose' />;
return (
<QuotedStatus
quote={quote}
onQuoteCancel={!isEditing ? handleQuoteCancel : undefined}
/>
);
};

View File

@ -0,0 +1,148 @@
import { useCallback, useMemo } from 'react';
import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { changeComposeVisibility } from '@/mastodon/actions/compose';
import { setComposeQuotePolicy } from '@/mastodon/actions/compose_typed';
import { openModal } from '@/mastodon/actions/modal';
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
import { Icon } from '@/mastodon/components/icon';
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import type { VisibilityModalCallback } from '../../ui/components/visibility_modal';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import { messages as privacyMessages } from './privacy_dropdown';
const messages = defineMessages({
anyone_quote: {
id: 'privacy.quote.anyone',
defaultMessage: '{visibility}, anyone can quote',
},
limited_quote: {
id: 'privacy.quote.limited',
defaultMessage: '{visibility}, quotes limited',
},
disabled_quote: {
id: 'privacy.quote.disabled',
defaultMessage: '{visibility}, quotes disabled',
},
});
interface PrivacyDropdownProps {
disabled?: boolean;
}
export const VisibilityButton: FC<PrivacyDropdownProps> = (props) => {
if (!isFeatureEnabled('outgoing_quotes')) {
return <PrivacyDropdownContainer {...props} />;
}
return <PrivacyModalButton {...props} />;
};
const visibilityOptions = {
public: {
icon: 'globe',
iconComponent: PublicIcon,
value: 'public',
text: privacyMessages.public_short,
},
unlisted: {
icon: 'unlock',
iconComponent: QuietTimeIcon,
value: 'unlisted',
text: privacyMessages.unlisted_short,
},
private: {
icon: 'lock',
iconComponent: LockIcon,
value: 'private',
text: privacyMessages.private_short,
},
direct: {
icon: 'at',
iconComponent: AlternateEmailIcon,
value: 'direct',
text: privacyMessages.direct_short,
},
};
const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
const intl = useIntl();
const { visibility, quotePolicy } = useAppSelector((state) => ({
visibility: state.compose.get('privacy') as StatusVisibility,
quotePolicy: state.compose.get('quote_policy') as ApiQuotePolicy,
}));
const { icon, iconComponent } = useMemo(() => {
const option = visibilityOptions[visibility];
return { icon: option.icon, iconComponent: option.iconComponent };
}, [visibility]);
const text = useMemo(() => {
const visibilityText = intl.formatMessage(
visibilityOptions[visibility].text,
);
if (visibility === 'private' || visibility === 'direct') {
return visibilityText;
}
if (quotePolicy === 'nobody') {
return intl.formatMessage(messages.disabled_quote, {
visibility: visibilityText,
});
}
if (quotePolicy !== 'public') {
return intl.formatMessage(messages.limited_quote, {
visibility: visibilityText,
});
}
return intl.formatMessage(messages.anyone_quote, {
visibility: visibilityText,
});
}, [quotePolicy, visibility, intl]);
const dispatch = useAppDispatch();
const handleChange: VisibilityModalCallback = useCallback(
(newVisibility, newQuotePolicy) => {
if (newVisibility !== visibility) {
dispatch(changeComposeVisibility(newVisibility));
}
if (newQuotePolicy !== quotePolicy) {
dispatch(setComposeQuotePolicy(newQuotePolicy));
}
},
[dispatch, quotePolicy, visibility],
);
const handleOpen = useCallback(() => {
dispatch(
openModal({
modalType: 'COMPOSE_PRIVACY',
modalProps: { onChange: handleChange },
}),
);
}, [dispatch, handleChange]);
return (
<button
type='button'
title={intl.formatMessage(privacyMessages.change_privacy)}
onClick={handleOpen}
disabled={disabled}
className={classNames('dropdown-button')}
>
<Icon id={icon} icon={iconComponent} />
<span className='dropdown-button__label'>{text}</span>
</button>
);
};

View File

@ -3,23 +3,16 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
@ -27,6 +20,7 @@ import { IconButton } from '../../../components/icon_button';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../../../initial_state';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
import { ReblogButton } from '@/mastodon/components/status/reblog_button';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -35,10 +29,6 @@ const messages = defineMessages({
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
removeFavourite: { id: 'status.remove_favourite', defaultMessage: 'Remove from favorites' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
@ -313,31 +303,15 @@ class ActionBar extends PureComponent {
replyIconComponent = ReplyAllIcon;
}
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark);
const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite);
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'>
<ReblogButton status={status} />
</div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={favouriteTitle} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={bookmarkTitle} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>

View File

@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
@ -15,7 +15,6 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
import { Hotkeys } from 'mastodon/components/hotkeys';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import ScrollContainer from 'mastodon/containers/scroll_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
@ -57,6 +56,7 @@ import {
translateStatus,
undoStatusTranslation,
} from '../../actions/statuses';
import { setStatusQuotePolicy } from '../../actions/statuses_typed';
import ColumnHeader from '../../components/column_header';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import { StatusQuoteManager } from '../../components/status_quoted';
@ -266,8 +266,14 @@ class Status extends ImmutablePureComponent {
};
handleQuotePolicyChange = (status) => {
const statusId = status.get('id');
const { dispatch } = this.props;
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
const handleChange = (_, quotePolicy) => {
dispatch(
setStatusQuotePolicy({ policy: quotePolicy, statusId }),
);
}
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
};
handleEditClick = (status) => {

View File

@ -1,25 +1,31 @@
import { forwardRef, useCallback, useId, useMemo } from 'react';
import {
forwardRef,
useCallback,
useId,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { changeComposeVisibility } from '@/mastodon/actions/compose';
import { setStatusQuotePolicy } from '@/mastodon/actions/statuses_typed';
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import { isQuotePolicy } from '@/mastodon/api_types/quotes';
import { isStatusVisibility } from '@/mastodon/api_types/statuses';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
import { Dropdown } from '@/mastodon/components/dropdown';
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
import { IconButton } from '@/mastodon/components/icon_button';
import { messages as privacyMessages } from '@/mastodon/features/compose/components/privacy_dropdown';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from '@/mastodon/store';
import { createAppSelector, useAppSelector } from '@/mastodon/store';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import type { BaseConfirmationModalProps } from './confirmation_modals/confirmation_modal';
@ -43,13 +49,26 @@ const messages = defineMessages({
},
});
export type VisibilityModalCallback = (
visibility: StatusVisibility,
quotePolicy: ApiQuotePolicy,
) => void;
interface VisibilityModalProps extends BaseConfirmationModalProps {
statusId: string;
statusId?: string;
onChange: VisibilityModalCallback;
}
const selectStatusPolicy = createAppSelector(
[(state) => state.statuses, (_state, statusId: string) => statusId],
(statuses, statusId) => {
[
(state) => state.statuses,
(_state, statusId?: string) => statusId,
(state) => state.compose.get('quote_policy') as ApiQuotePolicy,
],
(statuses, statusId, composeQuotePolicy) => {
if (!statusId) {
return composeQuotePolicy;
}
const status = statuses.get(statusId);
if (!status) {
return 'public';
@ -77,24 +96,25 @@ const selectStatusPolicy = createAppSelector(
);
export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ onClose, statusId }, ref) => {
({ onClose, onChange, statusId }, ref) => {
const intl = useIntl();
const currentVisibility = useAppSelector(
(state) =>
(state.statuses.getIn([statusId, 'visibility'], 'public') as
| StatusVisibility
| undefined) ?? 'public',
const currentVisibility = useAppSelector((state) =>
statusId
? ((state.statuses.getIn([statusId, 'visibility'], 'public') as
| StatusVisibility
| undefined) ?? 'public')
: (state.compose.get('privacy') as StatusVisibility),
);
const currentQuotePolicy = useAppSelector((state) =>
selectStatusPolicy(state, statusId),
);
const [visibility, setVisibility] = useState(currentVisibility);
const [quotePolicy, setQuotePolicy] = useState(currentQuotePolicy);
const disableVisibility = !!statusId;
const disableQuotePolicy =
currentVisibility === 'private' || currentVisibility === 'direct';
const isSaving = useAppSelector(
(state) =>
state.statuses.getIn([statusId, 'isSavingQuotePolicy']) === true,
);
visibility === 'private' || visibility === 'direct';
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(
() => [
@ -102,21 +122,30 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
value: 'public',
text: intl.formatMessage(privacyMessages.public_short),
meta: intl.formatMessage(privacyMessages.public_long),
icon: 'globe',
iconComponent: PublicIcon,
},
{
value: 'unlisted',
text: intl.formatMessage(privacyMessages.unlisted_short),
meta: intl.formatMessage(privacyMessages.unlisted_long),
extra: intl.formatMessage(privacyMessages.unlisted_extra),
icon: 'unlock',
iconComponent: QuietTimeIcon,
},
{
value: 'private',
text: intl.formatMessage(privacyMessages.private_short),
meta: intl.formatMessage(privacyMessages.private_long),
icon: 'lock',
iconComponent: LockIcon,
},
{
value: 'direct',
text: intl.formatMessage(privacyMessages.direct_short),
meta: intl.formatMessage(privacyMessages.direct_long),
icon: 'at',
iconComponent: AlternateEmailIcon,
},
],
[intl],
@ -133,24 +162,27 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
[intl],
);
const dispatch = useAppDispatch();
const handleVisibilityChange = useCallback(
(value: string) => {
// Published statuses cannot change visibility.
if (statusId) {
return;
}
dispatch(changeComposeVisibility(value));
},
[dispatch, statusId],
);
const handleQuotePolicyChange = useCallback(
(value: string) => {
if (isQuotePolicy(value) && !disableQuotePolicy) {
void dispatch(setStatusQuotePolicy({ policy: value, statusId }));
}
},
[disableQuotePolicy, dispatch, statusId],
const handleVisibilityChange = useCallback((value: string) => {
if (isStatusVisibility(value)) {
setVisibility(value);
}
}, []);
const handleQuotePolicyChange = useCallback((value: string) => {
if (isQuotePolicy(value)) {
setQuotePolicy(value);
}
}, []);
// Save on close
useImperativeHandle(
ref,
() => ({
getCloseConfirmationMessage() {
onChange(visibility, quotePolicy);
return null;
},
}),
[onChange, quotePolicy, visibility],
);
const privacyDropdownId = useId();
@ -192,7 +224,7 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
<label
htmlFor={privacyDropdownId}
className={classNames('visibility-dropdown__label', {
disabled: isSaving || !!statusId,
disabled: disableVisibility,
})}
>
<FormattedMessage
@ -203,10 +235,10 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
<Dropdown
items={visibilityItems}
classPrefix='visibility-dropdown'
current={currentVisibility}
current={visibility}
onChange={handleVisibilityChange}
title={intl.formatMessage(privacyMessages.change_privacy)}
disabled={isSaving || !!statusId}
disabled={disableVisibility}
id={privacyDropdownId}
/>
{!!statusId && (
@ -222,7 +254,7 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
<label
htmlFor={quoteDropdownId}
className={classNames('visibility-dropdown__label', {
disabled: disableQuotePolicy || isSaving,
disabled: disableQuotePolicy,
})}
>
<FormattedMessage
@ -234,15 +266,12 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
items={quoteItems}
onChange={handleQuotePolicyChange}
classPrefix='visibility-dropdown'
current={currentQuotePolicy}
current={quotePolicy}
title={intl.formatMessage(messages.buttonTitle)}
disabled={disableQuotePolicy || isSaving}
disabled={disableQuotePolicy}
id={quoteDropdownId}
/>
<QuotePolicyHelper
policy={currentQuotePolicy}
visibility={currentVisibility}
/>
<QuotePolicyHelper policy={quotePolicy} visibility={visibility} />
</label>
</div>
</div>

View File

@ -836,22 +836,24 @@
"search_results.see_all": "Праглядзець усе",
"search_results.statuses": "Допісы",
"search_results.title": "Шукаць \"{q}\"",
"server_banner.about_active_users": "Людзі, якія карыстаюцца гэтым сервера на працягу апошніх 30 дзён (Штомесячна Актыўныя Карыстальнікі)",
"server_banner.about_active_users": "Людзі, якія карысталіся гэтым серверам на працягу апошніх 30 дзён (штомесячна актыўныя карыстальнікі)",
"server_banner.active_users": "актыўныя карыстальнікі",
"server_banner.administered_by": "Адміністратар:",
"server_banner.is_one_of_many": "{domain} - гэта адзін з многіх незалежных сервераў Mastodon, які Вы можаце выкарыстоўваць для ўдзелу ў федэральным сусвеце.",
"server_banner.server_stats": "Статыстыка сервера:",
"sign_in_banner.create_account": "Стварыць уліковы запіс",
"sign_in_banner.follow_anyone": "Падпісвайцеся на каго захочаце ва ўсім федэральным сусвеце і глядзіце ўсё ў храналагічным парадку. Ніякіх алгарытмаў, рэкламы або клікбэйту.",
"sign_in_banner.mastodon_is": "Mastodon - лепшы спосаб быць у курсе ўсяго, што адбываецца.",
"sign_in_banner.mastodon_is": "Mastodon - найлепшы спосаб быць у курсе ўсяго, што адбываецца.",
"sign_in_banner.sign_in": "Увайсці",
"sign_in_banner.sso_redirect": "Уваход ці рэгістрацыя",
"status.admin_account": "Адкрыць інтэрфейс мадэратара для @{name}",
"status.admin_domain": "Адкрыць інтэрфейс мадэратара для {domain}",
"status.admin_status": "Адкрыць гэты допіс у інтэрфейсе мадэрацыі",
"status.all_disabled": "Пашырэнні і цытаванні адключаны",
"status.block": "Заблакаваць @{name}",
"status.bookmark": "Дадаць закладку",
"status.cancel_reblog_private": "Прыбраць",
"status.cannot_quote": "Аўтар допісу адключыў цытаванне",
"status.cannot_reblog": "Гэты допіс нельга пашырыць",
"status.context.load_new_replies": "Даступныя новыя адказы",
"status.context.loading": "Правяраюцца новыя адказы",
@ -880,6 +882,8 @@
"status.mute_conversation": "Ігнараваць размову",
"status.open": "Разгарнуць гэты допіс",
"status.pin": "Замацаваць у профілі",
"status.quote": "Цытаваць",
"status.quote.cancel": "Адмяніць цытаванне",
"status.quote_error.filtered": "Схавана адным з Вашых фільтраў",
"status.quote_error.not_available": "Допіс недаступны",
"status.quote_error.pending_approval": "Допіс чакае пацвярджэння",
@ -887,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Цытаваны допіс чакае пацвярджэння? Захоўвайце спакой",
"status.quote_policy_change": "Змяніць, хто можа цытаваць",
"status.quote_post_author": "Цытаваў допіс @{name}",
"status.quote_private": "Прыватныя допісы нельга цытаваць",
"status.read_more": "Чытаць болей",
"status.reblog": "Пашырыць",
"status.reblog_private": "Пашырыць з першапачатковай бачнасцю",

View File

@ -848,9 +848,11 @@
"status.admin_account": "Obre la interfície de moderació per a @{name}",
"status.admin_domain": "Obre la interfície de moderació per a @{domain}",
"status.admin_status": "Obre aquest tut a la interfície de moderació",
"status.all_disabled": "S'han deshabilitat impulsos i cites",
"status.block": "Bloca @{name}",
"status.bookmark": "Marca",
"status.cancel_reblog_private": "Desfés l'impuls",
"status.cannot_quote": "L'autor no permet que se citi aquesta publicació",
"status.cannot_reblog": "No es pot impulsar aquest tut",
"status.context.load_new_replies": "Hi ha respostes noves",
"status.context.loading": "Comprovació de més respostes",
@ -879,6 +881,7 @@
"status.mute_conversation": "Silencia la conversa",
"status.open": "Amplia el tut",
"status.pin": "Fixa en el perfil",
"status.quote": "Cita",
"status.quote.cancel": "Canceŀlar la citació",
"status.quote_error.filtered": "No es mostra a causa d'un dels vostres filtres",
"status.quote_error.not_available": "Publicació no disponible",
@ -887,6 +890,7 @@
"status.quote_error.pending_approval_popout.title": "Publicació pendent? Mantinguem la calma",
"status.quote_policy_change": "Canvieu qui us pot citar",
"status.quote_post_author": "S'ha citat una publicació de @{name}",
"status.quote_private": "No es poden citar les publicacions privades",
"status.read_more": "Més informació",
"status.reblog": "Impulsa",
"status.reblog_private": "Impulsa amb la visibilitat original",

View File

@ -849,9 +849,11 @@
"status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
"status.admin_domain": "Otevřít moderátorské rozhraní pro {domain}",
"status.admin_status": "Otevřít tento příspěvek v moderátorském rozhraní",
"status.all_disabled": "Boosty a citace jsou zakázány",
"status.block": "Blokovat @{name}",
"status.bookmark": "Přidat do záložek",
"status.cancel_reblog_private": "Zrušit boostnutí",
"status.cannot_quote": "Autor zakázal citování tohoto příspěvku",
"status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
"status.context.load_new_replies": "K dispozici jsou nové odpovědi",
"status.context.loading": "Hledání dalších odpovědí",
@ -880,6 +882,8 @@
"status.mute_conversation": "Skrýt konverzaci",
"status.open": "Rozbalit tento příspěvek",
"status.pin": "Připnout na profil",
"status.quote": "Citovat",
"status.quote.cancel": "Zrušit citování",
"status.quote_error.filtered": "Skryté kvůli jednomu z vašich filtrů",
"status.quote_error.not_available": "Příspěvek není dostupný",
"status.quote_error.pending_approval": "Příspěvek čeká na schválení",
@ -887,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Příspěvek čeká na schválení? Buďte klidní",
"status.quote_policy_change": "Změňte, kdo může citovat",
"status.quote_post_author": "Citovali příspěvek od @{name}",
"status.quote_private": "Soukromé příspěvky nelze citovat",
"status.read_more": "Číst více",
"status.reblog": "Boostnout",
"status.reblog_private": "Boostnout s původní viditelností",

View File

@ -849,9 +849,11 @@
"status.admin_account": "@{name} moderieren",
"status.admin_domain": "{domain} moderieren",
"status.admin_status": "Beitrag moderieren",
"status.all_disabled": "Teilen und Zitieren von Beiträgen ist deaktiviert",
"status.block": "@{name} blockieren",
"status.bookmark": "Lesezeichen setzen",
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
"status.cannot_quote": "Autor*in hat das Zitieren dieses Beitrags deaktiviert",
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
"status.context.load_new_replies": "Neue Antworten verfügbar",
"status.context.loading": "Weitere Antworten werden abgerufen",
@ -880,6 +882,7 @@
"status.mute_conversation": "Unterhaltung stummschalten",
"status.open": "Beitrag öffnen",
"status.pin": "Im Profil anheften",
"status.quote": "Zitieren",
"status.quote.cancel": "Zitat abbrechen",
"status.quote_error.filtered": "Ausgeblendet wegen eines deiner Filter",
"status.quote_error.not_available": "Beitrag nicht verfügbar",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Zitierter Beitrag noch nicht freigegeben? Immer mit der Ruhe",
"status.quote_policy_change": "Ändern, wer zitieren darf",
"status.quote_post_author": "Zitierte einen Beitrag von @{name}",
"status.quote_private": "Private Beiträge können nicht zitiert werden",
"status.read_more": "Gesamten Beitrag anschauen",
"status.reblog": "Teilen",
"status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen",

View File

@ -849,9 +849,11 @@
"status.admin_account": "Άνοιγμα διεπαφής συντονισμού για τον/την @{name}",
"status.admin_domain": "Άνοιγμα λειτουργίας διαμεσολάβησης για {domain}",
"status.admin_status": "Άνοιγμα αυτής της ανάρτησης σε διεπαφή συντονισμού",
"status.all_disabled": "Ενισχύσεις και παραθέσεις είναι απενεργοποιημένες",
"status.block": "Αποκλεισμός @{name}",
"status.bookmark": "Σελιδοδείκτης",
"status.cancel_reblog_private": "Ακύρωση ενίσχυσης",
"status.cannot_quote": "Ο συντάκτης έχει απενεργοποιήσει την παράθεση σε αυτή την ανάρτηση",
"status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί",
"status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες",
"status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις",
@ -880,6 +882,8 @@
"status.mute_conversation": "Σίγαση συνομιλίας",
"status.open": "Επέκταση ανάρτησης",
"status.pin": "Καρφίτσωσε στο προφίλ",
"status.quote": "Παράθεση",
"status.quote.cancel": "Ακύρωση παράθεσης",
"status.quote_error.filtered": "Κρυφό λόγω ενός από τα φίλτρα σου",
"status.quote_error.not_available": "Ανάρτηση μη διαθέσιμη",
"status.quote_error.pending_approval": "Ανάρτηση σε αναμονή",
@ -887,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Παράθεση σε εκκρεμότητα; Μείνετε ψύχραιμοι",
"status.quote_policy_change": "Αλλάξτε ποιός μπορεί να κάνει παράθεση",
"status.quote_post_author": "Παρατίθεται μια ανάρτηση από @{name}",
"status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν",
"status.read_more": "Διάβασε περισότερα",
"status.reblog": "Ενίσχυση",
"status.reblog_private": "Ενίσχυση με αρχική ορατότητα",

View File

@ -738,11 +738,18 @@
"privacy.private.short": "Followers",
"privacy.public.long": "Anyone on and off Mastodon",
"privacy.public.short": "Public",
"privacy.quote.anyone": "{visibility}, anyone can quote",
"privacy.quote.disabled": "{visibility}, quotes disabled",
"privacy.quote.limited": "{visibility}, quotes limited",
"privacy.unlisted.additional": "This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.",
"privacy.unlisted.long": "Fewer algorithmic fanfares",
"privacy.unlisted.short": "Quiet public",
"privacy_policy.last_updated": "Last updated {date}",
"privacy_policy.title": "Privacy Policy",
"quote_error.poll": "Quoting is not allowed with polls.",
"quote_error.quote": "Only one quote at a time is allowed.",
"quote_error.unauthorized": "You are not authorized to quote this post.",
"quote_error.upload": "Quoting is not allowed with media attachments.",
"recommended": "Recommended",
"refresh": "Refresh",
"regeneration_indicator.please_stand_by": "Please stand by.",
@ -849,9 +856,11 @@
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}",
"status.admin_status": "Open this post in the moderation interface",
"status.all_disabled": "Boosts and quotes are disabled",
"status.block": "Block @{name}",
"status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost",
"status.cannot_quote": "Author has disabled quoting on this post",
"status.cannot_reblog": "This post cannot be boosted",
"status.context.load_new_replies": "New replies available",
"status.context.loading": "Checking for more replies",
@ -880,6 +889,7 @@
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.quote": "Quote",
"status.quote.cancel": "Cancel quote",
"status.quote_error.filtered": "Hidden due to one of your filters",
"status.quote_error.not_available": "Post unavailable",
@ -888,6 +898,7 @@
"status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
"status.quote_policy_change": "Change who can quote",
"status.quote_post_author": "Quoted a post by @{name}",
"status.quote_private": "Private posts cannot be quoted",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",
@ -940,6 +951,7 @@
"upload_button.label": "Add images, a video or an audio file",
"upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.",
"upload_error.quote": "File upload not allowed with quotes.",
"upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.",
"upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.",
"upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.",

View File

@ -568,6 +568,8 @@
"navigation_bar.preferences": "Preferoj",
"navigation_bar.privacy_and_reach": "Privateco kaj atingo",
"navigation_bar.search": "Serĉi",
"navigation_panel.expand_followed_tags": "Malfermi la menuon de sekvataj haŝetikedoj",
"navigation_panel.expand_lists": "Malfermi la listmenuon",
"not_signed_in_indicator.not_signed_in": "Necesas saluti por aliri tiun rimedon.",
"notification.admin.report": "{name} raportis {target}",
"notification.admin.report_account": "{name} raportis {count, plural, one {afiŝon} other {# afiŝojn}} de {target} por {category}",
@ -837,6 +839,7 @@
"status.block": "Bloki @{name}",
"status.bookmark": "Aldoni al la legosignoj",
"status.cancel_reblog_private": "Ne plu diskonigi",
"status.cannot_quote": "La aŭtoro malŝaltis citadon en ĉi tiu afiŝo",
"status.cannot_reblog": "Ĉi tiun afiŝon ne eblas diskonigi",
"status.context.load_new_replies": "Disponeblaj novaj respondoj",
"status.context.loading": "Serĉante pliajn respondojn",
@ -865,9 +868,16 @@
"status.mute_conversation": "Silentigi konversacion",
"status.open": "Pligrandigu ĉi tiun afiŝon",
"status.pin": "Alpingli al la profilo",
"status.quote": "Citaĵo",
"status.quote.cancel": "Nuligi citaĵon",
"status.quote_error.filtered": "Kaŝita pro unu el viaj filtriloj",
"status.quote_error.not_available": "Afiŝo ne disponebla",
"status.quote_error.pending_approval": "Pritraktata afiŝo",
"status.quote_error.pending_approval_popout.body": "Citaĵoj diskonigitaj tra la Fediverso povas bezoni tempon por montriĝi, ĉar malsamaj serviloj havas malsamajn protokolojn.",
"status.quote_error.pending_approval_popout.title": "Ĉu pritraktata citaĵo? Restu trankvila",
"status.quote_policy_change": "Ŝanĝi kiu povas citi",
"status.quote_post_author": "Citis afiŝon de @{name}",
"status.quote_private": "Privataj afiŝoj ne povas esti cititaj",
"status.read_more": "Legi pli",
"status.reblog": "Diskonigi",
"status.reblog_private": "Diskonigi kun la sama videbleco",
@ -882,6 +892,7 @@
"status.reply": "Respondi",
"status.replyAll": "Respondi al la fadeno",
"status.report": "Raporti @{name}",
"status.revoke_quote": "Forigu mian afiŝon el la afiŝo de @{name}",
"status.sensitive_warning": "Tikla enhavo",
"status.share": "Kundividi",
"status.show_less_all": "Montri malpli ĉiun",

View File

@ -849,9 +849,11 @@
"status.admin_account": "Abrir interface de moderación para @{name}",
"status.admin_domain": "Abrir interface de moderación para {domain}",
"status.admin_status": "Abrir este mensaje en la interface de moderación",
"status.all_disabled": "Las adhesiones y citas están deshabilitadas",
"status.block": "Bloquear a @{name}",
"status.bookmark": "Marcar",
"status.cancel_reblog_private": "Quitar adhesión",
"status.cannot_quote": "El autor deshabilitó las citas en este mensaje",
"status.cannot_reblog": "No se puede adherir a este mensaje",
"status.context.load_new_replies": "Hay nuevas respuestas",
"status.context.loading": "Buscando más respuestas",
@ -880,6 +882,7 @@
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir este mensaje",
"status.pin": "Fijar en el perfil",
"status.quote": "Citar",
"status.quote.cancel": "Cancelar cita",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.not_available": "Mensaje no disponible",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Esperá un momento",
"status.quote_policy_change": "Cambiá quién puede citar",
"status.quote_post_author": "Se citó un mensaje de @{name}",
"status.quote_private": "No se pueden citar los mensajes privados",
"status.read_more": "Leé más",
"status.reblog": "Adherir",
"status.reblog_private": "Adherir a la audiencia original",

View File

@ -849,9 +849,11 @@
"status.admin_account": "Abrir interfaz de moderación para @{name}",
"status.admin_domain": "Abrir interfaz de moderación para {domain}",
"status.admin_status": "Abrir este estado en la interfaz de moderación",
"status.all_disabled": "Los impulsos y las citas están desactivadas",
"status.block": "Bloquear a @{name}",
"status.bookmark": "Añadir marcador",
"status.cancel_reblog_private": "Deshacer impulso",
"status.cannot_quote": "El autor ha desactivado las citas de esta publicación",
"status.cannot_reblog": "Esta publicación no puede ser impulsada",
"status.context.load_new_replies": "Nuevas respuestas disponibles",
"status.context.loading": "Comprobando si hay más respuestas",
@ -880,6 +882,7 @@
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado",
"status.pin": "Fijar",
"status.quote": "Cita",
"status.quote.cancel": "Cancelar cita",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.not_available": "Publicación no disponible",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Mantén la calma",
"status.quote_policy_change": "Cambia quién puede citarte",
"status.quote_post_author": "Ha citado una publicación de @{name}",
"status.quote_private": "Las publicaciones privadas no pueden citarse",
"status.read_more": "Leer más",
"status.reblog": "Impulsar",
"status.reblog_private": "Implusar a la audiencia original",

View File

@ -849,9 +849,11 @@
"status.admin_account": "Abrir interfaz de moderación para @{name}",
"status.admin_domain": "Abrir interfaz de moderación para {domain}",
"status.admin_status": "Abrir esta publicación en la interfaz de moderación",
"status.all_disabled": "Los impulsos y las citas están deshabilitados",
"status.block": "Bloquear a @{name}",
"status.bookmark": "Añadir marcador",
"status.cancel_reblog_private": "Deshacer impulso",
"status.cannot_quote": "El autor ha deshabilitado las citas en esta publicación",
"status.cannot_reblog": "Esta publicación no se puede impulsar",
"status.context.load_new_replies": "Hay nuevas respuestas",
"status.context.loading": "Buscando más respuestas",
@ -880,6 +882,7 @@
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir publicación",
"status.pin": "Fijar",
"status.quote": "Cita",
"status.quote.cancel": "Cancelar cita",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.not_available": "Publicación no disponible",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Mantén la calma",
"status.quote_policy_change": "Cambia quién puede citarte",
"status.quote_post_author": "Ha citado una publicación de @{name}",
"status.quote_private": "Las publicaciones privadas no pueden ser citadas",
"status.read_more": "Leer más",
"status.reblog": "Impulsar",
"status.reblog_private": "Impulsar a la audiencia original",
@ -966,7 +970,7 @@
"visibility_modal.button_title": "Configura la visibilidad",
"visibility_modal.header": "Visibilidad e interacciones",
"visibility_modal.helper.direct_quoting": "Las menciones privadas no se pueden citar.",
"visibility_modal.helper.privacy_editing": "Las publicaciones no pueden cambiar su visibilidad.",
"visibility_modal.helper.privacy_editing": "Una vez publicada, no se puede cambiar su visibilidad.",
"visibility_modal.helper.private_quoting": "Las publicaciones para seguidores no pueden citarse.",
"visibility_modal.helper.unlisted_quoting": "Cuando la gente te cite, su publicación tampoco se mostrará en las cronologías públicas.",
"visibility_modal.instructions": "Controla quién puede interactuar con esta publicación. Puedes encontrar los ajustes globales en <link>Preferencias > Otros</link>.",

View File

@ -43,7 +43,7 @@
"account.followers": "پی‌گیرندگان",
"account.followers.empty": "هنوز کسی پی‌گیر این کاربر نیست.",
"account.followers_counter": "{count, plural, one {{counter} پی‌گیرنده} other {{counter} پی‌گیرنده}}",
"account.followers_you_know_counter": "{counter} را می‌شناسید",
"account.followers_you_know_counter": "{counter} نفر را می‌شناسید",
"account.following": "پی می‌گیرید",
"account.following_counter": "{count, plural, one {{counter} پی‌گرفته} other {{counter} پی‌گرفته}}",
"account.follows.empty": "این کاربر هنوز پی‌گیر کسی نیست.",
@ -880,6 +880,7 @@
"status.mute_conversation": "خموشاندن گفت‌وگو",
"status.open": "گسترش این فرسته",
"status.pin": "سنجاق به نمایه",
"status.quote.cancel": "لغو نقل",
"status.quote_error.filtered": "نهفته بنا بر یکی از پالایه‌هایتان",
"status.quote_error.not_available": "فرسته در دسترس نیست",
"status.quote_error.pending_approval": "فرسته منتظر",

View File

@ -849,9 +849,11 @@
"status.admin_account": "Avaa tilin @{name} moderointinäkymä",
"status.admin_domain": "Avaa palvelimen {domain} moderointinäkymä",
"status.admin_status": "Avaa julkaisu moderointinäkymässä",
"status.all_disabled": "Tehostukset ja lainaukset ovat poissa käytöstä",
"status.block": "Estä @{name}",
"status.bookmark": "Lisää kirjanmerkki",
"status.cancel_reblog_private": "Peru tehostus",
"status.cannot_quote": "Julkaisun tekijä on poistanut lainaamisen käytöstä",
"status.cannot_reblog": "Tätä julkaisua ei voi tehostaa",
"status.context.load_new_replies": "Uusia vastauksia saatavilla",
"status.context.loading": "Tarkistetaan lisävastauksia",
@ -880,6 +882,7 @@
"status.mute_conversation": "Mykistä keskustelu",
"status.open": "Laajenna julkaisu",
"status.pin": "Kiinnitä profiiliin",
"status.quote": "Lainaa",
"status.quote.cancel": "Peruuta lainaus",
"status.quote_error.filtered": "Piilotettu jonkin asettamasi suodattimen takia",
"status.quote_error.not_available": "Julkaisu ei saatavilla",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Odottava lainaus? Pysy rauhallisena",
"status.quote_policy_change": "Vaihda, kuka voi lainata",
"status.quote_post_author": "Lainaa käyttäjän @{name} julkaisua",
"status.quote_private": "Yksityisiä julkaisuja ei voi lainata",
"status.read_more": "Näytä enemmän",
"status.reblog": "Tehosta",
"status.reblog_private": "Tehosta alkuperäiselle yleisölle",

View File

@ -849,9 +849,11 @@
"status.admin_account": "Lat kjakleiðaramarkamót upp fyri @{name}",
"status.admin_domain": "Lat umsjónarmarkamót upp fyri {domain}",
"status.admin_status": "Lat hendan postin upp í kjakleiðaramarkamótinum",
"status.all_disabled": "Stimbranir og sitatir eru gjørd óvirkin",
"status.block": "Blokera @{name}",
"status.bookmark": "Goym",
"status.cancel_reblog_private": "Strika stimbran",
"status.cannot_quote": "Høvundurin hevur gjørt sitering óvirkna fyri hendan postin",
"status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin",
"status.context.load_new_replies": "Nýggj svar tøk",
"status.context.loading": "Kanni um tað eru fleiri svar",
@ -880,6 +882,7 @@
"status.mute_conversation": "Doyv samrøðu",
"status.open": "Víðka henda postin",
"status.pin": "Ger fastan í vangan",
"status.quote": "Sitat",
"status.quote.cancel": "Ógilda sitat",
"status.quote_error.filtered": "Eitt av tínum filtrum fjalir hetta",
"status.quote_error.not_available": "Postur ikki tøkur",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Bíðar eftir sitati? Tak tað róligt",
"status.quote_policy_change": "Broyt hvør kann sitera",
"status.quote_post_author": "Siteraði ein post hjá @{name}",
"status.quote_private": "Privatir postar kunnu ikki siterast",
"status.read_more": "Les meira",
"status.reblog": "Stimbra",
"status.reblog_private": "Stimbra við upprunasýni",

View File

@ -849,9 +849,11 @@
"status.admin_account": "פתח/י ממשק פיקוח דיון עבור @{name}",
"status.admin_domain": "פתיחת ממשק פיקוח דיון עבור {domain}",
"status.admin_status": "לפתוח הודעה זו במסך ניהול הדיונים",
"status.all_disabled": "הדהודים וציטוטים כובו",
"status.block": "חסימת @{name}",
"status.bookmark": "סימניה",
"status.cancel_reblog_private": "הסרת הדהוד",
"status.cannot_quote": "יוצר\\ת ההודעה חסמו את האפשרות לצטט אותה",
"status.cannot_reblog": "לא ניתן להדהד חצרוץ זה",
"status.context.load_new_replies": "הגיעו תגובות חדשות",
"status.context.loading": "מחפש תגובות חדשות",
@ -880,6 +882,7 @@
"status.mute_conversation": "השתקת שיחה",
"status.open": "הרחבת הודעה זו",
"status.pin": "הצמדה לפרופיל שלי",
"status.quote": "ציטוט",
"status.quote.cancel": "ביטול הודעת ציטוט",
"status.quote_error.filtered": "מוסתר בהתאם לסננים שלך",
"status.quote_error.not_available": "ההודעה לא זמינה",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "ההודעה בהמתנה? המתינו ברוגע",
"status.quote_policy_change": "הגדרת הרשאה לציטוט הודעותיך",
"status.quote_post_author": "ההודעה צוטטה על ידי @{name}",
"status.quote_private": "הודעות פרטיות לא ניתנות לציטוט",
"status.read_more": "לקרוא עוד",
"status.reblog": "הדהוד",
"status.reblog_private": "להדהד ברמת הנראות המקורית",

View File

@ -1,6 +1,7 @@
{
"about.blocks": "Servitores moderate",
"about.contact": "Contacto:",
"about.default_locale": "Default",
"about.disclaimer": "Mastodon es software libere, de codice aperte, e un marca de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Ration non disponibile",
"about.domain_blocks.preamble": "Mastodon generalmente permitte vider le contento de, e interager con, usatores de qualcunque altere servitor in le fediverso. Istes es le exceptiones que ha essite facite sur iste servitor particular.",
@ -8,6 +9,7 @@
"about.domain_blocks.silenced.title": "Limitate",
"about.domain_blocks.suspended.explanation": "Nulle datos de iste servitor essera processate, immagazinate o excambiate, rendente omne interaction o communication con usatores de iste servitor impossibile.",
"about.domain_blocks.suspended.title": "Suspendite",
"about.language_label": "Lingua",
"about.not_available": "Iste information non ha essite rendite disponibile sur iste servitor.",
"about.powered_by": "Rete social decentralisate, actionate per {mastodon}",
"about.rules": "Regulas del servitor",
@ -41,6 +43,7 @@
"account.followers": "Sequitores",
"account.followers.empty": "Necuno seque ancora iste usator.",
"account.followers_counter": "{count, plural, one {{counter} sequitor} other {{counter} sequitores}}",
"account.followers_you_know_counter": "{counter} que tu cognosce",
"account.following": "Sequente",
"account.following_counter": "{count, plural, one {{counter} sequite} other {{counter} sequites}}",
"account.follows.empty": "Iste usator non seque ancora alcuno.",
@ -216,6 +219,13 @@
"confirmations.delete_list.confirm": "Deler",
"confirmations.delete_list.message": "Es tu secur que tu vole deler permanentemente iste lista?",
"confirmations.delete_list.title": "Deler lista?",
"confirmations.discard_draft.confirm": "Discartar e continuar",
"confirmations.discard_draft.edit.cancel": "Reprender le modification",
"confirmations.discard_draft.edit.message": "Si tu continua, tu perdera tote le modificatione que tu ha apportate al message que tu modifica in iste momento.",
"confirmations.discard_draft.edit.title": "Discartar le modificationes de tu message?",
"confirmations.discard_draft.post.cancel": "Retornar al esbosso",
"confirmations.discard_draft.post.message": "Si tu continua, tu perdera le message que tu compone in iste momento.",
"confirmations.discard_draft.post.title": "Discartar tu esbosso de message?",
"confirmations.discard_edit_media.confirm": "Abandonar",
"confirmations.discard_edit_media.message": "Tu ha cambiamentos non salvate in le description o previsualisation del objecto multimedial. Abandonar los?",
"confirmations.follow_to_list.confirm": "Sequer e adder al lista",
@ -235,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Remover sequitor",
"confirmations.remove_from_followers.message": "{name} non plus te sequera. Es tu secur de voler continuar?",
"confirmations.remove_from_followers.title": "Remover sequitor?",
"confirmations.revoke_quote.confirm": "Remover message",
"confirmations.revoke_quote.message": "Iste action non pote esser disfacite.",
"confirmations.revoke_quote.title": "Remover message?",
"confirmations.unfollow.confirm": "Non plus sequer",
"confirmations.unfollow.message": "Es tu secur que tu vole cessar de sequer {name}?",
"confirmations.unfollow.title": "Cessar de sequer le usator?",
@ -279,6 +292,7 @@
"domain_pill.your_handle": "Tu pseudonymo:",
"domain_pill.your_server": "Tu casa digital, ubi vive tote tu messages. Non te place? Cambia de servitor a omne momento e porta tu sequitores con te.",
"domain_pill.your_username": "Tu identificator unic sur iste servitor. Es possibile trovar usatores con le mesme nomine de usator sur servitores differente.",
"dropdown.empty": "Selige un option",
"embed.instructions": "Incorpora iste message sur tu sito web con le codice sequente.",
"embed.preview": "Ecce como illlo parera:",
"emoji_button.activity": "Activitate",
@ -296,6 +310,8 @@
"emoji_button.search_results": "Resultatos de recerca",
"emoji_button.symbols": "Symbolos",
"emoji_button.travel": "Viages e locos",
"empty_column.account_featured.me": "Tu non ha ancora mittite alcun cosa in evidentia. Sapeva tu que tu pote mitter in evidentia sur tu profilo le hashtags que tu usa le plus e mesmo le contos de tu amicos?",
"empty_column.account_featured.other": "{acct} non ha ancora mittite alcun cosa in evidentia. Sapeva tu que tu pote mitter in evidentia sur tu profilo le hashtags que tu usa le plus e mesmo le contos de tu amicos?",
"empty_column.account_featured_other.unknown": "Iste conto non ha ancora mittite alcun cosa in evidentia.",
"empty_column.account_hides_collections": "Le usator non ha rendite iste information disponibile",
"empty_column.account_suspended": "Conto suspendite",
@ -325,9 +341,15 @@
"errors.unexpected_crash.copy_stacktrace": "Copiar le traciamento del pila al area de transferentia",
"errors.unexpected_crash.report_issue": "Reportar problema",
"explore.suggested_follows": "Personas",
"explore.title": "In tendentia",
"explore.trending_links": "Novas",
"explore.trending_statuses": "Messages",
"explore.trending_tags": "Hashtags",
"featured_carousel.header": "{count, plural, one {Message fixate} other {Messages fixate}}",
"featured_carousel.next": "Sequente",
"featured_carousel.post": "Message",
"featured_carousel.previous": "Precedente",
"featured_carousel.slide": "{index} de {total}",
"filter_modal.added.context_mismatch_explanation": "Iste categoria de filtros non se applica al contexto in le qual tu ha accedite a iste message. Pro filtrar le message in iste contexto tamben, modifica le filtro.",
"filter_modal.added.context_mismatch_title": "Contexto incoherente!",
"filter_modal.added.expired_explanation": "Iste categoria de filtros ha expirate. Tu debe modificar le data de expiration pro applicar lo.",
@ -380,6 +402,8 @@
"generic.saved": "Salvate",
"getting_started.heading": "Prime passos",
"hashtag.admin_moderation": "Aperir le interfacie de moderation pro #{name}",
"hashtag.browse": "Percurrer messages in #{hashtag}",
"hashtag.browse_from_account": "Percurrer le messages de @{name} in #{hashtag}",
"hashtag.column_header.tag_mode.all": "e {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sin {additional}",
@ -404,6 +428,7 @@
"hints.profiles.see_more_followers": "Vider plus de sequitores sur {domain}",
"hints.profiles.see_more_follows": "Vider plus de sequites sur {domain}",
"hints.profiles.see_more_posts": "Vider plus de messages sur {domain}",
"home.column_settings.show_quotes": "Monstrar citationes",
"home.column_settings.show_reblogs": "Monstrar impulsos",
"home.column_settings.show_replies": "Monstrar responsas",
"home.hide_announcements": "Celar annuncios",
@ -477,6 +502,8 @@
"keyboard_shortcuts.translate": "a traducer un message",
"keyboard_shortcuts.unfocus": "Disfocalisar le area de composition de texto/de recerca",
"keyboard_shortcuts.up": "Displaciar in alto in le lista",
"learn_more_link.got_it": "Comprendite",
"learn_more_link.learn_more": "Pro saper plus",
"lightbox.close": "Clauder",
"lightbox.next": "Sequente",
"lightbox.previous": "Precedente",
@ -526,8 +553,10 @@
"mute_modal.you_wont_see_mentions": "Tu non videra le messages que mentiona iste persona.",
"mute_modal.you_wont_see_posts": "Iste persona pote totevia vider tu messages, ma tu non videra le sues.",
"navigation_bar.about": "A proposito",
"navigation_bar.account_settings": "Contrasigno e securitate",
"navigation_bar.administration": "Administration",
"navigation_bar.advanced_interface": "Aperir in le interfacie web avantiate",
"navigation_bar.automated_deletion": "Deletion automatic del message",
"navigation_bar.blocks": "Usatores blocate",
"navigation_bar.bookmarks": "Marcapaginas",
"navigation_bar.direct": "Mentiones private",
@ -537,13 +566,23 @@
"navigation_bar.follow_requests": "Requestas de sequimento",
"navigation_bar.followed_tags": "Hashtags sequite",
"navigation_bar.follows_and_followers": "Sequites e sequitores",
"navigation_bar.import_export": "Importar e exportar",
"navigation_bar.lists": "Listas",
"navigation_bar.live_feed_local": "Canal in directo (local)",
"navigation_bar.live_feed_public": "Canal in directo (public)",
"navigation_bar.logout": "Clauder session",
"navigation_bar.moderation": "Moderation",
"navigation_bar.more": "Plus",
"navigation_bar.mutes": "Usatores silentiate",
"navigation_bar.opened_in_classic_interface": "Messages, contos e altere paginas specific es aperite per predefinition in le interfacie web classic.",
"navigation_bar.preferences": "Preferentias",
"navigation_bar.privacy_and_reach": "Confidentialitate e portata",
"navigation_bar.search": "Cercar",
"navigation_bar.search_trends": "Cercar / In tendentia",
"navigation_panel.collapse_followed_tags": "Contraher le menu de hashtags sequite",
"navigation_panel.collapse_lists": "Contraher le menu de listas",
"navigation_panel.expand_followed_tags": "Expander le menu de hashtags sequite",
"navigation_panel.expand_lists": "Expander le menu de listas",
"not_signed_in_indicator.not_signed_in": "Es necessari aperir session pro acceder a iste ressource.",
"notification.admin.report": "{name} ha reportate {target}",
"notification.admin.report_account": "{name} ha reportate {count, plural, one {un message} other {# messages}} de {target} per {category}",
@ -565,6 +604,7 @@
"notification.label.mention": "Mention",
"notification.label.private_mention": "Mention private",
"notification.label.private_reply": "Responsa private",
"notification.label.quote": "{name} ha citate tu message",
"notification.label.reply": "Responder",
"notification.mention": "Mention",
"notification.mentioned_you": "{name} te ha mentionate",
@ -622,6 +662,7 @@
"notifications.column_settings.mention": "Mentiones:",
"notifications.column_settings.poll": "Resultatos del sondage:",
"notifications.column_settings.push": "Notificationes push",
"notifications.column_settings.quote": "Citationes:",
"notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Monstrar in columna",
"notifications.column_settings.sound": "Reproducer sono",
@ -770,6 +811,7 @@
"report_notification.categories.violation": "Violation del regulas",
"report_notification.categories.violation_sentence": "violation del regulas",
"report_notification.open": "Aperir reporto",
"search.clear": "Rader le recerca",
"search.no_recent_searches": "Nulle recercas recente",
"search.placeholder": "Cercar",
"search.quick_action.account_search": "Profilos correspondente a {x}",
@ -811,6 +853,8 @@
"status.bookmark": "Adder al marcapaginas",
"status.cancel_reblog_private": "Disfacer impulso",
"status.cannot_reblog": "Iste message non pote esser impulsate",
"status.context.load_new_replies": "Nove responsas disponibile",
"status.context.loading": "Cercante plus responsas",
"status.continued_thread": "Continuation del discussion",
"status.copy": "Copiar ligamine a message",
"status.delete": "Deler",
@ -836,6 +880,14 @@
"status.mute_conversation": "Silentiar conversation",
"status.open": "Expander iste message",
"status.pin": "Fixar sur profilo",
"status.quote.cancel": "Cancellar le citation",
"status.quote_error.filtered": "Celate a causa de un de tu filtros",
"status.quote_error.not_available": "Message indisponibile",
"status.quote_error.pending_approval": "Message pendente",
"status.quote_error.pending_approval_popout.body": "Le citationes distribuite a transverso le Fediverso pote tardar in apparer, perque differente servitores ha differente protocollos.",
"status.quote_error.pending_approval_popout.title": "Citation pendente? Resta calme",
"status.quote_policy_change": "Cambiar qui pote citar",
"status.quote_post_author": "Ha citate un message de @{name}",
"status.read_more": "Leger plus",
"status.reblog": "Impulsar",
"status.reblog_private": "Impulsar con visibilitate original",
@ -850,6 +902,7 @@
"status.reply": "Responder",
"status.replyAll": "Responder al discussion",
"status.report": "Reportar @{name}",
"status.revoke_quote": "Remover mi message del message de @{name}",
"status.sensitive_warning": "Contento sensibile",
"status.share": "Compartir",
"status.show_less_all": "Monstrar minus pro totes",
@ -865,9 +918,13 @@
"subscribed_languages.save": "Salvar le cambiamentos",
"subscribed_languages.target": "Cambiar le linguas subscribite pro {target}",
"tabs_bar.home": "Initio",
"tabs_bar.menu": "Menu",
"tabs_bar.notifications": "Notificationes",
"tabs_bar.publish": "Nove message",
"tabs_bar.search": "Cercar",
"terms_of_service.effective_as_of": "In vigor a partir de {date}",
"terms_of_service.title": "Conditiones de servicio",
"terms_of_service.upcoming_changes_on": "Cambiamentos a venir le {date}",
"time_remaining.days": "{number, plural, one {# die} other {# dies}} restante",
"time_remaining.hours": "{number, plural, one {# hora} other {# horas}} restante",
"time_remaining.minutes": "{number, plural, one {# minuta} other {# minutas}} restante",
@ -875,7 +932,7 @@
"time_remaining.seconds": "{number, plural, one {# secunda} other {# secundas}} restante",
"trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} personas}} in le passate {days, plural, one {die} other {{days} dies}}",
"trends.trending_now": "Ora in tendentias",
"ui.beforeunload": "Tu esbosso essera predite si tu exi de Mastodon.",
"ui.beforeunload": "Tu esbosso essera perdite si tu exi de Mastodon.",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
@ -902,7 +959,20 @@
"video.pause": "Pausa",
"video.play": "Reproducer",
"video.skip_backward": "Saltar a retro",
"video.skip_forward": "Avantiar",
"video.unmute": "Non plus silentiar",
"video.volume_down": "Abassar le volumine",
"video.volume_up": "Augmentar le volumine"
"video.volume_up": "Augmentar le volumine",
"visibility_modal.button_title": "Definir visibilitate",
"visibility_modal.header": "Visibilitate e interaction",
"visibility_modal.helper.direct_quoting": "Le mentiones private non pote esser citate.",
"visibility_modal.helper.privacy_editing": "Le messages ja publicate non pote cambiar de visibilitate.",
"visibility_modal.helper.private_quoting": "Le messages reservate al sequitores non pote esser citate.",
"visibility_modal.helper.unlisted_quoting": "Quando un persona te cita, su message essera tamben celate del chronologia \"In tendentia\".",
"visibility_modal.instructions": "Controla qui pote interager con iste message. Le parametros global se trova sub <link>Preferentias > Alteres</link>.",
"visibility_modal.privacy_label": "Confidentialitate",
"visibility_modal.quote_followers": "Solmente sequitores",
"visibility_modal.quote_label": "Cambiar qui pote citar",
"visibility_modal.quote_nobody": "Necuno",
"visibility_modal.quote_public": "Omnes"
}

View File

@ -849,9 +849,11 @@
"status.admin_account": "Opna umsjónarviðmót fyrir @{name}",
"status.admin_domain": "Opna umsjónarviðmót fyrir @{domain}",
"status.admin_status": "Opna þessa færslu í umsjónarviðmótinu",
"status.all_disabled": "Endurbirtingar og tilvitnanir eru óvirkar",
"status.block": "Útiloka @{name}",
"status.bookmark": "Bókamerki",
"status.cancel_reblog_private": "Taka úr endurbirtingu",
"status.cannot_quote": "Höfundurinn hefur gert tilvitnanir óvirkar fyrir þessa færslu",
"status.cannot_reblog": "Þessa færslu er ekki hægt að endurbirta",
"status.context.load_new_replies": "Ný svör hafa borist",
"status.context.loading": "Athuga með fleiri svör",
@ -880,6 +882,7 @@
"status.mute_conversation": "Þagga niður í samtali",
"status.open": "Opna þessa færslu",
"status.pin": "Festa á notandasnið",
"status.quote": "Tilvitnun",
"status.quote.cancel": "Hætta við tilvitnun",
"status.quote_error.filtered": "Falið vegna einnar síu sem er virk",
"status.quote_error.not_available": "Færsla ekki tiltæk",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Færsla í bið? Verum róleg",
"status.quote_policy_change": "Breyttu því hver getur tilvitnað",
"status.quote_post_author": "Vitnaði í færslu frá @{name}",
"status.quote_private": "Ekki er hægt að vitna í einkafærslur",
"status.read_more": "Lesa meira",
"status.reblog": "Endurbirting",
"status.reblog_private": "Endurbirta til upphaflegra lesenda",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Rimuovi il seguace",
"confirmations.remove_from_followers.message": "{name} smetterà di seguirti. Si è sicuri di voler procedere?",
"confirmations.remove_from_followers.title": "Rimuovi il seguace?",
"confirmations.revoke_quote.confirm": "Elimina il post",
"confirmations.revoke_quote.message": "Questa azione non può essere annullata.",
"confirmations.revoke_quote.title": "Rimuovere il post?",
"confirmations.unfollow.confirm": "Smetti di seguire",
"confirmations.unfollow.message": "Sei sicuro di voler smettere di seguire {name}?",
"confirmations.unfollow.title": "Smettere di seguire l'utente?",
@ -289,6 +292,7 @@
"domain_pill.your_handle": "Il tuo nome univoco:",
"domain_pill.your_server": "La tua casa digitale, dove vivono tutti i tuoi post. Non ti piace questa? Cambia server in qualsiasi momento e porta con te anche i tuoi seguaci.",
"domain_pill.your_username": "Il tuo identificatore univoco su questo server. È possibile trovare utenti con lo stesso nome utente su server diversi.",
"dropdown.empty": "Seleziona un'opzione",
"embed.instructions": "Incorpora questo post sul tuo sito web, copiando il seguente codice.",
"embed.preview": "Ecco come apparirà:",
"emoji_button.activity": "Attività",
@ -845,9 +849,11 @@
"status.admin_account": "Apri interfaccia di moderazione per @{name}",
"status.admin_domain": "Apri l'interfaccia di moderazione per {domain}",
"status.admin_status": "Apri questo post nell'interfaccia di moderazione",
"status.all_disabled": "Condivisioni e citazioni sono disabilitate",
"status.block": "Blocca @{name}",
"status.bookmark": "Aggiungi segnalibro",
"status.cancel_reblog_private": "Annulla reblog",
"status.cannot_quote": "L'autore ha disabilitato le citazioni su questo post",
"status.cannot_reblog": "Questo post non può essere condiviso",
"status.context.load_new_replies": "Nuove risposte disponibili",
"status.context.loading": "Controllo per altre risposte",
@ -876,12 +882,16 @@
"status.mute_conversation": "Silenzia conversazione",
"status.open": "Espandi questo post",
"status.pin": "Fissa in cima sul profilo",
"status.quote": "Cita",
"status.quote.cancel": "Annulla la citazione",
"status.quote_error.filtered": "Nascosto a causa di uno dei tuoi filtri",
"status.quote_error.not_available": "Post non disponibile",
"status.quote_error.pending_approval": "Post in attesa",
"status.quote_error.pending_approval_popout.body": "Le citazioni condivise in tutto il Fediverso possono richiedere del tempo per la visualizzazione, poiché server diversi hanno protocolli diversi.",
"status.quote_error.pending_approval_popout.title": "Citazione in attesa? Resta calmo",
"status.quote_policy_change": "Cambia chi può citare",
"status.quote_post_author": "Citato un post di @{name}",
"status.quote_private": "I post privati non possono essere citati",
"status.read_more": "Leggi di più",
"status.reblog": "Reblog",
"status.reblog_private": "Reblog con visibilità originale",
@ -896,6 +906,7 @@
"status.reply": "Rispondi",
"status.replyAll": "Rispondi alla conversazione",
"status.report": "Segnala @{name}",
"status.revoke_quote": "Rimuovi la mia citazione dal post di @{name}",
"status.sensitive_warning": "Contenuto sensibile",
"status.share": "Condividi",
"status.show_less_all": "Mostra meno per tutti",
@ -955,5 +966,17 @@
"video.skip_forward": "Vai avanti",
"video.unmute": "Muta",
"video.volume_down": "Abbassa volume",
"video.volume_up": "Alza volume"
"video.volume_up": "Alza volume",
"visibility_modal.button_title": "Imposta la visibilità",
"visibility_modal.header": "Visibilità e interazione",
"visibility_modal.helper.direct_quoting": "Le menzioni private non possono essere citate.",
"visibility_modal.helper.privacy_editing": "La visibilità dei post pubblicati non può essere modificata.",
"visibility_modal.helper.private_quoting": "I post riservati ai seguaci non possono essere citati.",
"visibility_modal.helper.unlisted_quoting": "Quando le persone ti citano, il loro post verrà nascosto anche dalle timeline di tendenza.",
"visibility_modal.instructions": "Controlla chi può interagire con questo post. Le impostazioni globali si trovano in <link>Preferenze > Altro</link>.",
"visibility_modal.privacy_label": "Privacy",
"visibility_modal.quote_followers": "Solo i seguaci",
"visibility_modal.quote_label": "Cambia chi può citare",
"visibility_modal.quote_nobody": "Nessuno",
"visibility_modal.quote_public": "Chiunque"
}

View File

@ -23,11 +23,14 @@
"account.edit_profile": "प्रोफाइल सम्पादन गर्नुहोस्",
"account.enable_notifications": "@{name} ले पोस्ट गर्दा मलाई सूचित गर्नुहोस्",
"account.endorse": "प्रोफाइलमा फिचर गर्नुहोस्",
"account.featured": "फिचर गरिएको",
"account.featured.accounts": "प्रोफाइलहरू",
"account.featured.hashtags": "ह्यासट्यागहरू",
"account.featured_tags.last_status_never": "कुनै पोस्ट छैन",
"account.follow": "फलो गर्नुहोस",
"account.follow_back": "फलो ब्याक गर्नुहोस्",
"account.followers": "फलोअरहरु",
"account.followers.empty": "यस प्रयोगकर्तालाई अहिलेसम्म कसैले फलो गर्दैन।",
"account.followers.empty": "यस प्रयोगकर्तालाई अहिलेसम्म कसैले फलो गरेको छैन।",
"account.followers_counter": "{count, plural, one {{counter} फलोअर} other {{counter} फलोअरहरू}}",
"account.following": "फलो गर्दै",
"account.following_counter": "{count, plural, one {{counter} फलो गर्दै} other {{counter} फलो गर्दै}}",
@ -42,6 +45,7 @@
"account.mute_notifications_short": "सूचनाहरू म्यूट गर्नुहोस्",
"account.mute_short": "म्युट",
"account.muted": "म्युट गरिएको",
"account.mutual": "तपाईंहरु एकअर्कालाई फलो गर्नुहुन्छ",
"account.no_bio": "कुनै विवरण प्रदान गरिएको छैन।",
"account.posts": "पोस्टहरू",
"account.posts_with_replies": "पोस्ट र जवाफहरू",
@ -53,6 +57,7 @@
"account.statuses_counter": "{count, plural, one {{counter} पोस्ट} other {{counter} पोस्टहरू}}",
"account.unblock": "@{name} लाई अनब्लक गर्नुहोस्",
"account.unblock_domain": "{domain} डोमेनलाई अनब्लक गर्नुहोस्",
"account.unblock_domain_short": "अनब्लक गर्नुहोस्",
"account.unblock_short": "अनब्लक गर्नुहोस्",
"account.unendorse": "प्रोफाइलमा फिचर नगर्नुहोस्",
"account.unfollow": "अनफलो गर्नुहोस्",
@ -64,6 +69,7 @@
"admin.dashboard.retention.cohort_size": "नयाँ प्रयोगकर्ताहरू",
"alert.rate_limited.message": "कृपया {retry_time, time, medium} पछि पुन: प्रयास गर्नुहोस्।",
"alert.unexpected.message": "एउटा अनपेक्षित त्रुटि भयो।",
"alt_text_modal.cancel": "रद्द गर्नुहोस्",
"announcement.announcement": "घोषणा",
"annual_report.summary.followers.followers": "फलोअरहरु",
"annual_report.summary.highlighted_post.by_reblogs": "सबैभन्दा बढि बूस्ट गरिएको पोस्ट",
@ -156,8 +162,13 @@
"notification_requests.confirm_accept_multiple.title": "सूचना अनुरोधहरू स्वीकार गर्ने?",
"notification_requests.confirm_dismiss_multiple.title": "सूचना अनुरोधहरू खारेज गर्ने?",
"notifications.clear_title": "सूचनाहरू खाली गर्ने?",
"notifications.column_settings.follow": "नयाँ फलोअरहरु:",
"notifications.column_settings.follow_request": "नयाँ फलोअर अनुरोधहरु",
"notifications.column_settings.reblog": "बूस्टहरू:",
"notifications.filter.boosts": "बूस्टहरू",
"privacy.private.long": "मात्र तपाईंको फलोअरहरु",
"privacy.private.short": "फलोअरहरु",
"reply_indicator.cancel": "रद्द गर्नुहोस्",
"report.comment.title": "के हामीले थाहा पाउनुपर्ने अरू केही छ जस्तो लाग्छ?",
"report.forward_hint": "यो खाता अर्को सर्भरबाट हो। त्यहाँ पनि रिपोर्टको गुमनाम प्रतिलिपि पठाउने हो?",
"report.rules.title": "कुन नियमहरू उल्लङ्घन भइरहेका छन्?",
@ -172,5 +183,6 @@
"status.reblog": "बूस्ट गर्नुहोस्",
"status.reblogged_by": "{name} ले बूस्ट गर्नुभएको",
"status.reblogs": "{count, plural, one {बूस्ट} other {बूस्टहरू}}",
"status.unmute_conversation": "कुराकानी अनम्यूट गर्नुहोस्"
"status.unmute_conversation": "कुराकानी अनम्यूट गर्नुहोस्",
"visibility_modal.quote_followers": "फलोअरहरु मात्र"
}

View File

@ -350,14 +350,14 @@
"featured_carousel.previous": "Предыдущий",
"featured_carousel.slide": "{index} из {total}",
"filter_modal.added.context_mismatch_explanation": "Этот фильтр не применяется в том контексте, в котором вы видели этот пост. Если вы хотите, чтобы пост был отфильтрован в текущем контексте, необходимо редактировать фильтр.",
"filter_modal.added.context_mismatch_title": "Несоответствие контекста!",
"filter_modal.added.context_mismatch_title": "Несоответствие контекста",
"filter_modal.added.expired_explanation": "Этот фильтр истёк. Чтобы он был применён, вам нужно изменить срок действия фильтра.",
"filter_modal.added.expired_title": "Истёкший фильтр!",
"filter_modal.added.expired_title": "Истёкший фильтр",
"filter_modal.added.review_and_configure": "Для просмотра или редактирования этого фильтра перейдите в {settings_link}.",
"filter_modal.added.review_and_configure_title": "Настройки фильтра",
"filter_modal.added.settings_link": "настройки",
"filter_modal.added.short_explanation": "Этот пост был добавлен к фильтру «{title}».",
"filter_modal.added.title": "Фильтр добавлен!",
"filter_modal.added.title": "Фильтр добавлен",
"filter_modal.select_filter.context_mismatch": "не применяется в этом контексте",
"filter_modal.select_filter.expired": "истёкший",
"filter_modal.select_filter.prompt_new": "Новый фильтр: {name}",
@ -433,7 +433,7 @@
"home.hide_announcements": "Скрыть объявления",
"home.pending_critical_update.body": "Пожалуйста, обновите свой сервер Mastodon как можно скорее!",
"home.pending_critical_update.link": "Посмотреть обновления",
"home.pending_critical_update.title": "Доступно критическое обновление безопасности!",
"home.pending_critical_update.title": "Доступно критическое обновление безопасности",
"home.show_announcements": "Показать объявления",
"ignore_notifications_modal.disclaimer": "Mastodon не может сообщить пользователям, что вы игнорируете их уведомления. Игнорирование уведомлений не остановит отправку самих сообщений.",
"ignore_notifications_modal.filter_instead": "Фильтровать",
@ -590,7 +590,7 @@
"notification.admin.report_statuses_other": "{name} пожаловался (-лась) на {target}",
"notification.admin.sign_up": "{name} зарегистрировался (-лась) на сервере",
"notification.admin.sign_up.name_and_others": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} зарегистрировались на сервере",
"notification.annual_report.message": "#Wrapstodon за {year} год ждёт вас! Откройте для себя итоги и памятные моменты этого года в Mastodon!",
"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 {# пользователь} few {# пользователя} other {# пользователей}}</a> добавили ваш пост в избранное",
@ -861,7 +861,7 @@
"status.direct": "Упомянуть @{name} лично",
"status.direct_indicator": "Личное упоминание",
"status.edit": "Редактировать",
"status.edited": "Дата последнего изменения: {date}",
"status.edited": "Последнее изменение: {date}",
"status.edited_x_times": "{count, plural, one {{count} изменение} many {{count} изменений} other {{count} изменения}}",
"status.embed": "Встроить на свой сайт",
"status.favourite": "Добавить в избранное",

View File

@ -246,6 +246,7 @@
"confirmations.remove_from_followers.message": "{name} kommer att sluta följa dig. Är du säker på att du vill fortsätta?",
"confirmations.remove_from_followers.title": "Ta bort följare?",
"confirmations.revoke_quote.confirm": "Ta bort inlägg",
"confirmations.revoke_quote.message": "Denna åtgärd kan inte ångras.",
"confirmations.revoke_quote.title": "Ta bort inlägg?",
"confirmations.unfollow.confirm": "Avfölj",
"confirmations.unfollow.message": "Är du säker på att du vill avfölja {name}?",
@ -603,6 +604,7 @@
"notification.label.mention": "Nämn",
"notification.label.private_mention": "Privat omnämnande",
"notification.label.private_reply": "Privata svar",
"notification.label.quote": "{name} citerade ditt inlägg",
"notification.label.reply": "Svar",
"notification.mention": "Nämn",
"notification.mentioned_you": "{name} nämnde dig",
@ -660,6 +662,7 @@
"notifications.column_settings.mention": "Omnämningar:",
"notifications.column_settings.poll": "Omröstningsresultat:",
"notifications.column_settings.push": "Push-aviseringar",
"notifications.column_settings.quote": "Citerat:",
"notifications.column_settings.reblog": "Boostar:",
"notifications.column_settings.show": "Visa i kolumnen",
"notifications.column_settings.sound": "Spela upp ljud",
@ -877,8 +880,14 @@
"status.mute_conversation": "Tysta konversation",
"status.open": "Utvidga detta inlägg",
"status.pin": "Fäst i profil",
"status.quote.cancel": "Återkalla citering",
"status.quote_error.filtered": "Dolt på grund av ett av dina filter",
"status.quote_error.not_available": "Inlägg ej tillgängligt",
"status.quote_error.pending_approval": "Väntande inlägg",
"status.quote_error.pending_approval_popout.body": "Citat som delas över Fediverse kan ta tid att visa, eftersom olika servrar har olika protokoll.",
"status.quote_error.pending_approval_popout.title": "Väntande citat? Förbli lugn",
"status.quote_policy_change": "Ändra vem som kan citera",
"status.quote_post_author": "Citerade ett inlägg av @{name}",
"status.read_more": "Läs mer",
"status.reblog": "Boosta",
"status.reblog_private": "Boosta med ursprunglig synlighet",
@ -893,6 +902,7 @@
"status.reply": "Svara",
"status.replyAll": "Svara på tråden",
"status.report": "Rapportera @{name}",
"status.revoke_quote": "Ta bort mitt inlägg från @{name}s inlägg",
"status.sensitive_warning": "Känsligt innehåll",
"status.share": "Dela",
"status.show_less_all": "Visa mindre för alla",
@ -953,9 +963,16 @@
"video.unmute": "Avtysta",
"video.volume_down": "Volym ned",
"video.volume_up": "Volym upp",
"visibility_modal.button_title": "Ange synlighet",
"visibility_modal.header": "Synlighet och interaktion",
"visibility_modal.helper.direct_quoting": "Privata omnämnanden kan inte bli citerade.",
"visibility_modal.helper.privacy_editing": "Publicerade inlägg kan inte ändra deras synlighet.",
"visibility_modal.helper.private_quoting": "Inlägg som endast är synliga för följare kan inte citeras.",
"visibility_modal.helper.unlisted_quoting": "När folk citerar dig, deras inlägg kommer också att döljas från trendiga tidslinjer.",
"visibility_modal.instructions": "Kontrollera vem som kan interagera med det här inlägget. Globala inställningar kan hittas under <link>Inställningar > Andra</link>.",
"visibility_modal.privacy_label": "Integritet",
"visibility_modal.quote_followers": "Endast följare",
"visibility_modal.quote_label": "Ändra vem som kan citera",
"visibility_modal.quote_nobody": "Ingen",
"visibility_modal.quote_public": "Alla"
}

View File

@ -292,6 +292,7 @@
"domain_pill.your_handle": "Tanıtıcınız:",
"domain_pill.your_server": "Dijital anasayfanız, tüm gönderilerinizin yaşadığı yerdir. Bunu beğenmediniz mi? İstediğiniz zaman sunucularınızı değiştirin ve takipçilerinizi de getirin.",
"domain_pill.your_username": "Bu sunucudaki tekil tanımlayıcınız. Farklı sunucularda aynı kullanıcı adına sahip kullanıcıları bulmak mümkündür.",
"dropdown.empty": "Bir seçenek seçin",
"embed.instructions": "Aşağıdaki kodu kopyalayarak bu durumu sitenize gömün.",
"embed.preview": "İşte nasıl görüneceği:",
"emoji_button.activity": "Aktivite",
@ -848,9 +849,11 @@
"status.admin_account": "@{name} için denetim arayüzünü açın",
"status.admin_domain": "{domain} için denetim arayüzünü açın",
"status.admin_status": "Denetim arayüzünde bu gönderiyi açın",
"status.all_disabled": "Yükseltmeler ve alıntılar devre dışı bırakıldı",
"status.block": "@{name} adlı kişiyi engelle",
"status.bookmark": "Yer işareti ekle",
"status.cancel_reblog_private": "Yeniden paylaşımı geri al",
"status.cannot_quote": "Yazar bu gönderide alıntılamayı devre dışı bıraktı",
"status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz",
"status.context.load_new_replies": "Yeni yanıtlar mevcut",
"status.context.loading": "Daha fazla yanıt için kontrol ediliyor",
@ -879,12 +882,16 @@
"status.mute_conversation": "Sohbeti sessize al",
"status.open": "Bu gönderiyi genişlet",
"status.pin": "Profile sabitle",
"status.quote": "Teklif",
"status.quote.cancel": "Teklifi iptal et",
"status.quote_error.filtered": "Bazı filtrelerinizden dolayı gizlenmiştir",
"status.quote_error.not_available": "Gönderi kullanılamıyor",
"status.quote_error.pending_approval": "Gönderi beklemede",
"status.quote_error.pending_approval_popout.body": "Fediverse genelinde paylaşılan alıntıların görüntülenmesi zaman alabilir, çünkü farklı sunucuların farklı protokolleri vardır.",
"status.quote_error.pending_approval_popout.title": "Bekleyen bir teklif mi var? Sakin olun.",
"status.quote_policy_change": "Kimin alıntı yapabileceğini değiştirin",
"status.quote_post_author": "@{name} adlı kullanıcının bir gönderisini alıntıladı",
"status.quote_private": "Özel gönderiler alıntılanamaz",
"status.read_more": "Devamını okuyun",
"status.reblog": "Yeniden paylaş",
"status.reblog_private": "Özgün görünürlük ile yeniden paylaş",
@ -959,5 +966,17 @@
"video.skip_forward": "İleriye atla",
"video.unmute": "Sesi aç",
"video.volume_down": "Sesi kıs",
"video.volume_up": "Sesi yükselt"
"video.volume_up": "Sesi yükselt",
"visibility_modal.button_title": "Görünürlüğü ayarla",
"visibility_modal.header": "Görünürlük ve etkileşim",
"visibility_modal.helper.direct_quoting": "Özel gönderiler alıntılanamaz.",
"visibility_modal.helper.privacy_editing": "Yayınlanan gönderilerin görünürlüğü değiştirilemez.",
"visibility_modal.helper.private_quoting": "Sadece takipçilere özel paylaşımlar alıntılanamaz.",
"visibility_modal.helper.unlisted_quoting": "İnsanlar sizden alıntı yaptığında, onların gönderileri de trend zaman tünellerinden gizlenecektir.",
"visibility_modal.instructions": "Bu gönderiyle kimlerin etkileşimde bulunabileceğini kontrol edin. Genel ayarlara <link>Tercihler > Diğer</link> bölümünden ulaşabilirsiniz.",
"visibility_modal.privacy_label": "Gizlilik",
"visibility_modal.quote_followers": "Sadece takipçiler",
"visibility_modal.quote_label": "Kimin alıntı yapabileceğini değiştirin",
"visibility_modal.quote_nobody": "Kimseden",
"visibility_modal.quote_public": "Herkesten"
}

View File

@ -849,9 +849,11 @@
"status.admin_account": "Mở giao diện quản trị @{name}",
"status.admin_domain": "Mở giao diện quản trị @{domain}",
"status.admin_status": "Mở tút này trong giao diện quản trị",
"status.all_disabled": "Đăng lại và trích dẫn bị tắt",
"status.block": "Chặn @{name}",
"status.bookmark": "Lưu",
"status.cancel_reblog_private": "Bỏ đăng lại",
"status.cannot_quote": "Tác giả không cho phép trích dẫn tút này",
"status.cannot_reblog": "Không thể đăng lại tút này",
"status.context.load_new_replies": "Có những trả lời mới",
"status.context.loading": "Kiểm tra nhiều trả lời hơn",
@ -880,6 +882,7 @@
"status.mute_conversation": "Tắt thông báo",
"status.open": "Mở tút",
"status.pin": "Ghim lên hồ sơ",
"status.quote": "Trích dẫn",
"status.quote.cancel": "Bỏ trích dẫn",
"status.quote_error.filtered": "Bị ẩn vì một bộ lọc của bạn",
"status.quote_error.not_available": "Tút không khả dụng",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "Đang chờ trích dẫn? Hãy bình tĩnh",
"status.quote_policy_change": "Thay đổi người có thể trích dẫn",
"status.quote_post_author": "Trích dẫn từ tút của @{name}",
"status.quote_private": "Không thể trích dẫn nhắn riêng",
"status.read_more": "Đọc tiếp",
"status.reblog": "Đăng lại",
"status.reblog_private": "Đăng lại (Riêng tư)",

View File

@ -849,9 +849,11 @@
"status.admin_account": "開啟 @{name} 的管理介面",
"status.admin_domain": "開啟 {domain} 的管理介面",
"status.admin_status": "於管理介面開啟此嘟文",
"status.all_disabled": "已停用轉嘟與引用",
"status.block": "封鎖 @{name}",
"status.bookmark": "書籤",
"status.cancel_reblog_private": "取消轉嘟",
"status.cannot_quote": "作者已停用引用此嘟文",
"status.cannot_reblog": "這則嘟文無法被轉嘟",
"status.context.load_new_replies": "有新回嘟",
"status.context.loading": "正在檢查更多回嘟",
@ -880,6 +882,7 @@
"status.mute_conversation": "靜音對話",
"status.open": "展開此嘟文",
"status.pin": "釘選至個人檔案頁面",
"status.quote": "引用",
"status.quote.cancel": "取消引用嘟文",
"status.quote_error.filtered": "由於您的過濾器,該嘟文被隱藏",
"status.quote_error.not_available": "無法取得該嘟文",
@ -888,6 +891,7 @@
"status.quote_error.pending_approval_popout.title": "引用嘟文正在發送中?別著急,請稍候片刻",
"status.quote_policy_change": "變更可以引用的人",
"status.quote_post_author": "已引用 @{name} 之嘟文",
"status.quote_private": "無法引用私人嘟文",
"status.read_more": "閱讀更多",
"status.reblog": "轉嘟",
"status.reblog_private": "依照原嘟可見性轉嘟",

View File

@ -1,10 +1,12 @@
import type { KeyboardEvent, MouseEvent, TouchEvent } from 'react';
interface BaseMenuItem {
text: string;
dangerous?: boolean;
}
export interface ActionMenuItem extends BaseMenuItem {
action: () => void;
action: (event: MouseEvent | KeyboardEvent | TouchEvent) => void;
}
export interface LinkMenuItem extends BaseMenuItem {

View File

@ -2,9 +2,9 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrde
import {
changeUploadCompose,
quoteComposeByStatus,
quoteCompose,
quoteComposeCancel,
setQuotePolicy,
setComposeQuotePolicy,
} from 'mastodon/actions/compose_typed';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
@ -329,22 +329,15 @@ export const composeReducer = (state = initialState, action) => {
return state.set('is_changing_upload', true);
} else if (changeUploadCompose.rejected.match(action)) {
return state.set('is_changing_upload', false);
} else if (quoteComposeByStatus.match(action)) {
} else if (quoteCompose.match(action)) {
const status = action.payload;
if (
status.getIn(['quote_approval', 'current_user']) === 'automatic' &&
state.get('media_attachments').size === 0 &&
!state.get('is_uploading') &&
!state.get('poll')
) {
return state
.set('quoted_status_id', status.get('id'))
.set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text'));
}
return state
.set('quoted_status_id', status.get('id'))
.set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text'));
} else if (quoteComposeCancel.match(action)) {
return state.set('quoted_status_id', null);
} else if (setQuotePolicy.match(action)) {
} else if (setComposeQuotePolicy.match(action)) {
return state.set('quote_policy', action.payload);
}
@ -520,6 +513,7 @@ export const composeReducer = (state = initialState, action) => {
map.set('sensitive', action.status.get('sensitive'));
map.set('language', action.status.get('language'));
map.set('id', null);
map.set('quoted_status_id', action.status.getIn(['quote', 'quoted_status']));
// Mastodon-authored posts can be expected to have at most one automatic approval policy
map.set('quote_policy', action.status.getIn(['quote_approval', 'automatic', 0]) || 'nobody');
@ -551,6 +545,7 @@ export const composeReducer = (state = initialState, action) => {
map.set('idempotencyKey', uuid());
map.set('sensitive', action.status.get('sensitive'));
map.set('language', action.status.get('language'));
map.set('quoted_status_id', action.status.getIn(['quote', 'quoted_status']));
// Mastodon-authored posts can be expected to have at most one automatic approval policy
map.set('quote_policy', action.status.getIn(['quote_approval', 'automatic', 0]) || 'nobody');

View File

@ -15,7 +15,7 @@ export const getFilters = createSelector(
(_, { contextType }: { contextType: string }) => contextType,
],
(filters, contextType) => {
if (!contextType || contextType === 'compose') {
if (!contextType) {
return null;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M791-56 425-422 320-240h-92l92-160q-66 0-113-47t-47-113q0-27 8.5-51t23.5-44L56-791l56-57 736 736-57 56Zm-55-281L520-553v-7q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480l-82 143ZM320-500q6 0 12-1t11-3l-79-79q-2 5-3 11t-1 12q0 25 17.5 42.5T320-500Zm360 0q25 0 42.5-17.5T740-560q0-25-17.5-42.5T680-620q-25 0-42.5 17.5T620-560q0 25 17.5 42.5T680-500Zm-374-41Zm374-19Z"/></svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@ -1482,18 +1482,19 @@ body > [data-popper-placement] {
border-bottom: 0;
.status__content,
.status__action-bar,
.media-gallery,
.video-player,
.audio-player,
.attachment-list,
.picture-in-picture-placeholder,
.more-from-author,
.status-card,
.hashtag-bar,
.content-warning,
.filter-warning {
& > .status__content,
& > .status__action-bar,
& > .media-gallery,
& > .video-player,
& > .audio-player,
& > .attachment-list,
& > .picture-in-picture-placeholder,
& > .more-from-author,
& > .status-card,
& > .hashtag-bar,
& > .content-warning,
& > .filter-warning,
& > .status__quote {
margin-inline-start: var(--thread-margin);
width: calc(100% - var(--thread-margin));
}
@ -2383,6 +2384,7 @@ a .account__avatar {
.detailed-status__display-name,
.detailed-status__datetime,
.detailed-status__application,
.detailed-status__link,
.account__display-name {
text-decoration: none;
}
@ -2415,7 +2417,8 @@ a.account__display-name {
}
.detailed-status__application,
.detailed-status__datetime {
.detailed-status__datetime,
.detailed-status__link {
color: inherit;
}
@ -2601,8 +2604,9 @@ a.account__display-name {
}
.status__relative-time,
.detailed-status__datetime {
&:hover {
.detailed-status__datetime,
.detailed-status__link {
&:is(a):hover {
text-decoration: underline;
}
}
@ -2856,10 +2860,43 @@ a.account__display-name {
&:focus,
&:hover,
&:active {
background: var(--dropdown-border-color);
outline: 0;
&:not(:disabled) {
background: var(--dropdown-border-color);
outline: 0;
}
}
}
button:disabled {
color: $dark-text-color;
cursor: default;
}
}
.reblog-button {
&__item {
width: 280px;
button {
display: flex;
align-items: center;
gap: 8px;
white-space: inherit;
}
div {
display: flex;
flex-direction: column;
}
&.active:not(.disabled) {
color: $highlight-text-color;
}
}
&__meta {
font-weight: 400;
}
}
.inline-account {
@ -5478,7 +5515,10 @@ a.status-card {
.privacy-dropdown__option__content,
.privacy-dropdown__option__content strong,
.privacy-dropdown__option__additional {
.privacy-dropdown__option__additional,
.visibility-dropdown__option__content,
.visibility-dropdown__option__content strong,
.visibility-dropdown__option__additional {
color: $primary-text-color;
}
}
@ -5492,13 +5532,15 @@ a.status-card {
}
}
.privacy-dropdown__option__icon {
.privacy-dropdown__option__icon,
.visibility-dropdown__option__icon {
display: flex;
align-items: center;
justify-content: center;
}
.privacy-dropdown__option__content {
.privacy-dropdown__option__content,
.visibility-dropdown__option__content {
flex: 1 1 auto;
color: $darker-text-color;

View File

@ -1,9 +1,13 @@
import { Map as ImmutableMap } from 'immutable';
import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships';
import type { ApiStatusJSON } from '@/mastodon/api_types/statuses';
import type {
CustomEmojiData,
UnicodeEmojiData,
} from '@/mastodon/features/emoji/types';
import { createAccountFromServerJSON } from '@/mastodon/models/account';
import type { Status } from '@/mastodon/models/status';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
type FactoryOptions<T> = {
@ -51,6 +55,36 @@ export const accountFactoryState = (
options: FactoryOptions<ApiAccountJSON> = {},
) => createAccountFromServerJSON(accountFactory(options));
export const statusFactory: FactoryFunction<ApiStatusJSON> = ({
id,
...data
} = {}) => ({
id: id ?? '1',
created_at: '2023-01-01T00:00:00.000Z',
sensitive: false,
visibility: 'public',
language: 'en',
uri: 'https://example.com/status/1',
url: 'https://example.com/status/1',
replies_count: 0,
reblogs_count: 0,
favorites_count: 0,
account: accountFactory(),
media_attachments: [],
mentions: [],
tags: [],
emojis: [],
content: '<p>This is a test status.</p>',
...data,
});
export const statusFactoryState = (
options: FactoryOptions<ApiStatusJSON> = {},
) =>
ImmutableMap<string, unknown>(
statusFactory(options) as unknown as Record<string, unknown>,
) as unknown as Status;
export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
id,
...data

View File

@ -45,6 +45,10 @@ class StatusEdit < ApplicationRecord
delegate :local?, :application, :edited?, :edited_at,
:discarded?, :visibility, :language, to: :status
def with_media?
ordered_media_attachments.any?
end
def emojis
return @emojis if defined?(@emojis)

View File

@ -12,7 +12,7 @@ class InitialStateSerializer < ActiveModel::Serializer
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
has_one :role, serializer: REST::RoleSerializer
def meta # rubocop:disable Metrics/AbcSize
def meta
store = default_meta_store
if object.current_account

View File

@ -1,6 +0,0 @@
- if status.ordered_media_attachments.first.video?
= render_video_component(status, visible: false)
- elsif status.ordered_media_attachments.first.audio?
= render_audio_component(status)
- else
= render_media_gallery_component(status, visible: false)

View File

@ -1,53 +0,0 @@
.batch-table__row
%label.batch-table__row__select.batch-checkbox
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
.batch-table__row__content
.status__card
- if status.reblog?
.status__prepend
= material_symbol('repeat')
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id)))
- elsif status.reply? && status.in_reply_to_id.present?
.status__prepend
= material_symbol('reply')
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil))
.status__content><
- if status.proper.spoiler_text.blank?
= prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis)
- else
%details<
%summary><
%strong> Content warning: #{prerender_custom_emojis(h(status.proper.spoiler_text), status.proper.emojis)}
= prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis)
- unless status.proper.ordered_media_attachments.empty?
= render partial: 'admin/reports/media_attachments', locals: { status: status.proper }
.detailed-status__meta
- if status.application
= status.application.name
·
= link_to admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
- if status.edited?
·
= link_to t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')),
admin_account_status_path(status.account_id, status),
class: 'detailed-status__datetime'
- if status.discarded?
·
%span.negative-hint= t('admin.statuses.deleted')
·
= material_symbol visibility_icon(status)
= t("statuses.visibilities.#{status.visibility}")
·
= link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', rel: 'noopener' do
= t('admin.statuses.view_publicly')
- if status.proper.sensitive?
·
= material_symbol('visibility_off')
= t('stream_entries.sensitive_content')

View File

@ -57,7 +57,7 @@
- if @statuses.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
- if @report.unresolved?
%hr.spacer/

View File

@ -0,0 +1,40 @@
-# locals: (status:)
.status__card><
- if status.reblog?
.status__prepend
= material_symbol('repeat')
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id)))
- elsif status.reply? && status.in_reply_to_id.present?
.status__prepend
= material_symbol('reply')
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil))
= render partial: 'admin/shared/status_content', locals: { status: status.proper }
.detailed-status__meta
- if status.application
= status.application.name
·
= conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }><= l(status.created_at)
- if status.edited?
&nbsp;·
= conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status, { anchor: 'history' }), class: 'detailed-status__datetime' do
%span><= t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'relative-formatted'))
- if status.discarded?
&nbsp;·
%span.negative-hint= t('admin.statuses.deleted')
- unless status.reblog?
&nbsp;·
%span<
= material_symbol(visibility_icon(status))
= t("statuses.visibilities.#{status.visibility}")
- if status.proper.sensitive?
&nbsp;·
= material_symbol('visibility_off')
= t('stream_entries.sensitive_content')
- unless status.direct_visibility?
&nbsp;·
= link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', target: 'blank', rel: 'noopener' do
= t('admin.statuses.view_publicly')

View File

@ -0,0 +1,7 @@
- if status.with_media?
- if status.ordered_media_attachments.first.video?
= render_video_component(status, visible: false)
- elsif status.ordered_media_attachments.first.audio?
= render_audio_component(status)
- else
= render_media_gallery_component(status, visible: false)

View File

@ -0,0 +1,5 @@
.batch-table__row
%label.batch-table__row__select.batch-checkbox
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
.batch-table__row__content
= render partial: 'admin/shared/status', object: status

View File

@ -0,0 +1,10 @@
.status__content><
- if status.spoiler_text.present?
%details<
%summary><
%strong> Content warning: #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
= prerender_custom_emojis(status_content_format(status), status.emojis)
= render partial: 'admin/shared/status_attachments', locals: { status: status.proper }
- else
= prerender_custom_emojis(status_content_format(status), status.emojis)
= render partial: 'admin/shared/status_attachments', locals: { status: status.proper }

View File

@ -9,17 +9,7 @@
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)
.status
.status__content><
- if status_edit.spoiler_text.blank?
= prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis)
- else
%details<
%summary><
%strong> Content warning: #{prerender_custom_emojis(h(status_edit.spoiler_text), status_edit.emojis)}
= prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis)
- unless status_edit.ordered_media_attachments.empty?
= render partial: 'admin/reports/media_attachments', locals: { status: status_edit }
= render partial: 'admin/shared/status_content', locals: { status: status_edit }
.detailed-status__meta
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)

View File

@ -47,6 +47,6 @@
- if @statuses.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
= paginate @statuses

View File

@ -53,52 +53,11 @@
%h3= t('admin.statuses.contents')
.status__card
- if @status.reblog?
.status__prepend
= material_symbol('repeat')
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(@status.proper.account, path: admin_account_status_path(@status.proper.account.id, @status.proper.id)))
- elsif @status.reply? && @status.in_reply_to_id.present?
.status__prepend
= material_symbol('reply')
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(@status.in_reply_to_account, path: @status.thread.present? ? admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id) : nil))
.status__content><
- if @status.proper.spoiler_text.blank?
= prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis)
- else
%details<
%summary><
%strong> Content warning: #{prerender_custom_emojis(h(@status.proper.spoiler_text), @status.proper.emojis)}
= prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis)
- unless @status.proper.ordered_media_attachments.empty?
= render partial: 'admin/reports/media_attachments', locals: { status: @status.proper }
.detailed-status__meta
- if @status.application
= @status.application.name
·
%span.detailed-status__datetime
%time.formatted{ datetime: @status.created_at.iso8601, title: l(@status.created_at) }= l(@status.created_at)
- if @status.edited?
·
%span.detailed-status__datetime
= t('statuses.edited_at_html', date: content_tag(:time, l(@status.edited_at), datetime: @status.edited_at.iso8601, title: l(@status.edited_at), class: 'formatted'))
- if @status.discarded?
·
%span.negative-hint= t('admin.statuses.deleted')
- unless @status.reblog?
·
= material_symbol(visibility_icon(@status))
= t("statuses.visibilities.#{@status.visibility}")
- if @status.proper.sensitive?
·
= material_symbol('visibility_off')
= t('stream_entries.sensitive_content')
= render partial: 'admin/shared/status', object: @status
%hr.spacer/
%h3= t('admin.statuses.history')
%h3#history= t('admin.statuses.history')
- if @status.edits.empty?
%p= t('admin.statuses.no_history')
- else

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
# Disable httplog in production unless log_level is `debug`
if !Rails.env.production? || Rails.configuration.log_level == :debug
# Disable in production unless log level is `debug`
if Rails.env.local? || Rails.logger.debug?
require 'httplog'
HttpLog.configure do |config|

View File

@ -49,6 +49,10 @@ ia:
attributes:
reblog:
taken: del message jam existe
terms_of_service:
attributes:
effective_date:
too_soon: es troppo tosto, debe esser plus tarde que %{date}
user:
attributes:
date_of_birth:

View File

@ -72,7 +72,7 @@ fa:
revoke: آیا مطمئن هستید؟
index:
authorized_at: تایید شده در %{date}
description_html: اینها نرم‌افزار هایی هستند که می‌توانند به حساب کاربری شما با استفاده از رابط نرم‌افزاری دسترسی پیدا کنند. اگر نرم‌افزار های در اینجا هستند که نمی‌شناسید، یا نرم‌افزاری که رفتار مشکوک دارد، می‌توانید دسترسی اش را باطل کنید.
description_html: اینها برنامه‌هاییند که می‌توانند با استفاده از میانای برنامه‌نویسی به حسابتان دسترسی پیدا کنند. اگر برنامه‌ای هست که نمی‌شناسیدش یا رفتار بدی دارد می‌توانید دسترسیش را باطل کنید.
last_used_at: آخرین استفاده در %{date}
never_used: هرگز استفاده نشده
scopes: اجازه‌ها

View File

@ -1915,7 +1915,7 @@ en:
public: Everyone
title: '%{name}: "%{quote}"'
visibilities:
direct: Direct
direct: Private mention
private: Followers-only
private_long: Only show to followers
public: Public

View File

@ -2090,4 +2090,4 @@ eo:
not_enabled: Vi ankoraŭ ne ŝaltis WebAuth
not_supported: Ĉi tiu legilo ne povas uzi sekurecŝlosilojn
otp_required: Por uzi sekurecŝlosilojn, ebligu 2-faktoran autentigon unue.
registered_on: Registrigita je %{date}
registered_on: Registrita je %{date}

View File

@ -190,6 +190,7 @@ ia:
create_relay: Crear repetitor
create_unavailable_domain: Crear dominio indisponibile
create_user_role: Crear un rolo
create_username_block: Crear regula de nomine de usator
demote_user: Degradar usator
destroy_announcement: Deler annuncio
destroy_canonical_email_block: Deler blocada de e-mail
@ -203,6 +204,7 @@ ia:
destroy_status: Deler message
destroy_unavailable_domain: Deler dominio indisponibile
destroy_user_role: Destruer rolo
destroy_username_block: Deler regula de nomine de usator
disable_2fa_user: Disactivar A2F
disable_custom_emoji: Disactivar emoji personalisate
disable_relay: Disactivar repetitor
@ -237,6 +239,7 @@ ia:
update_report: Actualisar le reporto
update_status: Actualisar le message
update_user_role: Actualisar rolo
update_username_block: Actualisar regula de nomine de usator
actions:
approve_appeal_html: "%{name} approbava appello del decision de moderation de %{target}"
approve_user_html: "%{name} approbava inscription de %{target}"
@ -255,6 +258,7 @@ ia:
create_relay_html: "%{name} ha create un repetitor %{target}"
create_unavailable_domain_html: "%{name} stoppava livration al dominio %{target}"
create_user_role_html: "%{name} creava rolo de %{target}"
create_username_block_html: "%{name} ha addite un regula pro nomines de usator que contine %{target}"
demote_user_html: "%{name} degradava usator %{target}"
destroy_announcement_html: "%{name} deleva annuncio %{target}"
destroy_canonical_email_block_html: "%{name} disblocava le e-mail con le hash %{target}"
@ -268,6 +272,7 @@ ia:
destroy_status_html: "%{name} removeva un message de %{target}"
destroy_unavailable_domain_html: "%{name} reprendeva le livration al dominio %{target}"
destroy_user_role_html: "%{name} deleva le rolo %{target}"
destroy_username_block_html: "%{name} ha removite un regula pro nomines de usator que contine %{target}"
disable_2fa_user_html: "%{name} disactivava le authentication a duo factores pro le usator %{target}"
disable_custom_emoji_html: "%{name} disactivava le emoji %{target}"
disable_relay_html: "%{name} ha disactivate le repetitor %{target}"
@ -302,6 +307,7 @@ ia:
update_report_html: "%{name} actualisava reporto %{target}"
update_status_html: "%{name} actualisava message de %{target}"
update_user_role_html: "%{name} cambiava rolo de %{target}"
update_username_block_html: "%{name} ha actualisate un regula pro nomines de usator que contine %{target}"
deleted_account: conto delite
empty: Nulle registros trovate.
filter_by_action: Filtrar per action
@ -321,7 +327,7 @@ ia:
preview:
disclaimer: Proque le usatores non pote optar pro non reciper los, le notificationes per e-mail debe limitar se a annuncios importante tal como le notificationes de violation de datos personal o de clausura de servitores.
explanation_html: 'Le e-mail essera inviate a <strong>%{display_count} usatores</strong>. Le sequente texto essera includite in le e-mail:'
title: Previder le notification de annuncio
title: Previsualisar le notification de annuncio
publish: Publicar
published_msg: Annuncio publicate con successo!
scheduled_for: Programmate pro %{time}
@ -480,6 +486,36 @@ ia:
new:
title: Importar blocadas de dominio
no_file: Necun file seligite
fasp:
debug:
callbacks:
created_at: Data de creation
delete: Deler
ip: Adresse IP
request_body: Corpore del requesta
title: Depurar le appellos de retorno
providers:
active: Active
base_url: URL de base
callback: Appello de retorno
delete: Deler
edit: Modificar fornitor
finish_registration: Terminar registration
name: Nomine
providers: Fornitores
public_key_fingerprint: Dactylogramma del clave public
registration_requested: Registration requestate
registrations:
confirm: Confirmar
description: Tu ha recipite un registration de un FSAF. Rejecta lo si tu non lo ha initiate. Si tu lo ha initiate, compara attentemente le nomine e le dactylogramma del clave ante de confirmar le registration.
reject: Rejectar
title: Confirmar registration FSAF
save: Salvar
select_capabilities: Selige capacitates
sign_in: Aperir session
status: Stato
title: Fornitores de Servicios Auxiliar del Fediverso
title: FSAF
follow_recommendations:
description_html: "<strong>Le recommendationes de sequimento adjuta le nove usatores a trovar rapidemente contento interessante.</strong> Quando un usator non ha un historia sufficiente de interactiones con alteres pro formar recommendationes personalisate de sequimento, iste contos es recommendate. Illos se recalcula cata die a partir de un mixtura de contos con le plus grande numero de ingagiamentos recente e le numero de sequitores local le plus alte pro un lingua date."
language: Pro le lingua
@ -548,6 +584,13 @@ ia:
all: Toto
limited: Limitate
title: Moderation
moderation_notes:
create: Adder nota de moderation
created_msg: Le nota de moderation del servitor ha essite create.
description_html: Vider e lassar notas pro altere moderatores e pro referentia futur
destroyed_msg: Le nota de moderation del servitor ha essite delite.
placeholder: Informationes sur iste servitor, actiones prendite o altere cosas que pote adjutar te a moderar iste servitor in le futuro.
title: Notas de moderation
private_comment: Commento private
public_comment: Commento public
purge: Purgar
@ -756,11 +799,17 @@ ia:
title: Rolos
rules:
add_new: Adder regula
add_translation: Adder traduction
delete: Deler
description_html: Ben que multes affirma de haber legite e acceptate le conditiones de servicio, generalmente le gente non los lege completemente usque un problema surge. <strong>Facilita le visibilitate del regulas de tu servitor in un colpo de oculo forniente los in un lista a punctos.</strong> Tenta mantener le regulas individual curte e simple, ma sin divider los in multe punctos separate.
edit: Modificar regula
empty: Necun regula del servitor ha essite definite ancora.
move_down: Displaciar a basso
move_up: Displaciar in alto
title: Regulas del servitor
translation: Traduction
translations: Traductiones
translations_explanation: Tu ha le option de adder traductiones al regulas. Le valor predefinite apparera si necun version traducite es disponibile. Per favor sempre assecura te que omne traduction fornite es ben synchronisate con le valor predefinite.
settings:
about:
manage_rules: Gerer le regulas del servitor
@ -877,7 +926,7 @@ ia:
database_schema_check:
message_html: Il ha migrationes de base de datos pendente. Per favor exeque los pro assecurar que le application se comporta como expectate
elasticsearch_analysis_index_mismatch:
message_html: Le parametros del analisator del indice de Elasticsearch es obsolete. Executa <code>tootctl search deploy --only-mapping --only=%{value}</code>
message_html: Le parametros del analysator del indice de Elasticsearch es obsolete. Executa <code>tootctl search deploy --only-mapping --only=%{value}</code>
elasticsearch_health_red:
message_html: Le aggregation Elasticsearch es malsan (stato rubie), le functiones de recerca es indisponibile
elasticsearch_health_yellow:
@ -942,7 +991,7 @@ ia:
changelog: Lo que ha cambiate
create: Usar tu proprie
current: Actual
draft: Provisori
draft: Esbosso
generate: Usar modello
generates:
action: Generar
@ -965,7 +1014,7 @@ ia:
title: Previsualisar le notification sur le conditiones de servicio
publish: Publicar
published_on_html: Publicate le %{date}
save_draft: Salvar version provisori
save_draft: Salvar esbosso
title: Conditiones de servicio
title: Administration
trends:
@ -1043,8 +1092,17 @@ ia:
title: Recommendationes e tendentias
trending: In tendentia
username_blocks:
add_new: Adder nove
block_registrations: Blocar registrationes
comparison:
contains: Contine
equals: Es equal a
contains_html: Contine %{string}
created_msg: Regula de nomine de usator create con successo
delete: Deler
edit:
title: Modificar regula de nomine de usator
matches_exactly_html: Es equal a %{string}
new:
create: Crear regula
title: Crear nove regula de nomine de usator
@ -1316,6 +1374,10 @@ ia:
basic_information: Information basic
hint_html: "<strong>Personalisa lo que le personas vide sur tu profilo public e presso tu messages.</strong> Il es plus probabile que altere personas te seque e interage con te quando tu ha un profilo complete e un photo."
other: Alteres
emoji_styles:
auto: Automatic
native: Native
twemoji: Twemoji
errors:
'400': Le requesta que tu ha inviate non es valide o es mal formate.
'403': Tu non ha le permission de acceder a iste pagina.
@ -1625,6 +1687,10 @@ ia:
title: Nove mention
poll:
subject: Un sondage de %{name} ha finite
quote:
body: 'Tu message ha essite citate per %{name}:'
subject: "%{name} ha citate tu message"
title: Nove citation
reblog:
body: "%{name} ha impulsate tu message:"
subject: "%{name} ha impulsate tu message"
@ -1835,6 +1901,7 @@ ia:
edited_at_html: Modificate le %{date}
errors:
in_reply_not_found: Le message a que tu tenta responder non pare exister.
quoted_status_not_found: Le message que tu tenta citar non pare exister.
over_character_limit: limite de characteres de %{max} excedite
pin_errors:
direct: Messages que es solo visibile a usatores mentionate non pote esser appunctate
@ -1842,6 +1909,8 @@ ia:
ownership: Le message de alcuno altere non pote esser appunctate
reblog: Un impulso non pote esser affixate
quote_policies:
followers: Solmente tu sequitores
nobody: Necuno
public: Omnes
title: "%{name}: “%{quote}”"
visibilities:
@ -1897,7 +1966,7 @@ ia:
terms_of_service:
title: Conditiones de servicio
terms_of_service_interstitial:
future_preamble_html: Nos face alcun cambios a nostre conditiones de servicio, que entrara in vigor in <strong>%{date}</strong>. Nos te invita a revider le conditiones actualisate.
future_preamble_html: Nos apporta alcun modificationes a nostre conditiones de servicio, le quales entrara in vigor le <strong>%{date}</strong>. Nos te invita a revider le conditiones actualisate.
past_preamble_html: Nos ha cambiate nostre conditiones de servicio desde tu ultime visita. Nos te invita a revider le conditiones actualisate.
review_link: Revider le conditiones de servicio
title: Le conditiones de servicio de %{domain} cambia
@ -1969,7 +2038,7 @@ ia:
agreement: Si tu continua a usar %{domain}, tu accepta iste conditiones. Si tu non es de accordo con le conditiones actualisate, tu pote sempre eliminar tu conto pro terminar tu accordo con %{domain}.
changelog: 'In summario, ecce lo que iste actualisation significa pro te:'
description: 'Tu recipe iste e-mail proque nos face alcun cambios al nostre conditiones de servicio a %{domain}. Iste actualisationes entrara in vigor in %{date}. Nos te invita a revider integralmente le conditiones actualisate hic:'
description_html: Tu recipe iste e-mail proque nos face alcun cambios al nostre conditiones de servicio a %{domain}. Iste actualisationes entrara in vigor in <strong>%{date}</strong>. Nos te invita a revider <a href="%{path}" target="_blank">integralmente le conditiones actualisate hic</a>.
description_html: Tu recipe iste message perque nos apporta alcun modificationes a nostre conditiones de servicio sur %{domain}. Iste actualisationes entrara in vigor le <strong>%{date}</strong>. Nos te invita a <a href="%{path}" target="_blank">leger integralmente le conditiones actualisate</a>.
sign_off: Le equipa de %{domain}
subject: Actualisationes de nostre conditiones de servicio
subtitle: Le conditiones de servicio de %{domain} ha cambiate

View File

@ -1,7 +1,7 @@
---
ru:
about:
about_mastodon_html: 'Социальная сеть будущего: никакой рекламы или слежки со стороны корпораций, этичный дизайн и децентрализация! С Mastodon ваши данные находятся только под вашим контролем!'
about_mastodon_html: 'Социальная сеть будущего: никакой рекламы или слежки со стороны корпораций, этичный дизайн и децентрализация. С Mastodon ваши данные находятся только под вашим контролем!'
contact_missing: Не указано
contact_unavailable: N/A
hosted_on: Сервер Mastodon на сайте %{domain}
@ -350,9 +350,9 @@ ru:
copy: Копировать
copy_failed_msg: Не удалось создать локальную копию эмодзи
create_new_category: Создать новую категорию
created_msg: Эмодзи добавлен!
created_msg: Эмодзи добавлен
delete: Удалить
destroyed_msg: Эмодзи удалён!
destroyed_msg: Эмодзи удалён
disable: Отключить
disabled: Отключён
disabled_msg: Эмодзи отключён
@ -375,7 +375,7 @@ ru:
unlist: Из списка
unlisted: Не в списке
update_failed_msg: Не удалось обновить эмодзи
updated_msg: Эмодзи обновлён!
updated_msg: Эмодзи обновлён
upload: Загрузить
dashboard:
active_users: активные пользователи
@ -1247,7 +1247,7 @@ ru:
delete_account: Удалить учётную запись
delete_account_html: Вы можете <a href="%{path}">удалить свою учётную запись</a>. Перед удалением у вас будет запрошено подтверждение.
description:
prefix_invited_by_user: Вы получили приглашение на сервер Mastodon от @%{name}!
prefix_invited_by_user: Вы получили приглашение на сервер Mastodon от @%{name}.
prefix_sign_up: Зарегистрируйтесь в Mastodon прямо сейчас!
suffix: С учётной записью вы сможете подписываться на людей, публиковать посты и обмениваться сообщениями с пользователями любого сервера Mastodon — и не только!
didnt_get_confirmation: Не получили письмо со ссылкой для подтверждения?
@ -1497,7 +1497,7 @@ ru:
one: Выбран <strong>%{count}</strong> элемент, соответствующий вашему запросу.
other: Выбраны все <strong>%{count}</strong> элементов, соответствующих вашему запросу.
cancel: Отмена
changes_saved_msg: Изменения сохранены!
changes_saved_msg: Изменения сохранены
confirm: Подтвердить
copy: Копировать
delete: Удалить
@ -1731,7 +1731,7 @@ ru:
subject: "%{name} добавил(а) ваш пост в избранное"
title: Ваш пост добавили в избранное
follow:
body: "%{name} теперь подписан(а) на вас!"
body: "%{name} теперь подписан(а) на вас"
subject: "%{name} теперь подписан(а) на вас"
title: Новый подписчик
follow_request:
@ -1965,7 +1965,7 @@ ru:
many: 'содержались запрещённые хэштеги: %{tags}'
one: 'содержался запрещённый хэштег: %{tags}'
other: 'содержались запрещённые хэштеги: %{tags}'
edited_at_html: 'Дата последнего изменения: %{date}'
edited_at_html: 'Последнее изменение: %{date}'
errors:
in_reply_not_found: Пост, на который вы собирались ответить, был удалён или не существует.
quoted_status_not_found: Пост, который вы собирались процитировать, был удалён или не существует.

View File

@ -56,10 +56,12 @@ ia:
scopes: Le APIs al quales le application habera accesso. Si tu selige un ambito de nivello superior, non es necessari seliger ambitos individual.
setting_aggregate_reblogs: Non monstrar nove impulsos pro messages que ha essite recentemente impulsate (affecta solmente le impulsos novemente recipite)
setting_always_send_emails: Normalmente, le notificationes de e-mail non es inviate quando tu activemente usa Mastodon
setting_default_quote_policy: Iste parametro solo habera effecto pro messages create con le proxime version de Mastodon, ma tu pote seliger tu preferentia anticipatemente.
setting_default_sensitive: Le medios sensibile es celate de ordinario e pote esser revelate con un clic
setting_display_media_default: Celar le medios marcate como sensibile
setting_display_media_hide_all: Sempre celar contento multimedial
setting_display_media_show_all: Sempre monstrar contento multimedial
setting_emoji_style: Como monstrar emojis. “Automatic” tentara usar emojis native, ma recurre al Twemojis pro navigatores ancian.
setting_system_scrollbars_ui: Se applica solmente al navigatores de scriptorio basate sur Safari e Chrome
setting_use_blurhash: Le imagines degradate se basa sur le colores del visuales celate, ma illos offusca tote le detalios
setting_use_pending_items: Requirer un clic pro monstrar nove messages in vice de rolar automaticamente le fluxo
@ -134,16 +136,23 @@ ia:
name: Tu pote solmente cambiar le litteras inter majusculas e minusculas, per exemplo, pro render lo plus legibile
terms_of_service:
changelog: Pote esser structurate con le syntaxe Markdown.
effective_date: Un periodo rationabile pote variar inter 10 e 30 dies post le data al qual tu notifica tu usatores.
text: Pote esser structurate con le syntaxe Markdown.
terms_of_service_generator:
admin_email: Le avisos juridic include le contra-avisos, ordinantias judiciari, demandas de retiro e demandas de application del lege.
arbitration_address: Pote esser le mesme que le adresse physic supra, o “N/A” si se usa e-mail.
arbitration_website: Pote esser un formulario web, o “N/A” si se usa e-mail.
choice_of_law: Citate, region, territorio o stato cuje leges substantive interne governa omne disputa juridic.
dmca_address: Pro operatores in le SUA, usa le adresse registrate in le Directorio de Agentes Designate pro le DMCA (DMCA Designated Agent Directory). Un adresse de cassa postal es disponibile per requesta directe; usa le Requesta de Exemption de Cassa Postal pro Agentes Designate del DMCA (DMCA Designated Agent Post Office Box Waiver Request) pro inviar un message electronic al Officio del Derecto de Autor (Copyright Office) e describer que tu es un moderator de contento que travalia de casa e qui time vengiantias o represalias pro tu actiones, necessitante le uso un cassa postal pro remover tu adresse personal del vista public.
dmca_email: Pote esser le mesme adresse de e-mail usate pro “Adresse de e-mail pro avisos juridic” supra.
domain: Identification unic del servicio in linea que tu forni.
jurisdiction: Indica le pais ubi vive le persona qui paga le facturas. Si se tracta de un interprisa o altere organisation, indica le pais ubi illo es incorporate, e le citate, region, territorio o stato del maniera appropriate pro le pais.
min_age: Non deberea esser infra le etate minime requirite per le leges de tu jurisdiction.
user:
chosen_languages: Si marcate, solmente le messages in le linguas seligite apparera in chronologias public
date_of_birth:
one: Nos debe assecurar que tu ha al minus %{count} anno pro usar %{domain}. Nos non va immagazinar isto.
other: Nos debe assecurar que tu ha al minus %{count} annos pro usar %{domain}. Nos non va immagazinar isto.
role: Le rolo controla qual permissos le usator ha.
user_role:
color: Color a esser usate pro le rolo in omne parte del UI, como RGB in formato hexadecimal
@ -151,6 +160,10 @@ ia:
name: Nomine public del rolo, si rolo es definite a esser monstrate como insignia
permissions_as_keys: Usatores con iste rolo habera accesso a...
position: Rolo superior decide resolution de conflicto in certe situationes. Certe actiones pote solo esser exequite sur rolos con un prioritate inferior
username_block:
allow_with_approval: In loco de impedir totalmente le inscription, le inscriptiones correspondente requirera tu approbation
comparison: Tene in conto le “problema Scunthorpe” si tu bloca correspondentias partial
username: Correspondera independentemente de majusculas e minusculas e de homoglyphos commun como "4" pro "a" or "3" pro "e"
webhook:
events: Selige le eventos a inviar
template: Compone tu proprie carga utile JSON per interpolation de variabile. Lassar blanc pro JSON predefinite.
@ -233,6 +246,7 @@ ia:
setting_display_media_default: Predefinite
setting_display_media_hide_all: Celar toto
setting_display_media_show_all: Monstrar toto
setting_emoji_style: Stilo de emojis
setting_expand_spoilers: Sempre expander messages marcate con avisos de contento
setting_hide_network: Cela tu rete social
setting_missing_alt_text_modal: Monstrar un dialogo de confirmation ante de publicar multimedia sin texto alternative
@ -271,6 +285,7 @@ ia:
favicon: Favicon
mascot: Personalisar le mascotte (hereditage)
media_cache_retention_period: Periodo de retention del cache multimedial
min_age: Etate minime requirite
peers_api_enabled: Publicar le lista de servitores discoperite in le API
profile_directory: Activar directorio de profilos
registrations_mode: Qui pote inscriber se
@ -314,6 +329,7 @@ ia:
follow_request: Alcuno ha demandate de sequer te
mention: Alcuno te ha mentionate
pending_account: Nove conto besonia de revision
quote: Alcuno te ha citate
reblog: Alcuno ha impulsate tu message
report: Un nove reporto es inviate
software_updates:
@ -360,6 +376,10 @@ ia:
name: Nomine
permissions_as_keys: Permissiones
position: Prioritate
username_block:
allow_with_approval: Permitter registrationes con approbation
comparison: Methodo de comparation
username: Parola a comparar
webhook:
events: Eventos activate
template: Modello de carga utile

View File

@ -56,6 +56,7 @@ sv:
scopes: 'Vilka API: er applikationen kommer tillåtas åtkomst till. Om du väljer en omfattning på högstanivån behöver du inte välja individuella sådana.'
setting_aggregate_reblogs: Visa inte nya boostar för inlägg som nyligen blivit boostade (påverkar endast nymottagna boostar)
setting_always_send_emails: E-postnotiser kommer vanligtvis inte skickas när du aktivt använder Mastodon
setting_default_quote_policy: Denna inställning kommer bara att träda i kraft för inlägg som skapats med nästa Mastodon-version, men du kan förbereda dina önskemål.
setting_default_sensitive: Känslig media döljs som standard och kan visas med ett klick
setting_display_media_default: Dölj media markerad som känslig
setting_display_media_hide_all: Dölj alltid all media
@ -149,6 +150,9 @@ sv:
min_age: Bör inte vara lägre än den minimiålder som krävs enligt lagarna i din jurisdiktion.
user:
chosen_languages: Vid aktivering visas bara inlägg på dina valda språk i offentliga tidslinjer
date_of_birth:
one: Vi måste se till att du är minst %{count} för att använda %{domain}. Vi sparar inte denna information.
other: Vi måste se till att du är minst %{count} för att använda %{domain}. Vi lagrar inte denna information.
role: Rollen styr vilka behörigheter användaren har.
user_role:
color: Färgen som ska användas för rollen i användargränssnittet, som RGB i hex-format
@ -156,6 +160,10 @@ sv:
name: Offentligt namn på rollen, om rollen är inställd på att visas som ett emblem
permissions_as_keys: Användare med denna roll kommer ha tillgång till...
position: Högre roll avgör konfliktlösning i vissa situationer. Vissa åtgärder kan endast utföras på roller med lägre prioritet
username_block:
allow_with_approval: I stället för att förhindra direkt registrering kräver matchning av registreringar ditt godkännande
comparison: Var uppmärksam på Scunthorpe Problem vid blockering av partiella matcher
username: Matchas oavsett versaler och gemena bokstäver som ”4” för ”a” eller 3 för ”e”
webhook:
events: Välj händelser att skicka
template: Skriv din egen JSON-nyttolast med variabel interpolation. Lämna tomt för förvald JSON.
@ -321,6 +329,7 @@ sv:
follow_request: Någon begärt att följa dig
mention: Någon nämnt dig
pending_account: Ett nytt konto behöver granskas
quote: Någon citerade dig
reblog: Någon boostade ditt inlägg
report: En ny rapport har skickats
software_updates:
@ -369,6 +378,7 @@ sv:
position: Prioritet
username_block:
allow_with_approval: Tillåt registreringar med godkännande
comparison: Jämförelsesätt
username: Ord att matcha
webhook:
events: Aktiverade händelser

View File

@ -258,6 +258,7 @@ sv:
create_relay_html: "%{name} skapade ombudet %{target}"
create_unavailable_domain_html: "%{name} stoppade leverans till domänen %{target}"
create_user_role_html: "%{name} skapade rollen %{target}"
create_username_block_html: "%{name} lade till regel för användarnamn som innehåller %{target}"
demote_user_html: "%{name} nedgraderade användare %{target}"
destroy_announcement_html: "%{name} raderade kungörelsen %{target}"
destroy_canonical_email_block_html: "%{name} avblockerade e-post med hash%{target}"
@ -271,6 +272,7 @@ sv:
destroy_status_html: "%{name} tog bort inlägget av %{target}"
destroy_unavailable_domain_html: "%{name} återupptog leverans till domänen %{target}"
destroy_user_role_html: "%{name} raderade rollen %{target}"
destroy_username_block_html: "%{name} borttagna regler för användarnamn som innehåller %{target}"
disable_2fa_user_html: "%{name} inaktiverade tvåfaktorsautentiseringskrav för användaren %{target}"
disable_custom_emoji_html: "%{name} inaktiverade emoji %{target}"
disable_relay_html: "%{name} inaktiverade ombudet %{target}"
@ -305,6 +307,7 @@ sv:
update_report_html: "%{name} uppdaterade rapporten %{target}"
update_status_html: "%{name} uppdaterade inlägget av %{target}"
update_user_role_html: "%{name} ändrade rollen %{target}"
update_username_block_html: "%{name} uppdaterade regel för användarnamn som innehåller %{target}"
deleted_account: raderat konto
empty: Inga loggar hittades.
filter_by_action: Filtrera efter åtgärd
@ -1090,15 +1093,23 @@ sv:
trending: Trendande
username_blocks:
add_new: Lägg till ny
block_registrations: Registrerade blockeringar
comparison:
contains: Innehåller
equals: Lika
contains_html: Innehåller %{string}
created_msg: Användarnamnsregel har skapats
delete: Ta bort
edit:
title: Redigera användarnamnsregel
matches_exactly_html: Lika med %{string}
new:
create: Skapa regel
title: Skapa ny användarnamnsregel
no_username_block_selected: Inga användarnamn regler ändrades eftersom ingen valdes
not_permitted: Ej tillåtet
title: Regler för användarnamn
updated_msg: Uppdaterade användarnamnsregel
warning_presets:
add_new: Lägg till ny
delete: Radera
@ -1676,6 +1687,10 @@ sv:
title: Ny omnämning
poll:
subject: En undersökning av %{name} har avslutats
quote:
body: 'Ditt inlägg citerades av %{name}:'
subject: "%{name} citerade ditt inlägg"
title: Nytt citat
reblog:
body: 'Ditt inlägg boostades av %{name}:'
subject: "%{name} boostade ditt inlägg"

View File

@ -5945,9 +5945,9 @@ __metadata:
linkType: hard
"core-js@npm:^3.30.2, core-js@npm:^3.45.0":
version: 3.45.0
resolution: "core-js@npm:3.45.0"
checksum: 10c0/118350f9f1d81f42a1276590d6c217dca04c789fdb8074c82e53056b1a784948769a62b16b98493fd73e8a988545432f302bca798571e56ad881b9c039a5a83c
version: 3.45.1
resolution: "core-js@npm:3.45.1"
checksum: 10c0/c38e5fae5a05ee3a129c45e10056aafe61dbb15fd35d27e0c289f5490387541c89741185e0aeb61acb558559c6697e016c245cca738fa169a73f2b06cd30e6b6
languageName: node
linkType: hard
@ -13818,8 +13818,8 @@ __metadata:
linkType: hard
"vite-plugin-pwa@npm:^1.0.2":
version: 1.0.2
resolution: "vite-plugin-pwa@npm:1.0.2"
version: 1.0.3
resolution: "vite-plugin-pwa@npm:1.0.3"
dependencies:
debug: "npm:^4.3.6"
pretty-bytes: "npm:^6.1.1"
@ -13834,13 +13834,13 @@ __metadata:
peerDependenciesMeta:
"@vite-pwa/assets-generator":
optional: true
checksum: 10c0/e4f2f4dfff843ee2585a0d89e74187168ba20da77cd0d127ce7ad7eebcf5a68b2bf09000afb6bb86d43a2034fea9f568cd6db2a2d4b47a72e175d999a5e07eb1
checksum: 10c0/03fc24bd12ae4a4130979da4877e3dabddf13d7d6ff2bd68e5e8497a2643b8874a78e6c2502874277ddf2f28593d0a3b025d78af2335bdcc5d2966295784fd46
languageName: node
linkType: hard
"vite-plugin-static-copy@npm:^3.1.1":
version: 3.1.1
resolution: "vite-plugin-static-copy@npm:3.1.1"
version: 3.1.2
resolution: "vite-plugin-static-copy@npm:3.1.2"
dependencies:
chokidar: "npm:^3.6.0"
fs-extra: "npm:^11.3.0"
@ -13849,7 +13849,7 @@ __metadata:
tinyglobby: "npm:^0.2.14"
peerDependencies:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
checksum: 10c0/a4dd5d31212b037d4902d55c2ee83866e496857bf948f258599dc24ec61b4628cf0dd23e4a7d7dc189d33ad1489427e976fa95e4db61b394d0be4f63077ef92c
checksum: 10c0/1a65f4c9d291cc27483a5b225b1ac5610edc3aa2f13fa3a76a77327874c83bbee52e1011ee0bf5b0168b9b7b974213d49fe800e44af398cfbcb6607814b45c5b
languageName: node
linkType: hard